feat: copy xattrs

This commit is contained in:
Samuel N Cui
2026-03-10 23:20:33 +08:00
parent 4a3bab1fbb
commit 2416828cef
8 changed files with 195 additions and 41 deletions
+2 -2
View File
@@ -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))
}
}
+14 -2
View File
@@ -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,
})
+1 -1
View File
@@ -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
+10 -17
View File
@@ -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.")
}
+9 -16
View File
@@ -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
}
+12 -2
View File
@@ -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)
}
+139
View File
@@ -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
}
+8 -1
View File
@@ -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)
}