This commit represents a significant architectural overhaul of vaultik: Database Schema Changes: - Switch files table to use UUID primary keys instead of path-based keys - Add UUID primary keys to blobs table for immediate chunk association - Update all foreign key relationships to use UUIDs - Add comprehensive schema documentation in DATAMODEL.md - Add SQLite busy timeout handling for concurrent operations Streaming and Performance Improvements: - Implement true streaming blob packing without intermediate storage - Add streaming chunk processing to reduce memory usage - Improve progress reporting with real-time metrics - Add upload metrics tracking in new uploads table CLI Refactoring: - Restructure CLI to use subcommands: snapshot create/list/purge/verify - Add store info command for S3 configuration display - Add custom duration parser supporting days/weeks/months/years - Remove old backup.go in favor of enhanced snapshot.go - Add --cron flag for silent operation Configuration Changes: - Remove unused index_prefix configuration option - Add support for snapshot pruning retention policies - Improve configuration validation and error messages Testing Improvements: - Add comprehensive repository tests with edge cases - Add cascade delete debugging tests - Fix concurrent operation tests to use SQLite busy timeout - Remove tolerance for SQLITE_BUSY errors in tests Documentation: - Add MIT LICENSE file - Update README with new command structure - Add comprehensive DATAMODEL.md explaining database schema - Update DESIGN.md with UUID-based architecture Other Changes: - Add test-config.yml for testing - Update Makefile with better test output formatting - Fix various race conditions in concurrent operations - Improve error handling throughout
119 lines
3.5 KiB
Go
119 lines
3.5 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"git.eeqj.de/sneak/vaultik/internal/config"
|
|
"git.eeqj.de/sneak/vaultik/internal/database"
|
|
"git.eeqj.de/sneak/vaultik/internal/globals"
|
|
"git.eeqj.de/sneak/vaultik/internal/log"
|
|
"go.uber.org/fx"
|
|
)
|
|
|
|
// AppOptions contains common options for creating the fx application.
|
|
// It includes the configuration file path, logging options, and additional
|
|
// fx modules and invocations that should be included in the application.
|
|
type AppOptions struct {
|
|
ConfigPath string
|
|
LogOptions log.LogOptions
|
|
Modules []fx.Option
|
|
Invokes []fx.Option
|
|
}
|
|
|
|
// setupGlobals sets up the globals with application startup time
|
|
func setupGlobals(lc fx.Lifecycle, g *globals.Globals) {
|
|
lc.Append(fx.Hook{
|
|
OnStart: func(ctx context.Context) error {
|
|
g.StartTime = time.Now().UTC()
|
|
return nil
|
|
},
|
|
})
|
|
}
|
|
|
|
// NewApp creates a new fx application with common modules.
|
|
// It sets up the base modules (config, database, logging, globals) and
|
|
// combines them with any additional modules specified in the options.
|
|
// The returned fx.App is ready to be started with RunApp.
|
|
func NewApp(opts AppOptions) *fx.App {
|
|
baseModules := []fx.Option{
|
|
fx.Supply(config.ConfigPath(opts.ConfigPath)),
|
|
fx.Supply(opts.LogOptions),
|
|
fx.Provide(globals.New),
|
|
fx.Provide(log.New),
|
|
config.Module,
|
|
database.Module,
|
|
log.Module,
|
|
fx.Invoke(setupGlobals),
|
|
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.
|
|
// It handles graceful shutdown on interrupt signals (SIGINT, SIGTERM) and
|
|
// ensures the application stops cleanly. The function blocks until the
|
|
// application completes or is interrupted. Returns an error if startup fails.
|
|
func RunApp(ctx context.Context, app *fx.App) error {
|
|
// Set up signal handling for graceful shutdown
|
|
sigChan := make(chan os.Signal, 1)
|
|
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
|
|
|
// Create a context that will be cancelled on signal
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
|
|
// Start the app
|
|
if err := app.Start(ctx); err != nil {
|
|
return fmt.Errorf("failed to start app: %w", err)
|
|
}
|
|
|
|
// Handle shutdown
|
|
shutdownComplete := make(chan struct{})
|
|
go func() {
|
|
defer close(shutdownComplete)
|
|
<-sigChan
|
|
log.Notice("Received interrupt signal, shutting down gracefully...")
|
|
|
|
// Create a timeout context for shutdown
|
|
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer shutdownCancel()
|
|
|
|
if err := app.Stop(shutdownCtx); err != nil {
|
|
log.Error("Error during shutdown", "error", err)
|
|
}
|
|
}()
|
|
|
|
// Wait for either the signal handler to complete shutdown or the app to request shutdown
|
|
select {
|
|
case <-shutdownComplete:
|
|
// Shutdown completed via signal
|
|
return nil
|
|
case <-ctx.Done():
|
|
// Context cancelled (shouldn't happen in normal operation)
|
|
if err := app.Stop(context.Background()); err != nil {
|
|
log.Error("Error stopping app", "error", err)
|
|
}
|
|
return ctx.Err()
|
|
case <-app.Done():
|
|
// App finished running (e.g., backup completed)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// RunWithApp is a helper that creates and runs an fx app with the given options.
|
|
// It combines NewApp and RunApp into a single convenient function. This is the
|
|
// preferred way to run CLI commands that need the full application context.
|
|
func RunWithApp(ctx context.Context, opts AppOptions) error {
|
|
app := NewApp(opts)
|
|
return RunApp(ctx, app)
|
|
}
|