check / check (push) Successful in 10m54s
The default config backs up the home directory, which holds vaultik's local index and PID file, so a backup stored a torn copy of the index it was writing. The scan now skips the index, its SQLite side files and the PID file whatever the config says, including when index_path or its directory is a symlink. database purge removes the same list of index files. Model: opus-5-5
116 lines
2.9 KiB
Go
116 lines
2.9 KiB
Go
package database
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"go.uber.org/fx"
|
|
"sneak.berlin/go/vaultik/internal/config"
|
|
"sneak.berlin/go/vaultik/internal/log"
|
|
)
|
|
|
|
// indexDirPerm restricts the local index directory to the owning user;
|
|
// the index describes the backed-up file tree and must stay private.
|
|
const indexDirPerm = 0o700
|
|
|
|
// indexFilePerm restricts the index file to the owning user; it lists every
|
|
// backed-up path and chunk hash and must stay private.
|
|
const indexFilePerm = 0o600
|
|
|
|
// IndexFiles returns the path of the index at indexPath and the paths of
|
|
// the side files SQLite keeps beside it while it writes.
|
|
func IndexFiles(indexPath string) []string {
|
|
return []string{
|
|
indexPath,
|
|
indexPath + "-wal",
|
|
indexPath + "-shm",
|
|
indexPath + "-journal",
|
|
}
|
|
}
|
|
|
|
// ensureIndexFileMode makes the index file owner-only before the SQLite
|
|
// driver opens it: it creates the file 0600 if absent, or chmods an existing
|
|
// one to 0600. Doing this first matters because SQLite creates its -wal and
|
|
// -shm side files with the mode of the main database file, so a private main
|
|
// file yields private side files. The driver treats a zero-byte file as an
|
|
// empty database, so pre-creating it here is safe.
|
|
func ensureIndexFileMode(path string) error {
|
|
info, err := os.Stat(path)
|
|
|
|
switch {
|
|
case err == nil:
|
|
if info.Mode().Perm() == indexFilePerm {
|
|
return nil
|
|
}
|
|
|
|
err = os.Chmod(path, indexFilePerm)
|
|
if err != nil {
|
|
return fmt.Errorf("restricting index file permissions: %w", err)
|
|
}
|
|
|
|
return nil
|
|
case errors.Is(err, os.ErrNotExist):
|
|
//nolint:gosec // G304: the index path is operator-configured by design
|
|
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, indexFilePerm)
|
|
if err != nil {
|
|
return fmt.Errorf("creating index file: %w", err)
|
|
}
|
|
|
|
return f.Close()
|
|
default:
|
|
return fmt.Errorf("checking index file: %w", err)
|
|
}
|
|
}
|
|
|
|
// Module provides database dependencies
|
|
//
|
|
//nolint:gochecknoglobals // fx module definitions are package globals by convention
|
|
var Module = fx.Module("database",
|
|
fx.Provide(
|
|
provideDatabase,
|
|
NewRepositories,
|
|
),
|
|
)
|
|
|
|
func provideDatabase(lc fx.Lifecycle, cfg *config.Config) (*DB, error) {
|
|
// Ensure the index directory exists
|
|
indexDir := filepath.Dir(cfg.IndexPath)
|
|
|
|
err := os.MkdirAll(indexDir, indexDirPerm)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("creating index directory: %w", err)
|
|
}
|
|
|
|
err = ensureIndexFileMode(cfg.IndexPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
db, err := New(context.Background(), cfg.IndexPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("opening database: %w", err)
|
|
}
|
|
|
|
lc.Append(fx.Hook{
|
|
OnStop: func(_ context.Context) error {
|
|
log.Debug("Database module OnStop hook called")
|
|
|
|
err := db.Close()
|
|
if err != nil {
|
|
log.Error("Failed to close database in OnStop hook", "error", err)
|
|
|
|
return err
|
|
}
|
|
|
|
log.Debug("Database closed successfully in OnStop hook")
|
|
|
|
return nil
|
|
},
|
|
})
|
|
|
|
return db, nil
|
|
}
|