// Package store owns RVBox server persistence and migrations. package store import ( "context" "database/sql" "errors" "fmt" "net/url" "os" "path/filepath" "strconv" "sync" "time" _ "modernc.org/sqlite" ) var ( ErrUnsafeDataDirectory = errors.New("unsafe server data directory") ErrAlreadyOpen = errors.New("server data directory is already locked") ) type Options struct { DataDir string BusyTimeout time.Duration SegmentTargetSize uint64 QuotaLimits QuotaLimits FreeSpaceProbe FreeSpaceProbe FaultInjector FaultInjector } type Store struct { db *sql.DB unlock func() error dataDir string segmentTarget uint64 quotaLimits QuotaLimits freeSpaceProbe FreeSpaceProbe faultInjector FaultInjector activeSegments map[[16]byte]*activeSegment mu sync.Mutex writeMu sync.Mutex } func Open(ctx context.Context, options Options) (*Store, error) { if !filepath.IsAbs(options.DataDir) || filepath.Clean(options.DataDir) == string(filepath.Separator) { return nil, ErrUnsafeDataDirectory } if options.BusyTimeout <= 0 { return nil, fmt.Errorf("busy timeout must be positive") } if options.SegmentTargetSize == 0 { options.SegmentTargetSize = 256 << 10 } if options.SegmentTargetSize > DefaultSegmentLimit { return nil, fmt.Errorf("segment target exceeds hard record limit") } if options.QuotaLimits == (QuotaLimits{}) { options.QuotaLimits = DefaultQuotaLimits() } if err := options.QuotaLimits.Validate(); err != nil { return nil, err } if options.FreeSpaceProbe == nil { options.FreeSpaceProbe = FreeSpaceProbeFunc(filesystemFreeBytes) } if err := ensurePrivateDirectory(options.DataDir); err != nil { return nil, err } for _, child := range []string{"segments", "audit", "deleting"} { if err := ensurePrivateDirectory(filepath.Join(options.DataDir, child)); err != nil { return nil, err } } unlock, err := acquireInstanceLock(filepath.Join(options.DataDir, "server.lock")) if err != nil { return nil, err } databasePath := filepath.Join(options.DataDir, "rvbox.db") if err := ensurePrivateFile(databasePath); err != nil { _ = unlock() return nil, err } query := url.Values{} query.Add("_defensive", "1") query.Add("_pragma", "journal_mode(WAL)") query.Add("_pragma", "foreign_keys(ON)") query.Add("_pragma", "synchronous(FULL)") query.Add("_pragma", "busy_timeout("+strconv.FormatInt(options.BusyTimeout.Milliseconds(), 10)+")") databaseURL := &url.URL{Scheme: "file", Path: filepath.ToSlash(databasePath)} databaseURL.RawQuery = query.Encode() dsn := databaseURL.String() db, err := sql.Open("sqlite", dsn) if err != nil { _ = unlock() return nil, err } db.SetMaxOpenConns(1) db.SetMaxIdleConns(1) store := &Store{ db: db, unlock: unlock, dataDir: options.DataDir, segmentTarget: options.SegmentTargetSize, quotaLimits: options.QuotaLimits, freeSpaceProbe: options.FreeSpaceProbe, faultInjector: options.FaultInjector, activeSegments: make(map[[16]byte]*activeSegment), } if err := db.PingContext(ctx); err != nil { _ = store.Close() return nil, fmt.Errorf("open SQLite: %w", err) } if err := applyMigrations(ctx, db); err != nil { _ = store.Close() return nil, err } return store, nil } func (store *Store) DB() *sql.DB { store.mu.Lock() defer store.mu.Unlock() return store.db } func (store *Store) Close() error { if store == nil { return nil } store.writeMu.Lock() defer store.writeMu.Unlock() store.mu.Lock() defer store.mu.Unlock() var result error for owner, active := range store.activeSegments { if err := active.segment.Close(); result == nil && err != nil { result = err } delete(store.activeSegments, owner) } if store.db != nil { result = store.db.Close() store.db = nil } if store.unlock != nil { if err := store.unlock(); result == nil { result = err } store.unlock = nil } return result } func ensurePrivateFile(path string) error { file, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_RDWR, 0o600) if err == nil { return file.Close() } if !errors.Is(err, os.ErrExist) { return err } info, err := os.Lstat(path) if err != nil { return err } if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 { return fmt.Errorf("%w: %s is not a regular file", ErrUnsafeDataDirectory, path) } if info.Mode().Perm()&0o077 != 0 { return fmt.Errorf("%w: %s permissions %04o expose private state", ErrUnsafeDataDirectory, path, info.Mode().Perm()) } return nil } func ensurePrivateDirectory(path string) error { info, err := os.Lstat(path) if os.IsNotExist(err) { if err := os.MkdirAll(path, 0o700); err != nil { return err } info, err = os.Lstat(path) } if err != nil { return err } if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() { return fmt.Errorf("%w: %s is not a real directory", ErrUnsafeDataDirectory, path) } if info.Mode().Perm()&0o077 != 0 { return fmt.Errorf("%w: %s permissions %04o expose private state", ErrUnsafeDataDirectory, path, info.Mode().Perm()) } return nil }