Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 148 additions & 0 deletions sync/copy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package sync

import (
"errors"
"fmt"
"io"
"io/fs"
"log"
"os"

"github.com/diskfs/go-diskfs/disk"
"github.com/diskfs/go-diskfs/filesystem"
"github.com/diskfs/go-diskfs/partition/part"
)

var excludedPaths = map[string]bool{
"lost+found": true,
".DS_Store": true,
"System Volume Information": true,
}

type copyData struct {
count int64
err error
}

// CopyFileSystem copies files from a source fs.FS to a destination filesystem.FileSystem, preserving structure and contents.
func CopyFileSystem(src fs.FS, dst filesystem.FileSystem) error {
return fs.WalkDir(src, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
// filter out special directories/files
if excludedPaths[d.Name()] {
if d.IsDir() {
return fs.SkipDir
}
return nil
}
if path == "." || path == "/" || path == "\\" {
return nil
}

info, err := d.Info()
if err != nil {
return err
}

// symlinks, when they exist
if info.Mode()&os.ModeSymlink != 0 {
// Check if your destination interface supports symlinks
// Most custom 'filesystem.FileSystem' interfaces might not.
return handleSymlink(src, dst, path)
}

if d.IsDir() {
if path == "." {
return nil
}
return dst.Mkdir(path)
}

if !info.Mode().IsRegular() {
// FAT32 / ISO / SquashFS should not have others
return nil
}

return copyOneFile(src, dst, path, info)
})
}

func copyOneFile(src fs.FS, dst filesystem.FileSystem, path string, info fs.FileInfo) error {
in, err := src.Open(path)
if err != nil {
return err
}
defer func() { _ = in.Close() }()

out, err := dst.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_RDWR)
if err != nil {
return err
}
defer func() { _ = out.Close() }()

if _, err := io.Copy(out, in); err != nil {
return err
}

// Restore timestamps *after* data is written (tar semantics)
atime := getAccessTime(info)
if atime.IsZero() {
atime = info.ModTime() // fallback
}
return dst.Chtimes(
path,
info.ModTime(), // creation time fallback if not available
atime, // access time: optional / policy choice
info.ModTime(),
)
}

// handleSymlink handles copying a symlink from src to dst. It reads the link target
//
//nolint:revive,unparam // keeping args for clarity of intent.
func handleSymlink(src fs.FS, dst filesystem.FileSystem, path string) error {
// Note: src must support ReadLink. If src is an os.DirFS,
// you might need a type assertion or use os.Readlink directly.
linkTarget, err := os.Readlink(path)
if err != nil {
return nil // Or handle error
}

// This assumes your 'dst' interface has a Symlink method
return dst.Symlink(linkTarget, path)
}

// CopyPartitionRaw copies raw data from one partition to another and verifies the copy.
func CopyPartitionRaw(d *disk.Disk, from, to int) error {
// copy raw data using a pipe so reads feed writes concurrently
pr, pw := io.Pipe()
ch := make(chan copyData, 1)

go func() {
defer func() { _ = pw.Close() }()
read, err := d.ReadPartitionContents(from, pw)
ch <- copyData{count: read, err: err}
}()

written, err := d.WritePartitionContents(to, pr)
var ierr *part.IncompletePartitionWriteError
if err != nil && !errors.As(err, &ierr) {
return fmt.Errorf("failed to write raw data for partition %d: %v", to, err)
}

readData := <-ch
if readData.err != nil {
return fmt.Errorf("failed to read raw data for partition %d: %v", from, readData.err)
}
if readData.count != written {
return fmt.Errorf("mismatched read/write sizes for partition %d: read %d bytes, wrote %d bytes", from, readData.count, written)
}
log.Printf("partition %d -> %d: contents copied byte for byte, %d bytes copied", from, to, written)
if err := verifyBlockCopy(d, from, to, readData.count); err != nil {
return fmt.Errorf("verification failed for partition %d: %v", from, err)
}
log.Printf("partition %d -> %d: block copy verified", from, to)
return nil
}
206 changes: 206 additions & 0 deletions sync/copy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
package sync

