diff --git a/chtimes_nolinux.go b/chtimes_nolinux.go index a3ba0988..08251ec2 100644 --- a/chtimes_nolinux.go +++ b/chtimes_nolinux.go @@ -1,3 +1,4 @@ +//go:build !linux // +build !linux package fsutil diff --git a/copy/mkdir.go b/copy/mkdir.go index c2fdd8be..5400d6cd 100644 --- a/copy/mkdir.go +++ b/copy/mkdir.go @@ -23,9 +23,11 @@ func MkdirAll(path string, perm os.FileMode, user Chowner, tm *time.Time) error i-- } j := i + for j > 0 && !os.IsPathSeparator(path[j-1]) { // Scan backward over element. j-- } + if j > 1 { // Create parent. err = MkdirAll(fixRootDirectory(path[:j-1]), perm, user, tm) @@ -33,6 +35,7 @@ func MkdirAll(path string, perm os.FileMode, user Chowner, tm *time.Time) error return err } } + dir, err1 := os.Lstat(path) if err1 == nil && dir.IsDir() { return nil diff --git a/diff.go b/diff.go index d9fa5a0c..a7405dc5 100644 --- a/diff.go +++ b/diff.go @@ -4,7 +4,6 @@ import ( "context" "hash" "os" - "path/filepath" "github.com/pkg/errors" "github.com/tonistiigi/fsutil/types" @@ -33,7 +32,7 @@ func getWalkerFn(root string) walkerFn { } p := ¤tPath{ - path: filepath.FromSlash(path), + path: path, stat: stat, } diff --git a/diskwriter.go b/diskwriter.go index a9ba1de9..37c85f57 100644 --- a/diskwriter.go +++ b/diskwriter.go @@ -4,6 +4,7 @@ import ( "context" "hash" "io" + gofs "io/fs" "os" "path/filepath" "strconv" @@ -33,10 +34,11 @@ type DiskWriter struct { opt DiskWriterOpt dest string - ctx context.Context - cancel func() - eg *errgroup.Group - filter FilterFunc + ctx context.Context + cancel func() + eg *errgroup.Group + filter FilterFunc + dirModTimes map[string]int64 } func NewDiskWriter(ctx context.Context, dest string, opt DiskWriterOpt) (*DiskWriter, error) { @@ -51,17 +53,32 @@ func NewDiskWriter(ctx context.Context, dest string, opt DiskWriterOpt) (*DiskWr eg, ctx := errgroup.WithContext(ctx) return &DiskWriter{ - opt: opt, - dest: dest, - eg: eg, - ctx: ctx, - cancel: cancel, - filter: opt.Filter, + opt: opt, + dest: dest, + eg: eg, + ctx: ctx, + cancel: cancel, + filter: opt.Filter, + dirModTimes: map[string]int64{}, }, nil } func (dw *DiskWriter) Wait(ctx context.Context) error { - return dw.eg.Wait() + if err := dw.eg.Wait(); err != nil { + return err + } + return filepath.WalkDir(dw.dest, func(path string, d gofs.DirEntry, prevErr error) error { + if prevErr != nil { + return prevErr + } + if !d.IsDir() { + return nil + } + if mtime, ok := dw.dirModTimes[path]; ok { + return chtimes(path, mtime) + } + return nil + }) } func (dw *DiskWriter) HandleChange(kind ChangeKind, p string, fi os.FileInfo, err error) (retErr error) { @@ -147,6 +164,7 @@ func (dw *DiskWriter) HandleChange(kind ChangeKind, p string, fi os.FileInfo, er if err := os.Mkdir(newPath, fi.Mode()); err != nil { return errors.Wrapf(err, "failed to create dir %s", newPath) } + dw.dirModTimes[destPath] = statCopy.ModTime case fi.Mode()&os.ModeDevice != 0 || fi.Mode()&os.ModeNamedPipe != 0: if err := handleTarTypeBlockCharFifo(newPath, &statCopy); err != nil { return errors.Wrapf(err, "failed to create device %s", newPath) diff --git a/receive_test.go b/receive_test.go index fe639c58..e38ef200 100644 --- a/receive_test.go +++ b/receive_test.go @@ -99,6 +99,42 @@ func TestCopyWithSubDir(t *testing.T) { assert.Equal(t, "data1", string(dt)) } +func TestCopyDirectoryTimestamps(t *testing.T) { + d, err := tmpDir(changeStream([]string{ + "ADD foo dir", + "ADD foo/bar file data1", + })) + assert.NoError(t, err) + defer os.RemoveAll(d) + + timestamp := time.Unix(0, 0) + require.NoError(t, os.Chtimes(filepath.Join(d, "foo"), timestamp, timestamp)) + + dest := t.TempDir() + + eg, ctx := errgroup.WithContext(context.Background()) + s1, s2 := sockPairProto(ctx) + + eg.Go(func() error { + defer s1.(*fakeConnProto).closeSend() + return Send(ctx, s1, NewFS(d, nil), nil) + }) + eg.Go(func() error { + return Receive(ctx, s2, dest, ReceiveOpt{}) + }) + + err = eg.Wait() + assert.NoError(t, err) + + dt, err := os.ReadFile(filepath.Join(dest, "foo/bar")) + assert.NoError(t, err) + assert.Equal(t, "data1", string(dt)) + + stat, err := os.Stat(filepath.Join(dest, "foo")) + require.NoError(t, err) + assert.Equal(t, timestamp, stat.ModTime()) +} + func TestCopySwitchDirToFile(t *testing.T) { d, err := tmpDir(changeStream([]string{ "ADD foo file data1",