package main import ( "errors" "fmt" "os" "path/filepath" "syscall" ) // errInUse means another process holds the database lock. var errInUse = errors.New("the database is in use by a running stewardd or by 'stewardd restore'") // lockDatabase takes the exclusive lock (on DATABASE.lock) that marks the // database as in use by a running stewardd or by 'stewardd restore', so a // restore can never replace the database under a running stewardd, and two // stewardds can't serve the same database. Other commands (backup, user, ...) // don't take it: they work alongside a running stewardd. func lockDatabase(db string) (release func(), err error) { if err := os.MkdirAll(filepath.Dir(db), 0o750); err != nil { return nil, err } path := db + ".lock" // Read-only is enough for flock, and works whoever created the file. f, err := os.OpenFile(path, os.O_RDONLY|os.O_CREATE, 0o640) if err != nil { return nil, err } if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err != nil { f.Close() if errors.Is(err, syscall.EWOULDBLOCK) { return nil, errInUse } return nil, fmt.Errorf("locking %s: %w", path, err) } return func() { syscall.Flock(int(f.Fd()), syscall.LOCK_UN) f.Close() }, nil }