From 4a3bab1fbb2e1ffe254afe7510ae80ad9e47996c Mon Sep 17 00:00:00 2001 From: Samuel N Cui Date: Tue, 10 Mar 2026 21:08:00 +0800 Subject: [PATCH] feat: add sys attr copy and one to one copy --- .gitignore | 1 + cleanup.go | 6 +++--- cmd/acp/main.go | 47 +++++++++++++++++++++++++++++++++++++++++++--- copy.go | 13 ++++++++++++- index.go | 2 ++ job.go | 3 ++- syscall_darwin.go | 34 +++++++++++++++++++++++++++++++++ syscall_linux.go | 34 +++++++++++++++++++++++++++++++++ syscall_other.go | 23 +++++++++++++++++++++++ syscall_windows.go | 26 +++++++++++++++++++++++++ 10 files changed, 181 insertions(+), 8 deletions(-) create mode 100644 syscall_darwin.go create mode 100644 syscall_linux.go create mode 100644 syscall_other.go create mode 100644 syscall_windows.go diff --git a/.gitignore b/.gitignore index 80518b2..348328b 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ go.sum output/ test.sh +upload_test.sh diff --git a/cleanup.go b/cleanup.go index 401ef59..c24de07 100644 --- a/cleanup.go +++ b/cleanup.go @@ -3,7 +3,6 @@ package acp import ( "context" "fmt" - "os" ) func (c *Copyer) cleanupJob(ctx context.Context, copyed <-chan *baseJob) { @@ -13,9 +12,10 @@ func (c *Copyer) cleanupJob(ctx context.Context, copyed <-chan *baseJob) { if !ok { return } + for _, name := range job.successTargets { - if err := os.Chtimes(name, job.modTime, job.modTime); err != nil { - c.reportError(job.path, name, fmt.Errorf("change info, chtimes fail, %w", err)) + if err := copyAttrs(name, job); err != nil { + c.reportError(job.path, name, fmt.Errorf("change info, copy attrs fail, %w", err)) } } diff --git a/cmd/acp/main.go b/cmd/acp/main.go index a9d74c0..670e0fb 100644 --- a/cmd/acp/main.go +++ b/cmd/acp/main.go @@ -5,6 +5,7 @@ import ( "flag" "os" "os/signal" + "strings" "github.com/klauspost/cpuid/v2" "github.com/samuelncui/acp" @@ -63,7 +64,47 @@ func main() { }() opts := make([]acp.Option, 0, 8) - opts = append(opts, acp.WildcardJob(acp.Source(sources...), acp.Target(targetPaths...))) + + useAccurate := func() bool { + if *noTarget { + return false + } + if len(sources) > 1 { + return false + } + if len(targetPaths) > 1 { + return false + } + + dst, src := targetPaths[0], sources[0] + if strings.HasSuffix(dst, "/") { + return false + } + + dstStat, err := os.Stat(dst) + if err == nil { + return !dstStat.IsDir() + } + if !os.IsNotExist(err) { + logrus.Fatalf("stat dst path fail, %s", err) + panic(err) + } + + srcStat, err := os.Stat(src) + if err == nil { + return srcStat.Mode().IsRegular() + } + + logrus.Fatalf("stat src path fail, %s", err) + panic(err) + }() + + if useAccurate { + opts = append(opts, acp.AccurateJob(sources[0], []string{targetPaths[0]})) + } else { + opts = append(opts, acp.WildcardJob(acp.Source(sources...), acp.Target(targetPaths...))) + } + // if *continueReport != "" { // f, err := os.Open(*continueReport) // if err != nil { @@ -106,8 +147,8 @@ func main() { report := getter() r, err := os.Create(*reportPath) if err != nil { - logrus.Warnf("open report fail, path= '%s', err= %w", *reportPath, err) - logrus.Infof("report: %s", report) + logrus.Warnf("open report fail, path= '%s', err= %s", *reportPath, err) + logrus.Infof("report= %q", report.ToJSONString(false)) return } defer r.Close() diff --git a/copy.go b/copy.go index d87676e..bf51d92 100644 --- a/copy.go +++ b/copy.go @@ -139,6 +139,12 @@ func (c *Copyer) write(ctx context.Context, job *writeJob, ch chan<- *baseJob, c job.fail(target, fmt.Errorf("open dst file fail, %w", err)) continue } + if !job.copyer.toDevice.linear { + if err := truncate(file, job.size); err != nil { + job.fail(target, fmt.Errorf("truncate dst file fail, %w", err)) + continue + } + } ch := make(chan []byte, 4) chans = append(chans, ch) @@ -150,7 +156,7 @@ func (c *Copyer) write(ctx context.Context, job *writeJob, ch chan<- *baseJob, c var rerr error defer func() { if rerr == nil { - job.succes(target) + job.success(target) return } @@ -183,8 +189,13 @@ func (c *Copyer) write(ctx context.Context, job *writeJob, ch chan<- *baseJob, c } } + if err := file.Sync(); err != nil { + rerr = fmt.Errorf("sync dst file fail, %w", err) + return + } if readErr != nil { rerr = readErr + return } }) } diff --git a/index.go b/index.go index 4c49efe..c0c16c2 100644 --- a/index.go +++ b/index.go @@ -98,6 +98,7 @@ func (c *Copyer) walk(ctx context.Context) ([]*baseJob, error) { size: stat.Size(), mode: stat.Mode(), modTime: stat.ModTime(), + sys: stat.Sys(), targets: targets, }) @@ -154,6 +155,7 @@ func (c *Copyer) walk(ctx context.Context) ([]*baseJob, error) { size: stat.Size(), mode: stat.Mode(), modTime: stat.ModTime(), + sys: stat.Sys(), targets: j.dsts, }) diff --git a/job.go b/job.go index 42e6372..e281a05 100644 --- a/job.go +++ b/job.go @@ -42,6 +42,7 @@ type baseJob struct { size int64 // length in bytes for regular files; system-dependent for others mode fs.FileMode // file mode bits modTime time.Time // modification time + sys any lock sync.Mutex writeTime time.Time @@ -73,7 +74,7 @@ func (j *baseJob) setHash(h []byte) { j.copyer.submit(&EventUpdateJob{j.report()}) } -func (j *baseJob) succes(path string) { +func (j *baseJob) success(path string) { j.lock.Lock() defer j.lock.Unlock() diff --git a/syscall_darwin.go b/syscall_darwin.go new file mode 100644 index 0000000..85965cb --- /dev/null +++ b/syscall_darwin.go @@ -0,0 +1,34 @@ +//go:build darwin +// +build darwin + +package acp + +import ( + "fmt" + "os" + "syscall" +) + +func truncate(file *os.File, size int64) error { + if err := file.Truncate(size); err != nil { + return err + } + return nil +} + +func copyAttrs(name string, j *baseJob) error { + if err := os.Chmod(name, j.mode); err != nil { + return fmt.Errorf("chmod fail, %w", err) + } + if os.Geteuid() == 0 { + if stat, ok := j.sys.(*syscall.Stat_t); ok { + if err := os.Chown(name, int(stat.Uid), int(stat.Gid)); err != nil { + return fmt.Errorf("chown fail, %w", err) + } + } + } + if err := os.Chtimes(name, j.modTime, j.modTime); err != nil { + return fmt.Errorf("chtimes fail, %w", err) + } + return nil +} diff --git a/syscall_linux.go b/syscall_linux.go new file mode 100644 index 0000000..1f8a9e4 --- /dev/null +++ b/syscall_linux.go @@ -0,0 +1,34 @@ +//go:build linux +// +build linux + +package acp + +import ( + "fmt" + "os" + "syscall" +) + +func truncate(file *os.File, size int64) error { + if err := syscall.Fallocate(int(file.Fd()), 0, 0, size); err != nil { + return err + } + return nil +} + +func copyAttrs(name string, j *baseJob) error { + if err := os.Chmod(name, j.mode); err != nil { + return fmt.Errorf("chmod fail, %w", err) + } + if os.Geteuid() == 0 { + if stat, ok := j.sys.(*syscall.Stat_t); ok { + if err := os.Chown(name, int(stat.Uid), int(stat.Gid)); err != nil { + return fmt.Errorf("chown fail, %w", err) + } + } + } + if err := os.Chtimes(name, j.modTime, j.modTime); err != nil { + return fmt.Errorf("chtimes fail, %w", err) + } + return nil +} diff --git a/syscall_other.go b/syscall_other.go new file mode 100644 index 0000000..4aaa301 --- /dev/null +++ b/syscall_other.go @@ -0,0 +1,23 @@ +//go:build !linux && !darwin && !windows +// +build !linux,!darwin,!windows + +package acp + +import ( + "fmt" + "os" +) + +func truncate(_ *os.File, _ int64) error { + return nil +} + +func copyAttrs(name string, j *baseJob) error { + if err := os.Chmod(name, j.mode); err != nil { + return fmt.Errorf("chmod fail, %w", err) + } + if err := os.Chtimes(name, j.modTime, j.modTime); err != nil { + return fmt.Errorf("chtimes fail, %w", err) + } + return nil +} diff --git a/syscall_windows.go b/syscall_windows.go new file mode 100644 index 0000000..984fc43 --- /dev/null +++ b/syscall_windows.go @@ -0,0 +1,26 @@ +//go:build windows +// +build windows + +package acp + +import ( + "fmt" + "os" +) + +func truncate(file *os.File, size int64) error { + if err := file.Truncate(size); err != nil { + return err + } + return nil +} + +func copyAttrs(name string, j *baseJob) error { + if err := os.Chmod(name, j.mode); err != nil { + return fmt.Errorf("chmod fail, %w", err) + } + if err := os.Chtimes(name, j.modTime, j.modTime); err != nil { + return fmt.Errorf("chtimes fail, %w", err) + } + return nil +}