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.5 KiB
Go
109 lines
2.5 KiB
Go
package pidlock
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestAcquireAndRelease(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
// Acquire lock
|
|
lock, err := Acquire(tmpDir)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, lock)
|
|
|
|
// Verify PID file exists with our PID
|
|
data, err := os.ReadFile(filepath.Join(tmpDir, "vaultik.pid"))
|
|
require.NoError(t, err)
|
|
pid, err := strconv.Atoi(string(data))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, os.Getpid(), pid)
|
|
|
|
// Release lock
|
|
err = lock.Release()
|
|
require.NoError(t, err)
|
|
|
|
// Verify PID file is gone
|
|
_, err = os.Stat(filepath.Join(tmpDir, "vaultik.pid"))
|
|
assert.True(t, os.IsNotExist(err))
|
|
}
|
|
|
|
func TestAcquireBlocksSecondInstance(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
// Acquire first lock
|
|
lock1, err := Acquire(tmpDir)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, lock1)
|
|
defer func() { _ = lock1.Release() }()
|
|
|
|
// Try to acquire second lock - should fail
|
|
lock2, err := Acquire(tmpDir)
|
|
assert.ErrorIs(t, err, ErrAlreadyRunning)
|
|
assert.Nil(t, lock2)
|
|
}
|
|
|
|
func TestAcquireWithStaleLock(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
// Write a stale PID file (PID that doesn't exist)
|
|
stalePID := 999999999 // Unlikely to be a real process
|
|
pidPath := filepath.Join(tmpDir, "vaultik.pid")
|
|
err := os.WriteFile(pidPath, []byte(strconv.Itoa(stalePID)), 0600)
|
|
require.NoError(t, err)
|
|
|
|
// Should be able to acquire lock (stale lock is cleaned up)
|
|
lock, err := Acquire(tmpDir)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, lock)
|
|
defer func() { _ = lock.Release() }()
|
|
|
|
// Verify our PID is now in the file
|
|
data, err := os.ReadFile(pidPath)
|
|
require.NoError(t, err)
|
|
pid, err := strconv.Atoi(string(data))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, os.Getpid(), pid)
|
|
}
|
|
|
|
func TestReleaseIsIdempotent(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
lock, err := Acquire(tmpDir)
|
|
require.NoError(t, err)
|
|
|
|
// Release multiple times - should not error
|
|
err = lock.Release()
|
|
require.NoError(t, err)
|
|
|
|
err = lock.Release()
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestReleaseNilLock(t *testing.T) {
|
|
var lock *Lock
|
|
err := lock.Release()
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestAcquireCreatesDirectory(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
nestedDir := filepath.Join(tmpDir, "nested", "dir")
|
|
|
|
lock, err := Acquire(nestedDir)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, lock)
|
|
defer func() { _ = lock.Release() }()
|
|
|
|
// Verify directory was created
|
|
info, err := os.Stat(nestedDir)
|
|
require.NoError(t, err)
|
|
assert.True(t, info.IsDir())
|
|
}
|