diff --git a/pkg/archive/archive.go b/pkg/archive/archive.go index 982d0f0f5..ad64b4ad0 100644 --- a/pkg/archive/archive.go +++ b/pkg/archive/archive.go @@ -16,6 +16,7 @@ import ( "strings" "sync" "syscall" + "time" "github.com/containers/storage/pkg/fileutils" "github.com/containers/storage/pkg/idtools" @@ -67,6 +68,8 @@ type ( CopyPass bool // ForceMask, if set, indicates the permission mask used for created files. ForceMask *os.FileMode + // Timestamp, if set, will be set in each header as create/mod/access time + Timestamp *time.Time } ) @@ -494,15 +497,19 @@ type tarWriter struct { // from the traditional behavior/format to get features like subsecond // precision in timestamps. CopyPass bool + + // Timestamp, if set, will be set in each header as create/mod/access time + Timestamp *time.Time } -func newTarWriter(idMapping *idtools.IDMappings, writer io.Writer, chownOpts *idtools.IDPair) *tarWriter { +func newTarWriter(idMapping *idtools.IDMappings, writer io.Writer, chownOpts *idtools.IDPair, timestamp *time.Time) *tarWriter { return &tarWriter{ SeenFiles: make(map[uint64]string), TarWriter: tar.NewWriter(writer), Buffer: pools.BufioWriter32KPool.Get(nil), IDMappings: idMapping, ChownOpts: chownOpts, + Timestamp: timestamp, } } @@ -600,6 +607,13 @@ func (ta *tarWriter) addFile(path, name string) error { hdr.Gname = "" } + // if override timestamp set, replace all times with this + if ta.Timestamp != nil { + hdr.ModTime = *ta.Timestamp + hdr.AccessTime = *ta.Timestamp + hdr.ChangeTime = *ta.Timestamp + } + maybeTruncateHeaderModTime(hdr) if ta.WhiteoutConverter != nil { @@ -866,6 +880,7 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error) idtools.NewIDMappingsFromMaps(options.UIDMaps, options.GIDMaps), compressWriter, options.ChownOpts, + options.Timestamp, ) ta.WhiteoutConverter = GetWhiteoutConverter(options.WhiteoutFormat, options.WhiteoutData) ta.CopyPass = options.CopyPass diff --git a/pkg/archive/archive_test.go b/pkg/archive/archive_test.go index 01b137b33..ec05231d3 100644 --- a/pkg/archive/archive_test.go +++ b/pkg/archive/archive_test.go @@ -12,6 +12,7 @@ import ( "runtime" "strings" "testing" + "time" "github.com/containers/storage/pkg/idtools" "github.com/stretchr/testify/assert" @@ -1222,3 +1223,46 @@ func readFileFromArchive(t *testing.T, archive io.ReadCloser, name string, expec assert.NoError(t, err) return string(content) } + +func TestTimestamp(t *testing.T) { + // write single file into dir that we'll tar + td := t.TempDir() + tf := filepath.Join(td, "foo") + + require.NoError(t, os.WriteFile(tf, []byte("bar"), 0o644)) + + // helper function to tar that dir and return byte slice + tarToByteSlice := func(options *TarOptions) []byte { + rc, err := TarWithOptions(td, options) + assert.NoError(t, err) + defer rc.Close() + + rv, err := io.ReadAll(rc) + assert.NoError(t, err) + return rv + } + + // default options + defaultOptions := &TarOptions{} + + // override timestamp option + epochOptions := &TarOptions{Timestamp: &time.Time{}} + + // get tar bytes slices now + origTarDefaultOptions := tarToByteSlice(defaultOptions) + origTarEpochOptions := tarToByteSlice(epochOptions) + + // set the mod time of the file to an hour later + oneHourLater := time.Now().Add(time.Hour) + require.NoError(t, os.Chtimes(tf, oneHourLater, oneHourLater)) + + // then tar again + laterTarDefaultOptions := tarToByteSlice(defaultOptions) + laterTarEpochOptions := tarToByteSlice(epochOptions) + + // we expect the ones without a fixed timestamp to be different + assert.NotEqual(t, origTarDefaultOptions, laterTarDefaultOptions) + + // we expect the ones with a fixed timestamp to be the same + assert.Equal(t, origTarEpochOptions, laterTarEpochOptions) +} diff --git a/pkg/archive/changes.go b/pkg/archive/changes.go index f5eefd579..37e9f947f 100644 --- a/pkg/archive/changes.go +++ b/pkg/archive/changes.go @@ -452,7 +452,7 @@ func ChangesSize(newDir string, changes []Change) int64 { func ExportChanges(dir string, changes []Change, uidMaps, gidMaps []idtools.IDMap) (io.ReadCloser, error) { reader, writer := io.Pipe() go func() { - ta := newTarWriter(idtools.NewIDMappingsFromMaps(uidMaps, gidMaps), writer, nil) + ta := newTarWriter(idtools.NewIDMappingsFromMaps(uidMaps, gidMaps), writer, nil, nil) // this buffer is needed for the duration of this piped stream defer pools.BufioWriter32KPool.Put(ta.Buffer)