import (
"bytes"
"io/fs"
"os"
"testing"
"testing/fstest"
"time"

"github.com/diskfs/go-diskfs/filesystem"
)

// fakeFS implements filesystem.FileSystem for testing CopyFileSystem.
type fakeFS struct {
dirs []string
files map[string][]byte
times map[string]time.Time
}

// fakeFile satisfies filesystem.File.
type fakeFile struct {
path string
buf *bytes.Buffer
fs *fakeFS
}

// Mkdir records directory creations.
func (f *fakeFS) Mkdir(path string) error {
f.dirs = append(f.dirs, path)
return nil
}

// OpenFile returns a fakeFile for writing.

// Chtimes records the creation time for a file.
//
//nolint:revive // keeping args for clarity of intent.
func (f *fakeFS) Chtimes(path string, ctime, atime, mtime time.Time) error {
f.times[path] = ctime
return nil
}

// Chmod satisfies filesystem.FileSystem interface (no-op).
//
//nolint:revive // keeping args for clarity of intent.
func (f *fakeFS) Chmod(path string, mode os.FileMode) error { return nil }

// Chown satisfies filesystem.FileSystem interface (no-op).
//
//nolint:revive // keeping args for clarity of intent.
func (f *fakeFS) Chown(path string, uid, gid int) error { return nil }

// Remove satisfies filesystem.FileSystem interface (no-op).
//
//nolint:revive // keeping args for clarity of intent.
func (f *fakeFS) Remove(path string) error { return nil }

// Rename satisfies filesystem.FileSystem interface (no-op).
//
//nolint:revive // keeping args for clarity of intent.
func (f *fakeFS) Rename(oldpath, newpath string) error { return nil }

// Stat satisfies filesystem.FileSystem interface (no-op).
//
//nolint:revive // keeping args for clarity of intent.
func (f *fakeFS) Stat(path string) (os.FileInfo, error) { return nil, nil }

// Close satisfies filesystem.FileSystem interface (no-op).
func (f *fakeFS) Close() error { return nil }

// Type satisfies filesystem.FileSystem interface.
func (f *fakeFS) Type() filesystem.Type { return filesystem.TypeFat32 }

// Mknod satisfies filesystem.FileSystem interface.
//
//nolint:revive // keeping args for clarity of intent.
func (f *fakeFS) Mknod(pathname string, mode uint32, dev int) error { return nil }

// Link satisfies filesystem.FileSystem interface.
//
//nolint:revive // keeping args for clarity of intent.
func (f *fakeFS) Link(oldpath, newpath string) error { return nil }

// Symlink satisfies filesystem.FileSystem interface.
//
//nolint:revive // keeping args for clarity of intent.
func (f *fakeFS) Symlink(oldpath, newpath string) error { return nil }

// Open satisfies filesystem.FileSystem interface (unused for copy).
//
//nolint:revive // keeping args for clarity of intent.
func (f *fakeFS) Open(pathname string) (fs.File, error) { return nil, nil }

// ReadDir satisfies fs.ReadDirFS (no-op).
//
//nolint:revive // keeping args for clarity of intent.
func (f *fakeFS) ReadDir(name string) ([]fs.DirEntry, error) { return nil, nil }

// ReadFile satisfies fs.ReadFileFS (no-op).
//
//nolint:revive // keeping args for clarity of intent.
func (f *fakeFS) ReadFile(name string) ([]byte, error) { return nil, nil }

// OpenFile satisfies filesystem.FileSystem interface for writing files.
//
//nolint:revive // flag is unused, keeping for clarity of intent.
func (f *fakeFS) OpenFile(pathname string, flag int) (filesystem.File, error) {
buf := &bytes.Buffer{}
ff := &fakeFile{path: pathname, buf: buf, fs: f}
if f.files == nil {
f.files = make(map[string][]byte)
}
if f.times == nil {
f.times = make(map[string]time.Time)
}
return ff, nil
}

// Label satisfies filesystem.FileSystem interface.
func (f *fakeFS) Label() string { return "" }

