package files import ( "context" "errors" "os" "path/filepath" "strings" "syscall" "testing" "time" ) func TestInside(t *testing.T) { for _, c := range []struct { from, to string want bool }{ {"/a/b", "/a/b", true}, {"/a/b", "/a/b/c", true}, {"/a/b", "/a/bc", false}, {"/a/b", "/a", false}, {"/", "/x", true}, } { if got := Inside(c.from, c.to); got != c.want { t.Errorf("Inside(%q, %q) = %v, want %v", c.from, c.to, got, c.want) } } } func TestIsCrossDevice(t *testing.T) { if !IsCrossDevice(&os.LinkError{Op: "rename", Old: "/a", New: "/b", Err: syscall.EXDEV}) { t.Fatal("the kernel's EXDEV") } if !IsCrossDevice(errors.New("rename /a /b: invalid cross-device link")) { t.Fatal("the helper's words for it") } if IsCrossDevice(errors.New("permission denied")) || IsCrossDevice(nil) { t.Fatal("anything else is not") } } // The copy path, exercised directly: a tree with nested folders, a symlink, an odd mode // and old mtimes lands whole under the new name, and the original is gone only then. func TestMoveByCopy(t *testing.T) { root := t.TempDir() src := filepath.Join(root, "src", "album") if err := os.MkdirAll(filepath.Join(src, "sub", "deep"), 0o755); err != nil { t.Fatal(err) } old := time.Date(2020, 1, 2, 3, 4, 5, 0, time.UTC) write := func(p, s string, mode os.FileMode) { if err := os.WriteFile(p, []byte(s), mode); err != nil { t.Fatal(err) } _ = os.Chmod(p, mode) _ = os.Chtimes(p, old, old) } write(filepath.Join(src, "a.txt"), "hello", 0o644) write(filepath.Join(src, "sub", "deep", "b.bin"), strings.Repeat("x", 100000), 0o600) write(filepath.Join(src, "run.sh"), "#!/bin/sh\n", 0o755) if err := os.Symlink("a.txt", filepath.Join(src, "link")); err != nil { t.Fatal(err) } _ = os.Chtimes(filepath.Join(src, "sub"), old, old) dst := filepath.Join(root, "dst", "moved") if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil { t.Fatal(err) } var out strings.Builder if err := moveByCopy(context.Background(), src, dst, &out); err != nil { t.Fatal(err) } if _, err := os.Lstat(src); !os.IsNotExist(err) { t.Fatal("the original is still there") } if b, _ := os.ReadFile(filepath.Join(dst, "a.txt")); string(b) != "hello" { t.Fatal("a.txt") } if st, err := os.Stat(filepath.Join(dst, "sub", "deep", "b.bin")); err != nil || st.Size() != 100000 || st.Mode().Perm() != 0o600 || !st.ModTime().Equal(old) { t.Fatalf("b.bin: %v %v", st, err) } if st, _ := os.Stat(filepath.Join(dst, "run.sh")); st.Mode().Perm() != 0o755 { t.Fatal("mode lost") } if target, err := os.Readlink(filepath.Join(dst, "link")); err != nil || target != "a.txt" { t.Fatal("symlink not copied as a symlink") } if st, _ := os.Stat(filepath.Join(dst, "sub")); !st.ModTime().Equal(old) { t.Fatal("folder mtime lost") } if left, _ := filepath.Glob(filepath.Join(root, "dst", ".*hamlab-moving*")); len(left) != 0 { t.Fatal("temporary copy left behind") } if !strings.Contains(out.String(), "moved ") || !strings.Contains(out.String(), "4 files") { t.Fatalf("progress said: %q", out.String()) } } // A failure part-way leaves the original as it was and no partial copy. func TestMoveByCopyFailsClean(t *testing.T) { root := t.TempDir() src := filepath.Join(root, "src") if err := os.MkdirAll(src, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(src, "a"), []byte("a"), 0o644); err != nil { t.Fatal(err) } if err := syscall.Mkfifo(filepath.Join(src, "pipe"), 0o644); err != nil { t.Skip("no fifos here") } dst := filepath.Join(root, "dst") err := moveByCopy(context.Background(), src, dst, &strings.Builder{}) if err == nil || !strings.Contains(err.Error(), "pipe") { t.Fatalf("expected the pipe to be refused, got %v", err) } if _, err := os.Stat(filepath.Join(src, "a")); err != nil { t.Fatal("the original was touched") } if _, err := os.Lstat(dst); !os.IsNotExist(err) { t.Fatal("a partial copy was left") } if left, _ := filepath.Glob(filepath.Join(root, ".*hamlab-moving*")); len(left) != 0 { t.Fatal("temporary copy left behind") } // a cancel is the same story if err := os.Remove(filepath.Join(src, "pipe")); err != nil { t.Fatal(err) } ctx, cancel := context.WithCancel(context.Background()) cancel() if err := moveByCopy(ctx, src, dst, &strings.Builder{}); err == nil { t.Fatal("a cancelled move must fail") } if _, err := os.Stat(filepath.Join(src, "a")); err != nil { t.Fatal("the original was touched by a cancel") } } // Move on one filesystem is a rename: nothing is copied, and the guards hold. func TestMoveSameFilesystem(t *testing.T) { root := t.TempDir() src := filepath.Join(root, "f") if err := os.WriteFile(src, []byte("x"), 0o644); err != nil { t.Fatal(err) } var l Local var out strings.Builder if err := l.Move(context.Background(), src, filepath.Join(root, "g"), &out); err != nil { t.Fatal(err) } if out.Len() != 0 { t.Fatal("a rename has nothing to say") } if _, err := os.Stat(filepath.Join(root, "g")); err != nil { t.Fatal("not renamed") } if err := l.Move(context.Background(), root, filepath.Join(root, "inside"), &out); err == nil { t.Fatal("into itself") } if err := l.Move(context.Background(), filepath.Join(root, "g"), filepath.Join(root, "g"), &out); err == nil { t.Fatal("onto itself") } if err := os.WriteFile(filepath.Join(root, "h"), nil, 0o644); err != nil { t.Fatal(err) } if err := l.Move(context.Background(), filepath.Join(root, "g"), filepath.Join(root, "h"), &out); err == nil || !strings.Contains(err.Error(), "exists") { t.Fatalf("onto an existing file: %v", err) } }