Implement local SQLite index database with repositories

- Add SQLite database connection management with proper error handling
- Implement schema for files, chunks, blobs, and snapshots tables
- Create repository pattern for each database table
- Add transaction support with proper rollback handling
- Integrate database module with fx dependency injection
- Make index path configurable via VAULTIK_INDEX_PATH env var
- Add fatal error handling for database integrity issues
- Update DESIGN.md to clarify file_chunks vs chunk_files distinction
- Remove FinalHash from BlobInfo (blobs are content-addressable)
- Add file metadata support (mtime, ctime, mode, uid, gid, symlinks)
This commit is contained in:
2025-07-20 10:26:15 +02:00
parent 9de439a0a4
commit b2e85d9e76
31 changed files with 1229 additions and 83 deletions

56
internal/cli/app.go Normal file
View File

@@ -0,0 +1,56 @@
package cli
import (
"context"
"fmt"
"git.eeqj.de/sneak/vaultik/internal/config"
"git.eeqj.de/sneak/vaultik/internal/database"
"git.eeqj.de/sneak/vaultik/internal/globals"
"go.uber.org/fx"
)
// AppOptions contains common options for creating the fx application
type AppOptions struct {
ConfigPath string
Modules []fx.Option
Invokes []fx.Option
}
// NewApp creates a new fx application with common modules
func NewApp(opts AppOptions) *fx.App {
baseModules := []fx.Option{
fx.Supply(config.ConfigPath(opts.ConfigPath)),
fx.Provide(globals.New),
config.Module,
database.Module,
fx.NopLogger,
}
allOptions := append(baseModules, opts.Modules...)
allOptions = append(allOptions, opts.Invokes...)
return fx.New(allOptions...)
}
// RunApp starts and stops the fx application within the given context
func RunApp(ctx context.Context, app *fx.App) error {
if err := app.Start(ctx); err != nil {
return fmt.Errorf("failed to start app: %w", err)
}
defer func() {
if err := app.Stop(ctx); err != nil {
fmt.Printf("error stopping app: %v\n", err)
}
}()
// Wait for context cancellation
<-ctx.Done()
return nil
}
// RunWithApp is a helper that creates and runs an fx app with the given options
func RunWithApp(ctx context.Context, opts AppOptions) error {
app := NewApp(opts)
return RunApp(ctx, app)
}

View File

@@ -6,6 +6,7 @@ import (
"os"
"git.eeqj.de/sneak/vaultik/internal/config"
"git.eeqj.de/sneak/vaultik/internal/database"
"git.eeqj.de/sneak/vaultik/internal/globals"
"github.com/spf13/cobra"
"go.uber.org/fx"
@@ -56,34 +57,22 @@ a path using --config or by setting VAULTIK_CONFIG to a path.`,
}
func runBackup(ctx context.Context, opts *BackupOptions) error {
app := fx.New(
fx.Supply(config.ConfigPath(opts.ConfigPath)),
fx.Provide(globals.New),
config.Module,
// Additional modules will be added here
fx.Invoke(func(g *globals.Globals, cfg *config.Config) error {
// TODO: Implement backup logic
fmt.Printf("Running backup with config: %s\n", opts.ConfigPath)
fmt.Printf("Version: %s, Commit: %s\n", g.Version, g.Commit)
if opts.Daemon {
fmt.Println("Running in daemon mode")
}
if opts.Cron {
fmt.Println("Running in cron mode")
}
return nil
}),
fx.NopLogger,
)
if err := app.Start(ctx); err != nil {
return fmt.Errorf("failed to start backup: %w", err)
}
defer func() {
if err := app.Stop(ctx); err != nil {
fmt.Printf("error stopping app: %v\n", err)
}
}()
return nil
}
return RunWithApp(ctx, AppOptions{
ConfigPath: opts.ConfigPath,
Invokes: []fx.Option{
fx.Invoke(func(g *globals.Globals, cfg *config.Config, repos *database.Repositories) error {
// TODO: Implement backup logic
fmt.Printf("Running backup with config: %s\n", opts.ConfigPath)
fmt.Printf("Version: %s, Commit: %s\n", g.Version, g.Commit)
fmt.Printf("Index path: %s\n", cfg.IndexPath)
if opts.Daemon {
fmt.Println("Running in daemon mode")
}
if opts.Cron {
fmt.Println("Running in cron mode")
}
return nil
}),
},
})
}

View File

@@ -10,4 +10,4 @@ func CLIEntry() {
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
}

View File

@@ -12,11 +12,11 @@ func TestCLIEntry(t *testing.T) {
if cmd == nil {
t.Fatal("NewRootCommand() returned nil")
}
if cmd.Use != "vaultik" {
t.Errorf("Expected command use to be 'vaultik', got '%s'", cmd.Use)
}
// Verify all subcommands are registered
expectedCommands := []string{"backup", "restore", "prune", "verify", "fetch"}
for _, expected := range expectedCommands {
@@ -31,7 +31,7 @@ func TestCLIEntry(t *testing.T) {
t.Errorf("Expected command '%s' not found", expected)
}
}
// Verify backup command has proper flags
backupCmd, _, err := cmd.Find([]string{"backup"})
if err != nil {
@@ -47,4 +47,4 @@ func TestCLIEntry(t *testing.T) {
t.Error("Backup command missing --cron flag")
}
}
}
}

View File

@@ -85,4 +85,4 @@ func runFetch(ctx context.Context, opts *FetchOptions) error {
}()
return nil
}
}

View File

@@ -75,4 +75,4 @@ func runPrune(ctx context.Context, opts *PruneOptions) error {
}()
return nil
}
}

View File

@@ -80,4 +80,4 @@ func runRestore(ctx context.Context, opts *RestoreOptions) error {
}()
return nil
}
}

View File

@@ -25,4 +25,4 @@ on the source system.`,
)
return cmd
}
}

View File

@@ -83,4 +83,4 @@ func runVerify(ctx context.Context, opts *VerifyOptions) error {
}()
return nil
}
}