// SetLabel satisfies filesystem.FileSystem interface.
//
//nolint:revive // keeping args for clarity of intent.
func (f *fakeFS) SetLabel(label string) error { return nil }

// Write implements io.Writer.
func (f *fakeFile) Write(p []byte) (int, error) {
n, err := f.buf.Write(p)
f.fs.files[f.path] = f.buf.Bytes()
return n, err
}

// Read implements io.Reader (unused here).
func (f *fakeFile) Read(p []byte) (int, error) { return f.buf.Read(p) }

// Close is a no-op.
func (f *fakeFile) Close() error { return nil }

// Seek is a no-op.
//
//nolint:revive // keeping args for clarity of intent.
func (f *fakeFile) Seek(offset int64, whence int) (int64, error) { return 0, nil }

// Stat returns a minimal FileInfo.
func (f *fakeFile) Stat() (os.FileInfo, error) {
return f, nil
}

// The fakeFile itself implements os.FileInfo for simplicity.
func (f *fakeFile) Name() string { return f.path }
func (f *fakeFile) Size() int64 { return int64(f.buf.Len()) }
func (f *fakeFile) Mode() os.FileMode { return 0 }
func (f *fakeFile) ModTime() time.Time { return f.fs.times[f.path] }
func (f *fakeFile) IsDir() bool { return false }
func (f *fakeFile) Sys() interface{} { return nil }

// TestCopyFileSystem_Basic verifies directories and files are copied.
func TestCopyFileSystem_Basic(t *testing.T) {
now := time.Now()
src := fstest.MapFS{
"foo.txt": {Data: []byte("hello"), ModTime: now},
"dir": {Mode: fs.ModeDir, ModTime: now},
"dir/bar": {Data: []byte("world"), ModTime: now},
}
dst := &fakeFS{}
if err := CopyFileSystem(src, dst); err != nil {
t.Fatalf("CopyFileSystem failed: %v", err)
}
// directory created
found := false
for _, d := range dst.dirs {
if d == "dir" {
found = true
}
}
if !found {
t.Errorf("expected Mkdir(\"dir\"), got %v", dst.dirs)
}
// files copied
if string(dst.files["foo.txt"]) != "hello" {
t.Errorf("foo.txt = %q, want %q", dst.files["foo.txt"], "hello")
}
if string(dst.files["dir/bar"]) != "world" {
t.Errorf("dir/bar = %q, want %q", dst.files["dir/bar"], "world")
}
// timestamp recorded (should default to zero time)
if ts, ok := dst.times["foo.txt"]; !ok || ts != now {
t.Errorf("expected timestamp for foo.txt, got %v", ts)
}
}

// TestCopyFileSystem_SkipNonRegular ensures non-regular entries (symlinks) are skipped.
func TestCopyFileSystem_SkipNonRegular(t *testing.T) {
src := fstest.MapFS{
"sl": {Data: []byte(""), Mode: fs.ModeSymlink},
}
dst := &fakeFS{}
if err := CopyFileSystem(src, dst); err != nil {
t.Fatalf("CopyFileSystem failed: %v", err)
}
if _, ok := dst.files["sl"]; ok {
t.Errorf("expected non-regular file to be skipped, but copied")
}
}
23 changes: 23 additions & 0 deletions sync/time_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//go:build linux || darwin || freebsd || netbsd || openbsd

package sync

import (
"io/fs"
"time"

"golang.org/x/sys/unix"
)

func getAccessTime(info fs.FileInfo) time.Time {
sys := info.Sys()
if sys == nil {
// return zero time
return time.Time{}
}
stat, ok := sys.(*unix.Stat_t)
if !ok {
return time.Time{}
}
return time.Unix(stat.Atim.Sec, stat.Atim.Nsec)
}
19 changes: 19 additions & 0 deletions sync/time_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//go:build windows

package sync

import (
"io/fs"
"syscall"
"time"
)

func getAccessTime(info fs.FileInfo) time.Time {
sys := info.Sys()
if sys == nil {
// return zero time
return time.Time{}
}
stat := sys.(*syscall.Win32FileAttributeData)
return time.Unix(0, stat.LastAccessTime.Nanoseconds())
}
Loading