check / check (push) Waiting to run
The files table held mtime in whole seconds and the scanner compared whole seconds. A file rewritten with its size unchanged and a new mtime in the same second as the indexed one was treated as unchanged, and every later snapshot restored the old content. A new mtime_nsec column now holds the nanoseconds within the second that mtime holds, and the scanner compares the full mtime. A local index created before this change lacks the column and is rebuilt with `vaultik database delete` and a full backup. A snapshot made before it cannot be restored by this version. Model: opus-5-5
74 lines
2.2 KiB
Go
74 lines
2.2 KiB
Go
package vaultik_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/spf13/afero"
|
|
"github.com/stretchr/testify/require"
|
|
"sneak.berlin/go/vaultik/internal/database"
|
|
"sneak.berlin/go/vaultik/internal/log"
|
|
"sneak.berlin/go/vaultik/internal/storage"
|
|
"sneak.berlin/go/vaultik/internal/vaultik"
|
|
)
|
|
|
|
// A file rewritten with its size unchanged and a new mtime in the same
|
|
// second as the mtime the index holds must still be backed up. See
|
|
// https://git.eeqj.de/sneak/vaultik/issues/226.
|
|
//
|
|
//nolint:paralleltest // installs the global logger via log.Initialize
|
|
func TestBackupOfSameSecondRewriteRestoresNewContent(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
|
|
fs := afero.NewOsFs()
|
|
tempDir := t.TempDir()
|
|
dataDir := filepath.Join(tempDir, "src")
|
|
storeDir := filepath.Join(tempDir, "remote")
|
|
restoreDir := filepath.Join(tempDir, "restored")
|
|
dbPath := filepath.Join(tempDir, "index.sqlite")
|
|
rewrittenPath := filepath.Join(dataDir, "small.txt")
|
|
|
|
ctx := context.Background()
|
|
files := writeFaultSourceTree(t, fs, dataDir)
|
|
cfg := changedFileConfig(dataDir, dbPath)
|
|
|
|
firstMTime := time.Date(2026, time.January, 2, 3, 4, 5, 0, time.UTC).
|
|
Add(100 * time.Millisecond)
|
|
secondMTime := firstMTime.Add(800 * time.Millisecond)
|
|
|
|
require.NoError(t, fs.Chtimes(rewrittenPath, firstMTime, firstMTime))
|
|
|
|
store, err := storage.NewFileStorer(storeDir)
|
|
require.NoError(t, err)
|
|
|
|
db, err := database.New(ctx, dbPath)
|
|
require.NoError(t, err)
|
|
|
|
repos := database.NewRepositories(db)
|
|
v := newBackupVaultik(ctx, cfg, store, repos, db, fs)
|
|
|
|
require.NoError(t, backUp(v, "first"))
|
|
|
|
// Upper-casing ASCII text keeps its size.
|
|
files[rewrittenPath] = bytes.ToUpper(files[rewrittenPath])
|
|
require.NoError(t, afero.WriteFile(fs, rewrittenPath, files[rewrittenPath], 0o644))
|
|
require.NoError(t, fs.Chtimes(rewrittenPath, secondMTime, secondMTime))
|
|
|
|
require.NoError(t, backUp(v, "second"))
|
|
|
|
id := localSnapshotID(ctx, t, repos, "second")
|
|
require.NoError(t, db.Close())
|
|
|
|
reader := newReaderVaultik(ctx, cfg, store, nil, fs)
|
|
require.NoError(t, reader.Restore(&vaultik.RestoreOptions{
|
|
SnapshotID: id,
|
|
TargetDir: restoreDir,
|
|
Verify: true,
|
|
}))
|
|
|
|
assertRestoredTree(t, fs, restoreDir, files)
|
|
}
|