Storage backend: - Add internal/storage package with Storer interface - Implement FileStorer for local filesystem storage (file:// URLs) - Implement S3Storer wrapping existing s3.Client - Support storage_url config field (s3:// or file://) - Migrate all consumers to use storage.Storer interface PID locking: - Add internal/pidlock package to prevent concurrent instances - Acquire lock before app start, release on exit - Detect stale locks from crashed processes Scan progress improvements: - Add fast file enumeration pass before stat() phase - Use enumerated set for deletion detection (no extra filesystem access) - Show progress with percentage, files/sec, elapsed time, and ETA - Change "changed" to "changed/new" for clarity Config improvements: - Add tilde expansion for paths (~/) - Use xdg library for platform-specific default index path
109 lines
2.8 KiB
Go
109 lines
2.8 KiB
Go
// Package pidlock provides process-level locking using PID files.
|
|
// It prevents multiple instances of vaultik from running simultaneously,
|
|
// which would cause database locking conflicts.
|
|
package pidlock
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
)
|
|
|
|
// ErrAlreadyRunning indicates another vaultik instance is running.
|
|
var ErrAlreadyRunning = errors.New("another vaultik instance is already running")
|
|
|
|
// Lock represents an acquired PID lock.
|
|
type Lock struct {
|
|
path string
|
|
}
|
|
|
|
// Acquire attempts to acquire a PID lock in the specified directory.
|
|
// If the lock file exists and the process is still running, it returns
|
|
// ErrAlreadyRunning with details about the existing process.
|
|
// On success, it writes the current PID to the lock file and returns
|
|
// a Lock that must be released with Release().
|
|
func Acquire(lockDir string) (*Lock, error) {
|
|
// Ensure lock directory exists
|
|
if err := os.MkdirAll(lockDir, 0700); err != nil {
|
|
return nil, fmt.Errorf("creating lock directory: %w", err)
|
|
}
|
|
|
|
lockPath := filepath.Join(lockDir, "vaultik.pid")
|
|
|
|
// Check for existing lock
|
|
existingPID, err := readPIDFile(lockPath)
|
|
if err == nil {
|
|
// Lock file exists, check if process is running
|
|
if isProcessRunning(existingPID) {
|
|
return nil, fmt.Errorf("%w (PID %d)", ErrAlreadyRunning, existingPID)
|
|
}
|
|
// Process is not running, stale lock file - we can take over
|
|
}
|
|
|
|
// Write our PID
|
|
pid := os.Getpid()
|
|
if err := os.WriteFile(lockPath, []byte(strconv.Itoa(pid)), 0600); err != nil {
|
|
return nil, fmt.Errorf("writing PID file: %w", err)
|
|
}
|
|
|
|
return &Lock{path: lockPath}, nil
|
|
}
|
|
|
|
// Release removes the PID lock file.
|
|
// It is safe to call Release multiple times.
|
|
func (l *Lock) Release() error {
|
|
if l == nil || l.path == "" {
|
|
return nil
|
|
}
|
|
|
|
// Verify we still own the lock (our PID is in the file)
|
|
existingPID, err := readPIDFile(l.path)
|
|
if err != nil {
|
|
// File already gone or unreadable - that's fine
|
|
return nil
|
|
}
|
|
|
|
if existingPID != os.Getpid() {
|
|
// Someone else wrote to our lock file - don't remove it
|
|
return nil
|
|
}
|
|
|
|
if err := os.Remove(l.path); err != nil && !os.IsNotExist(err) {
|
|
return fmt.Errorf("removing PID file: %w", err)
|
|
}
|
|
|
|
l.path = "" // Prevent double-release
|
|
return nil
|
|
}
|
|
|
|
// readPIDFile reads and parses the PID from a lock file.
|
|
func readPIDFile(path string) (int, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
pid, err := strconv.Atoi(strings.TrimSpace(string(data)))
|
|
if err != nil {
|
|
return 0, fmt.Errorf("parsing PID: %w", err)
|
|
}
|
|
|
|
return pid, nil
|
|
}
|
|
|
|
// isProcessRunning checks if a process with the given PID is running.
|
|
func isProcessRunning(pid int) bool {
|
|
process, err := os.FindProcess(pid)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
// On Unix, FindProcess always succeeds. We need to send signal 0 to check.
|
|
err = process.Signal(syscall.Signal(0))
|
|
return err == nil
|
|
}
|