Fix move duplicate handling with hash verification
This commit is contained in:
+312
@@ -1,9 +1,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -140,6 +143,265 @@ func TestEvaluateGenericDuplicateNameHash(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransferFileMoveRemovesSource(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir := t.TempDir()
|
||||
sourcePath := filepath.Join(dir, "source.csv")
|
||||
targetPath := filepath.Join(dir, "target.csv")
|
||||
want := []byte("move-me")
|
||||
|
||||
if err := os.WriteFile(sourcePath, want, 0o644); err != nil {
|
||||
t.Fatalf("write source error = %v", err)
|
||||
}
|
||||
|
||||
if err := transferFile(sourcePath, targetPath, "move"); err != nil {
|
||||
t.Fatalf("transferFile() error = %v", err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(sourcePath); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected source to be removed after move, stat err = %v", err)
|
||||
}
|
||||
|
||||
got, err := os.ReadFile(targetPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read target error = %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("target content = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessGenericCandidateMoveDuplicateDeletesSourceAfterHashVerification(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rootDir := t.TempDir()
|
||||
sourceDir := filepath.Join(rootDir, "source")
|
||||
targetDir := filepath.Join(rootDir, "target")
|
||||
if err := os.MkdirAll(sourceDir, 0o755); err != nil {
|
||||
t.Fatalf("mkdir source error = %v", err)
|
||||
}
|
||||
if err := os.MkdirAll(targetDir, 0o755); err != nil {
|
||||
t.Fatalf("mkdir target error = %v", err)
|
||||
}
|
||||
|
||||
sourcePath := filepath.Join(sourceDir, "dup.csv")
|
||||
targetPath := filepath.Join(targetDir, "dup.csv")
|
||||
data := []byte("same-content")
|
||||
|
||||
if err := os.WriteFile(sourcePath, data, 0o644); err != nil {
|
||||
t.Fatalf("write source error = %v", err)
|
||||
}
|
||||
if err := os.WriteFile(targetPath, data, 0o644); err != nil {
|
||||
t.Fatalf("write target error = %v", err)
|
||||
}
|
||||
|
||||
cfg := config{
|
||||
mode: "move",
|
||||
generic: genericRule{
|
||||
targetDir: targetDir,
|
||||
dedupeMode: genericDedupeNameSize,
|
||||
},
|
||||
}
|
||||
|
||||
decision, handled, err := processGenericCandidate(cfg, sourcePath)
|
||||
if err != nil {
|
||||
t.Fatalf("processGenericCandidate() error = %v", err)
|
||||
}
|
||||
if !handled {
|
||||
t.Fatalf("expected duplicate move to be treated as handled")
|
||||
}
|
||||
if decision == "" {
|
||||
t.Fatalf("expected non-empty decision")
|
||||
}
|
||||
if _, err := os.Stat(sourcePath); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected source to be removed, stat err = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessGenericCandidateMoveDuplicateKeepsSourceWhenHashDiffers(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rootDir := t.TempDir()
|
||||
sourceDir := filepath.Join(rootDir, "source")
|
||||
targetDir := filepath.Join(rootDir, "target")
|
||||
if err := os.MkdirAll(sourceDir, 0o755); err != nil {
|
||||
t.Fatalf("mkdir source error = %v", err)
|
||||
}
|
||||
if err := os.MkdirAll(targetDir, 0o755); err != nil {
|
||||
t.Fatalf("mkdir target error = %v", err)
|
||||
}
|
||||
|
||||
sourcePath := filepath.Join(sourceDir, "dup.csv")
|
||||
targetPath := filepath.Join(targetDir, "dup.csv")
|
||||
|
||||
if err := os.WriteFile(sourcePath, []byte("abcd"), 0o644); err != nil {
|
||||
t.Fatalf("write source error = %v", err)
|
||||
}
|
||||
if err := os.WriteFile(targetPath, []byte("wxyz"), 0o644); err != nil {
|
||||
t.Fatalf("write target error = %v", err)
|
||||
}
|
||||
|
||||
cfg := config{
|
||||
mode: "move",
|
||||
generic: genericRule{
|
||||
targetDir: targetDir,
|
||||
dedupeMode: genericDedupeNameSize,
|
||||
},
|
||||
}
|
||||
|
||||
_, handled, err := processGenericCandidate(cfg, sourcePath)
|
||||
if err != nil {
|
||||
t.Fatalf("processGenericCandidate() error = %v", err)
|
||||
}
|
||||
if handled {
|
||||
t.Fatalf("expected hash mismatch not to be treated as handled duplicate")
|
||||
}
|
||||
if _, err := os.Stat(sourcePath); err != nil {
|
||||
t.Fatalf("expected source to remain, stat err = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessArchiveCandidateMoveDuplicateZipDeletesSource(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rootDir := t.TempDir()
|
||||
sourceDir := filepath.Join(rootDir, "source")
|
||||
zipDir := filepath.Join(rootDir, "zip")
|
||||
unzipDir := filepath.Join(rootDir, "unzip")
|
||||
for _, dir := range []string{sourceDir, zipDir, unzipDir} {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
t.Fatalf("mkdir %q error = %v", dir, err)
|
||||
}
|
||||
}
|
||||
|
||||
sourcePath := filepath.Join(sourceDir, "demo.tar.gz")
|
||||
targetPath := filepath.Join(zipDir, "demo.tar.gz")
|
||||
data := []byte("archive-binary")
|
||||
|
||||
if err := os.WriteFile(sourcePath, data, 0o644); err != nil {
|
||||
t.Fatalf("write source archive error = %v", err)
|
||||
}
|
||||
if err := os.WriteFile(targetPath, data, 0o644); err != nil {
|
||||
t.Fatalf("write target archive error = %v", err)
|
||||
}
|
||||
|
||||
cfg := config{
|
||||
mode: "move",
|
||||
archive: archiveRule{
|
||||
zipDir: zipDir,
|
||||
unzipDir: unzipDir,
|
||||
},
|
||||
}
|
||||
|
||||
decision, handled, err := processArchiveCandidate(cfg, sourcePath)
|
||||
if err != nil {
|
||||
t.Fatalf("processArchiveCandidate() error = %v", err)
|
||||
}
|
||||
if !handled {
|
||||
t.Fatalf("expected duplicate archive move to be treated as handled")
|
||||
}
|
||||
if decision == "" {
|
||||
t.Fatalf("expected non-empty decision")
|
||||
}
|
||||
if _, err := os.Stat(sourcePath); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected source archive to be removed, stat err = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessArchiveCandidateMoveDuplicateUnzipDeletesSource(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rootDir := t.TempDir()
|
||||
sourceDir := filepath.Join(rootDir, "source")
|
||||
zipDir := filepath.Join(rootDir, "zip")
|
||||
unzipDir := filepath.Join(rootDir, "unzip")
|
||||
for _, dir := range []string{sourceDir, zipDir, unzipDir} {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
t.Fatalf("mkdir %q error = %v", dir, err)
|
||||
}
|
||||
}
|
||||
|
||||
sourcePath := filepath.Join(sourceDir, "demo.tar.gz")
|
||||
unzipTarget := filepath.Join(unzipDir, "demo")
|
||||
files := map[string]string{
|
||||
"a.txt": "hello",
|
||||
"nested/b.json": `{"ok":true}`,
|
||||
"nested/c/data": "payload",
|
||||
}
|
||||
|
||||
if err := writeTarGz(sourcePath, "demo", files); err != nil {
|
||||
t.Fatalf("write tar.gz error = %v", err)
|
||||
}
|
||||
if err := writeExtractedDir(unzipTarget, files); err != nil {
|
||||
t.Fatalf("write extracted dir error = %v", err)
|
||||
}
|
||||
|
||||
cfg := config{
|
||||
mode: "move",
|
||||
archive: archiveRule{
|
||||
zipDir: zipDir,
|
||||
unzipDir: unzipDir,
|
||||
},
|
||||
}
|
||||
|
||||
decision, handled, err := processArchiveCandidate(cfg, sourcePath)
|
||||
if err != nil {
|
||||
t.Fatalf("processArchiveCandidate() error = %v", err)
|
||||
}
|
||||
if !handled {
|
||||
t.Fatalf("expected matching unzip directory to be treated as handled duplicate")
|
||||
}
|
||||
if decision == "" {
|
||||
t.Fatalf("expected non-empty decision")
|
||||
}
|
||||
if _, err := os.Stat(sourcePath); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected source archive to be removed, stat err = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessArchiveCandidateMoveDuplicateUnzipKeepsSourceWhenContentDiffers(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rootDir := t.TempDir()
|
||||
sourceDir := filepath.Join(rootDir, "source")
|
||||
zipDir := filepath.Join(rootDir, "zip")
|
||||
unzipDir := filepath.Join(rootDir, "unzip")
|
||||
for _, dir := range []string{sourceDir, zipDir, unzipDir} {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
t.Fatalf("mkdir %q error = %v", dir, err)
|
||||
}
|
||||
}
|
||||
|
||||
sourcePath := filepath.Join(sourceDir, "demo.tar.gz")
|
||||
unzipTarget := filepath.Join(unzipDir, "demo")
|
||||
if err := writeTarGz(sourcePath, "demo", map[string]string{"a.txt": "hello"}); err != nil {
|
||||
t.Fatalf("write tar.gz error = %v", err)
|
||||
}
|
||||
if err := writeExtractedDir(unzipTarget, map[string]string{"a.txt": "DIFF"}); err != nil {
|
||||
t.Fatalf("write extracted dir error = %v", err)
|
||||
}
|
||||
|
||||
cfg := config{
|
||||
mode: "move",
|
||||
archive: archiveRule{
|
||||
zipDir: zipDir,
|
||||
unzipDir: unzipDir,
|
||||
},
|
||||
}
|
||||
|
||||
_, handled, err := processArchiveCandidate(cfg, sourcePath)
|
||||
if err != nil {
|
||||
t.Fatalf("processArchiveCandidate() error = %v", err)
|
||||
}
|
||||
if handled {
|
||||
t.Fatalf("expected different unzip content not to be treated as handled duplicate")
|
||||
}
|
||||
if _, err := os.Stat(sourcePath); err != nil {
|
||||
t.Fatalf("expected source archive to remain, stat err = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveAndLoadSettings(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -212,3 +474,53 @@ func TestLoadLegacySettings(t *testing.T) {
|
||||
t.Fatalf("expected legacy settings to default to name_size dedupe")
|
||||
}
|
||||
}
|
||||
|
||||
func writeTarGz(targetPath, rootName string, files map[string]string) error {
|
||||
file, err := os.Create(targetPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
gzipWriter := gzip.NewWriter(file)
|
||||
defer gzipWriter.Close()
|
||||
|
||||
tarWriter := tar.NewWriter(gzipWriter)
|
||||
defer tarWriter.Close()
|
||||
|
||||
paths := make([]string, 0, len(files))
|
||||
for name := range files {
|
||||
paths = append(paths, name)
|
||||
}
|
||||
sort.Strings(paths)
|
||||
|
||||
for _, name := range paths {
|
||||
body := []byte(files[name])
|
||||
header := &tar.Header{
|
||||
Name: filepath.ToSlash(filepath.Join(rootName, name)),
|
||||
Mode: 0o644,
|
||||
Size: int64(len(body)),
|
||||
}
|
||||
if err := tarWriter.WriteHeader(header); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tarWriter.Write(body); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeExtractedDir(root string, files map[string]string) error {
|
||||
for name, body := range files {
|
||||
targetPath := filepath.Join(root, filepath.FromSlash(name))
|
||||
if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(targetPath, []byte(body), 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user