- Add afero.Fs field to Vaultik struct for filesystem operations - Vaultik now owns and manages the filesystem instance - SnapshotManager receives filesystem via SetFilesystem() setter - Update blob packer to use afero for temporary files - Convert all filesystem operations to use afero abstraction - Remove filesystem module - Vaultik manages filesystem directly - Update tests: remove symlink test (unsupported by afero memfs) - Fix TestMultipleFileChanges to handle scanner examining directories This enables full end-to-end testing without touching disk by using memory-backed filesystems. Database operations continue using real filesystem as SQLite requires actual files.
125 lines
3.7 KiB
Go
125 lines
3.7 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"
|
|
"git.eeqj.de/sneak/vaultik/internal/s3"
|
|
"git.eeqj.de/sneak/vaultik/internal/snapshot"
|
|
"git.eeqj.de/sneak/vaultik/internal/vaultik"
|
|
"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,
|
|
s3.Module,
|
|
snapshot.Module,
|
|
fx.Provide(vaultik.New),
|
|
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)
|
|
}
|