From 2416828cef25690ab32f1fd988e92180b6963283 Mon Sep 17 00:00:00 2001 From: Samuel N Cui Date: Tue, 10 Mar 2026 23:20:33 +0800 Subject: [PATCH] feat: copy xattrs --- cleanup.go | 4 +- index.go | 16 +++++- job.go | 2 +- syscall_darwin.go | 27 ++++----- syscall_linux.go | 25 +++----- syscall_other.go | 14 ++++- syscall_unix.go | 139 +++++++++++++++++++++++++++++++++++++++++++++ syscall_windows.go | 9 ++- 8 files changed, 195 insertions(+), 41 deletions(-) create mode 100644 syscall_unix.go diff --git a/cleanup.go b/cleanup.go index c24de07..1228966 100644 --- a/cleanup.go +++ b/cleanup.go @@ -14,8 +14,8 @@ func (c *Copyer) cleanupJob(ctx context.Context, copyed <-chan *baseJob) { } for _, name := range job.successTargets { - if err := copyAttrs(name, job); err != nil { - c.reportError(job.path, name, fmt.Errorf("change info, copy attrs fail, %w", err)) + if err := writeSysStat(name, job); err != nil { + c.reportError(job.path, name, fmt.Errorf("change info, write sys stat fail, %w", err)) } } diff --git a/index.go b/index.go index c0c16c2..cc1d338 100644 --- a/index.go +++ b/index.go @@ -90,6 +90,12 @@ func (c *Copyer) walk(ctx context.Context) ([]*baseJob, error) { targets = append(targets, src.dst(d)) } + sysStat, err := readSysStat(path, stat) + if err != nil { + c.reportError(path, "", fmt.Errorf("read sys stat, %w", err)) + return + } + appendJob(&baseJob{ copyer: c, src: src, @@ -98,7 +104,7 @@ func (c *Copyer) walk(ctx context.Context) ([]*baseJob, error) { size: stat.Size(), mode: stat.Mode(), modTime: stat.ModTime(), - sys: stat.Sys(), + sys: sysStat, targets: targets, }) @@ -147,6 +153,12 @@ func (c *Copyer) walk(ctx context.Context) ([]*baseJob, error) { continue } + sysStat, err := readSysStat(j.src, stat) + if err != nil { + c.reportError(j.src, "", fmt.Errorf("read sys stat, %w", err)) + continue + } + appendJob(&baseJob{ copyer: c, src: &source{base: "/", path: lo.Filter(strings.Split(j.src, "/"), func(s string, _ int) bool { return s != "" })}, @@ -155,7 +167,7 @@ func (c *Copyer) walk(ctx context.Context) ([]*baseJob, error) { size: stat.Size(), mode: stat.Mode(), modTime: stat.ModTime(), - sys: stat.Sys(), + sys: sysStat, targets: j.dsts, }) diff --git a/job.go b/job.go index e281a05..e5841b9 100644 --- a/job.go +++ b/job.go @@ -42,7 +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 + sys *sysStat lock sync.Mutex writeTime time.Time diff --git a/syscall_darwin.go b/syscall_darwin.go index 85965cb..20fd40a 100644 --- a/syscall_darwin.go +++ b/syscall_darwin.go @@ -4,9 +4,11 @@ package acp import ( - "fmt" + "errors" "os" - "syscall" + "strings" + + "golang.org/x/sys/unix" ) func truncate(file *os.File, size int64) error { @@ -16,19 +18,10 @@ func truncate(file *os.File, size 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 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 +func isNoAttrErr(err error) bool { + return errors.Is(err, unix.ENOATTR) || errors.Is(err, unix.ENODATA) +} + +func checkXattrKey(key string) bool { + return !strings.HasPrefix(key, "system.") } diff --git a/syscall_linux.go b/syscall_linux.go index 1f8a9e4..358ba0e 100644 --- a/syscall_linux.go +++ b/syscall_linux.go @@ -4,9 +4,11 @@ package acp import ( - "fmt" + "errors" "os" "syscall" + + "golang.org/x/sys/unix" ) func truncate(file *os.File, size int64) error { @@ -16,19 +18,10 @@ func truncate(file *os.File, size 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 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 +func isNoAttrErr(err error) bool { + return errors.Is(err, unix.ENODATA) +} + +func checkXattrKey(key string) bool { + return true } diff --git a/syscall_other.go b/syscall_other.go index 4aaa301..151ba06 100644 --- a/syscall_other.go +++ b/syscall_other.go @@ -5,14 +5,24 @@ package acp import ( "fmt" + "io/fs" "os" ) -func truncate(_ *os.File, _ int64) error { +type sysStat struct{} + +func readSysStat(path string, stat fs.FileInfo) (*sysStat, error) { + return nil, nil +} + +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 { +func writeSysStat(name string, j *baseJob) error { if err := os.Chmod(name, j.mode); err != nil { return fmt.Errorf("chmod fail, %w", err) } diff --git a/syscall_unix.go b/syscall_unix.go new file mode 100644 index 0000000..1f32881 --- /dev/null +++ b/syscall_unix.go @@ -0,0 +1,139 @@ +//go:build darwin || linux +// +build darwin linux + +package acp + +import ( + "errors" + "fmt" + "io/fs" + "os" + "syscall" + + "golang.org/x/sys/unix" +) + +type xattr struct { + key string + value []byte +} + +type sysStat struct { + *syscall.Stat_t + xattrs []xattr +} + +func readSysStat(path string, stat fs.FileInfo) (*sysStat, error) { + sysstat, ok := stat.Sys().(*syscall.Stat_t) + if !ok { + return nil, fmt.Errorf("stat sys failed, %T", stat.Sys()) + } + + xattrs, err := readXattrs(path) + if err != nil { + return nil, fmt.Errorf("read xattrs failed, %w", err) + } + + return &sysStat{Stat_t: sysstat, xattrs: xattrs}, nil +} + +func writeSysStat(name string, j *baseJob) error { + if err := writeXattrs(name, j.sys.xattrs); err != nil { + return fmt.Errorf("write xattr fail, %w", err) + } + if err := os.Chmod(name, j.mode); err != nil { + return fmt.Errorf("chmod fail, %w", err) + } + if os.Geteuid() == 0 { + if err := os.Chown(name, int(j.sys.Uid), int(j.sys.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 +} + +func readXattrs(path string) ([]xattr, error) { + size, err := unix.Listxattr(path, nil) + if err != nil { + if errors.Is(err, unix.ENOTSUP) || isNoAttrErr(err) { + return nil, nil + } + return nil, fmt.Errorf("count xattrs failed, %w", err) + } + if size == 0 { + return nil, nil + } + + keyBuf := make([]byte, size) + n, err := unix.Listxattr(path, keyBuf) + if err != nil { + if errors.Is(err, unix.ENOTSUP) || isNoAttrErr(err) { + return nil, nil + } + return nil, fmt.Errorf("list xattrs failed, %w", err) + } + keyBuf = keyBuf[:n] + + start := 0 + xattrs := make([]xattr, 0) + for i, b := range keyBuf { + if b != 0 { + continue + } + if i <= start { + start = i + 1 + continue + } + + name := string(keyBuf[start:i]) + start = i + 1 + if name == "" { + continue + } + if !checkXattrKey(name) { + continue + } + + valSize, err := unix.Getxattr(path, name, nil) + if err != nil { + if isNoAttrErr(err) { + continue + } + return nil, err + } + if valSize == 0 { + xattrs = append(xattrs, xattr{key: name, value: []byte{}}) + continue + } + + val := make([]byte, valSize) + n, err := unix.Getxattr(path, name, val) + if err != nil { + if isNoAttrErr(err) { + continue + } + return nil, err + } + + xattrs = append(xattrs, xattr{key: name, value: val[:n]}) + } + + return xattrs, nil +} + +func writeXattrs(path string, xattrs []xattr) error { + for _, xattr := range xattrs { + if err := unix.Setxattr(path, xattr.key, xattr.value, 0); err != nil { + if errors.Is(err, unix.ENOTSUP) || + errors.Is(err, unix.EPERM) || + errors.Is(err, unix.EROFS) { + continue + } + return err + } + } + return nil +} diff --git a/syscall_windows.go b/syscall_windows.go index 984fc43..684b43a 100644 --- a/syscall_windows.go +++ b/syscall_windows.go @@ -5,9 +5,16 @@ package acp import ( "fmt" + "io/fs" "os" ) +type sysStat struct{} + +func readSysStat(path string, stat fs.FileInfo) (*sysStat, error) { + return nil, nil +} + func truncate(file *os.File, size int64) error { if err := file.Truncate(size); err != nil { return err @@ -15,7 +22,7 @@ func truncate(file *os.File, size int64) error { return nil } -func copyAttrs(name string, j *baseJob) error { +func writeSysStat(name string, j *baseJob) error { if err := os.Chmod(name, j.mode); err != nil { return fmt.Errorf("chmod fail, %w", err) }