Compare commits
3 Commits
3ce3335695
...
f7c72626ee
Author | SHA1 | Date | |
---|---|---|---|
f7c72626ee | |||
e66d800881 | |||
9acc9a03aa |
11
Makefile
11
Makefile
@ -3,6 +3,15 @@ BINARIES = $(patsubst cmd/%/,%,$(wildcard cmd/*/))
|
||||
|
||||
.PHONY: all build test validate clean run $(BINARIES)
|
||||
|
||||
VERSION := $(shell git describe --tags --always --dirty)
|
||||
COMMIT := $(shell git rev-parse --short HEAD)
|
||||
COMMIT_DATETIME := $(shell git log -1 --format=%cd --date=iso8601)
|
||||
|
||||
LDFLAGS := -X git.omicron.one/omicron/linkshare/internal/version.Version=$(VERSION) \
|
||||
-X git.omicron.one/omicron/linkshare/internal/version.GitCommit=$(COMMIT) \
|
||||
-X "git.omicron.one/omicron/linkshare/internal/version.CommitDateTime=$(COMMIT_DATETIME)"
|
||||
|
||||
|
||||
all: build
|
||||
|
||||
|
||||
@ -13,7 +22,7 @@ $(BINARY_DIR):
|
||||
mkdir -p $(BINARY_DIR)
|
||||
|
||||
$(BINARIES): %: $(BINARY_DIR)
|
||||
go build -o $(BINARY_DIR)/$@ ./cmd/$@/
|
||||
go build -ldflags '$(LDFLAGS)' -o $(BINARY_DIR)/$@ ./cmd/$@/
|
||||
|
||||
test:
|
||||
go test ./...
|
||||
|
27
cmd/linkctl/config.go
Normal file
27
cmd/linkctl/config.go
Normal file
@ -0,0 +1,27 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func configPreRun(cmd *cobra.Command, args []string) error {
|
||||
return setupDb()
|
||||
}
|
||||
|
||||
func configPostRun(cmd *cobra.Command, args []string) error {
|
||||
return cleanupDb()
|
||||
}
|
||||
|
||||
func configSetHandler(cmd *cobra.Command, args []string) {
|
||||
fmt.Println("Not implemented")
|
||||
}
|
||||
|
||||
func configGetHandler(cmd *cobra.Command, args []string) {
|
||||
fmt.Println("Not implemented")
|
||||
}
|
||||
|
||||
func configListHandler(cmd *cobra.Command, args []string) {
|
||||
fmt.Println("Not implemented")
|
||||
}
|
48
cmd/linkctl/db.go
Normal file
48
cmd/linkctl/db.go
Normal file
@ -0,0 +1,48 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.omicron.one/omicron/linkshare/internal/database"
|
||||
"git.omicron.one/omicron/linkshare/internal/util"
|
||||
"git.omicron.one/omicron/linkshare/internal/version"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func openDB() (*database.DB, error) {
|
||||
paths, err := util.FindDirectories(dbPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return database.Open(paths.DatabaseFile)
|
||||
}
|
||||
|
||||
func dbPreRun(cmd *cobra.Command, args []string) error {
|
||||
return setupDb()
|
||||
}
|
||||
|
||||
func dbPostRun(cmd *cobra.Command, args []string) error {
|
||||
return cleanupDb()
|
||||
}
|
||||
|
||||
func dbInitHandler(cmd *cobra.Command, args []string) {
|
||||
err := db.Initialize(paths.SchemaDir)
|
||||
if err == database.ErrAlreadyInitialized {
|
||||
fmt.Printf("Database %q is already initialized\n", dbPath)
|
||||
return
|
||||
}
|
||||
if err == nil {
|
||||
fmt.Printf("Initialized database %q with schema version %d\n", dbPath, version.SchemaVersion)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Failed to initialize database %q: %v\n", dbPath, err)
|
||||
}
|
||||
|
||||
func dbBackupHandler(cmd *cobra.Command, args []string) {
|
||||
fmt.Println("Not implemented")
|
||||
}
|
||||
|
||||
func dbUpdateHandler(cmd *cobra.Command, args []string) {
|
||||
fmt.Println("Not implemented")
|
||||
}
|
@ -2,17 +2,156 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"git.omicron.one/omicron/linkshare/internal/database"
|
||||
"git.omicron.one/omicron/linkshare/internal/util"
|
||||
"git.omicron.one/omicron/linkshare/internal/version"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func main() {
|
||||
paths, err := util.FindDirectories("")
|
||||
var (
|
||||
dbPath string
|
||||
verbosity int
|
||||
)
|
||||
|
||||
var (
|
||||
paths *util.AppPaths
|
||||
db *database.DB
|
||||
)
|
||||
|
||||
func exitIfError(err error) {
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func setupPaths() error {
|
||||
if paths != nil {
|
||||
return nil
|
||||
}
|
||||
paths_, err := util.FindDirectories(dbPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
paths = paths_
|
||||
return nil
|
||||
}
|
||||
|
||||
func setupDb() error {
|
||||
if db != nil {
|
||||
return nil
|
||||
}
|
||||
err := setupPaths()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Println("Paths:")
|
||||
fmt.Println(" Schema:", paths.SchemaDir)
|
||||
fmt.Println(" Database:", paths.DatabaseFile)
|
||||
db_, err := database.Open(dbPath)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
db = db_
|
||||
return nil
|
||||
}
|
||||
|
||||
func cleanupDb() error {
|
||||
if db != nil {
|
||||
err := db.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "linkctl",
|
||||
Short: "LinkShare CLI tool",
|
||||
Long: `Command line tool to manage your self-hosted LinkShare service.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmd.Help()
|
||||
},
|
||||
}
|
||||
rootCmd.CompletionOptions.DisableDefaultCmd = true
|
||||
|
||||
rootCmd.PersistentFlags().StringVarP(&dbPath, "db", "d", "", "Database file path")
|
||||
rootCmd.PersistentFlags().CountVarP(&verbosity, "verbose", "v", "Increase verbosity level")
|
||||
|
||||
configCmd := &cobra.Command{
|
||||
Use: "config",
|
||||
Short: "Configuration commands",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmd.Help()
|
||||
},
|
||||
PersistentPreRunE: configPreRun,
|
||||
PersistentPostRunE: configPostRun,
|
||||
}
|
||||
|
||||
configSetCmd := &cobra.Command{
|
||||
Use: "set",
|
||||
Short: "Set a configuration value",
|
||||
Run: configSetHandler,
|
||||
}
|
||||
|
||||
configGetCmd := &cobra.Command{
|
||||
Use: "get",
|
||||
Short: "Get a configuration value",
|
||||
Run: configGetHandler,
|
||||
}
|
||||
|
||||
configListCmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all configuration values",
|
||||
Run: configListHandler,
|
||||
}
|
||||
|
||||
configCmd.AddCommand(configSetCmd, configGetCmd, configListCmd)
|
||||
|
||||
dbCmd := &cobra.Command{
|
||||
Use: "db",
|
||||
Short: "Database commands",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmd.Help()
|
||||
},
|
||||
PersistentPreRunE: dbPreRun,
|
||||
PersistentPostRunE: dbPostRun,
|
||||
}
|
||||
|
||||
dbInitCmd := &cobra.Command{
|
||||
Use: "init",
|
||||
Short: "Initialize the database",
|
||||
Run: dbInitHandler,
|
||||
}
|
||||
|
||||
dbBackupCmd := &cobra.Command{
|
||||
Use: "backup",
|
||||
Short: "Backup the database",
|
||||
Run: dbBackupHandler,
|
||||
}
|
||||
|
||||
dbUpdateCmd := &cobra.Command{
|
||||
Use: "update",
|
||||
Short: "Update the database schema",
|
||||
Run: dbUpdateHandler,
|
||||
}
|
||||
|
||||
dbCmd.AddCommand(dbInitCmd, dbBackupCmd, dbUpdateCmd)
|
||||
|
||||
versionCmd := &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Display version information",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
version.Print()
|
||||
},
|
||||
}
|
||||
|
||||
rootCmd.AddCommand(configCmd, dbCmd, versionCmd)
|
||||
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
10
go.mod
10
go.mod
@ -1,3 +1,13 @@
|
||||
module git.omicron.one/omicron/linkshare
|
||||
|
||||
go 1.24
|
||||
|
||||
require (
|
||||
github.com/mattn/go-sqlite3 v1.14.28
|
||||
github.com/spf13/cobra v1.9.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
)
|
||||
|
12
go.sum
12
go.sum
@ -0,0 +1,12 @@
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
|
||||
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
163
internal/database/manager.go
Normal file
163
internal/database/manager.go
Normal file
@ -0,0 +1,163 @@
|
||||
// Package database provides all database interactions for linkshare.
|
||||
// This includes functions to read and write structured link data, setting and
|
||||
// getting configurations, updating and initializing the schema and backing up
|
||||
// data
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
|
||||
"git.omicron.one/omicron/linkshare/internal/version"
|
||||
)
|
||||
|
||||
// DB represents a database connection
|
||||
type DB struct {
|
||||
conn *sql.DB
|
||||
}
|
||||
|
||||
var (
|
||||
ErrNotInitialized = errors.New("database not initialized")
|
||||
ErrAlreadyInitialized = errors.New("database already initialized")
|
||||
ErrSchemaOutdated = errors.New("database schema needs updating")
|
||||
ErrSchemaUnsupported = errors.New("database schema is too new for the server")
|
||||
ErrMigrationFailed = errors.New("migration failed")
|
||||
)
|
||||
|
||||
// Open opens a connection to the sqlite database at the given path
|
||||
func Open(dbPath string) (*DB, error) {
|
||||
conn, err := sql.Open("sqlite3", dbPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open database: %w", err)
|
||||
}
|
||||
|
||||
conn.SetMaxOpenConns(1) // SQLite only supports one writer at a time
|
||||
|
||||
if err := conn.Ping(); err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("failed to ping database: %w", err)
|
||||
}
|
||||
|
||||
_, err = conn.Exec("PRAGMA foreign_keys = ON")
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("failed to enable foreign key constraints: %w", err)
|
||||
}
|
||||
|
||||
return &DB{conn: conn}, nil
|
||||
}
|
||||
|
||||
// Close closes the database connection if it's open
|
||||
func (db *DB) Close() error {
|
||||
if db.conn != nil {
|
||||
return db.conn.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Initialize the database schema
|
||||
func (db *DB) Initialize(schemaPath string) error {
|
||||
err := db.CheckInitialized()
|
||||
if err == nil {
|
||||
return ErrAlreadyInitialized
|
||||
}
|
||||
|
||||
currentSchema := filepath.Join(schemaPath, "current.sql")
|
||||
schema, err := os.ReadFile(currentSchema)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read schema file: %w", err)
|
||||
}
|
||||
|
||||
_, err = db.conn.Exec(string(schema))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize database: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckInitialized returns nil if the database is initialized and an error otherwise
|
||||
func (db *DB) CheckInitialized() error {
|
||||
var count int
|
||||
err := db.conn.QueryRow("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='settings'").Scan(&count)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check if database is initialized: %w", err)
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
return ErrNotInitialized
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSchemaVersion returns the schema version or an error
|
||||
func (db *DB) GetSchemaVersion() (int, error) {
|
||||
var version string
|
||||
err := db.conn.QueryRow("SELECT value FROM settings WHERE key='schema-version'").Scan(&version)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to get schema version: %w", err)
|
||||
}
|
||||
|
||||
versionInt, err := strconv.Atoi(version)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid schema version: %w", err)
|
||||
}
|
||||
|
||||
if versionInt < 1 {
|
||||
return 0, fmt.Errorf("invalid schema version %d", versionInt)
|
||||
}
|
||||
|
||||
return versionInt, nil
|
||||
}
|
||||
|
||||
// CheckSchemaVersion verifies that the schema is initialized and has the correct version
|
||||
func (db *DB) CheckSchemaVersion() error {
|
||||
err := db.CheckInitialized()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
version_, err := db.GetSchemaVersion()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if version_ < version.SchemaVersion {
|
||||
return ErrSchemaOutdated
|
||||
} else if version_ > version.SchemaVersion {
|
||||
return ErrSchemaUnsupported
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *DB) transaction(ctx context.Context, fn func(*sql.Tx) error) error {
|
||||
tx, err := db.conn.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to begin transaction: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if p := recover(); p != nil {
|
||||
tx.Rollback()
|
||||
panic(p)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := fn(tx); err != nil {
|
||||
if rbErr := tx.Rollback(); rbErr != nil {
|
||||
return fmt.Errorf("error rolling back transaction: %v (original error: %w)", rbErr, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("failed to commit transaction: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
17
internal/version/version.go
Normal file
17
internal/version/version.go
Normal file
@ -0,0 +1,17 @@
|
||||
package version
|
||||
|
||||
import "fmt"
|
||||
|
||||
var (
|
||||
Version = "dev"
|
||||
GitCommit = "unknown"
|
||||
CommitDateTime = "unknown"
|
||||
SchemaVersion = 1
|
||||
)
|
||||
|
||||
// PrintVersionInfo prints formatted version information to stdout
|
||||
func Print() {
|
||||
fmt.Printf("Version: %s\n", Version)
|
||||
fmt.Printf("Git commit: %s %s\n", GitCommit, CommitDateTime)
|
||||
fmt.Printf("Schema: v%d\n", SchemaVersion)
|
||||
}
|
Reference in New Issue
Block a user