Skip to content

Commit 3bdeac5

Browse files
committed
Preserve file timestamps during transfer
Add os.Chtimes() calls to preserve modification times when receiving files. Timestamps are now preserved for: - Regular files after reception - Empty files during creation - Files when initially created for writing - Symlinks (with platform-specific behavior noted) Also add comprehensive test (TestCrocTimestampPreservation) to verify timestamp preservation works correctly across file transfers.
1 parent 74e69e9 commit 3bdeac5

File tree

2 files changed

+144
-4
lines changed

2 files changed

+144
-4
lines changed

src/croc/croc.go

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1654,6 +1654,14 @@ func (c *Client) recipientInitializeFile() (err error) {
16541654
if errChmod != nil {
16551655
log.Error(errChmod)
16561656
}
1657+
// Preserve file modification time when creating file
1658+
modTime := c.FilesToTransfer[c.FilesToTransferCurrentNum].ModTime
1659+
if !modTime.IsZero() {
1660+
errChtimes := os.Chtimes(pathToFile, time.Now(), modTime)
1661+
if errChtimes != nil {
1662+
log.Debugf("error setting modification time for %s: %v", pathToFile, errChtimes)
1663+
}
1664+
}
16571665
truncate = true
16581666
}
16591667
if truncate {
@@ -1754,6 +1762,14 @@ func (c *Client) createEmptyFileAndFinish(fileInfo FileInfo, i int) (err error)
17541762
if err != nil {
17551763
return
17561764
}
1765+
// Preserve file modification time for symlinks
1766+
// Note: os.Chtimes follows symlinks on most systems, changing the target's time
1767+
if !fileInfo.ModTime.IsZero() {
1768+
errChtimes := os.Chtimes(pathToFile, time.Now(), fileInfo.ModTime)
1769+
if errChtimes != nil {
1770+
log.Debugf("error setting modification time for symlink %s: %v", pathToFile, errChtimes)
1771+
}
1772+
}
17571773
} else {
17581774
emptyFile, errCreate := os.Create(pathToFile)
17591775
if errCreate != nil {
@@ -1762,6 +1778,13 @@ func (c *Client) createEmptyFileAndFinish(fileInfo FileInfo, i int) (err error)
17621778
return
17631779
}
17641780
emptyFile.Close()
1781+
// Preserve file modification time for empty files
1782+
if !fileInfo.ModTime.IsZero() {
1783+
errChtimes := os.Chtimes(pathToFile, time.Now(), fileInfo.ModTime)
1784+
if errChtimes != nil {
1785+
log.Debugf("error setting modification time for %s: %v", pathToFile, errChtimes)
1786+
}
1787+
}
17651788
}
17661789
// setup the progressbar
17671790
description := fmt.Sprintf("%-*s", c.longestFilename, c.FilesToTransfer[i].Name)
@@ -2021,11 +2044,19 @@ func (c *Client) receiveData(i int) {
20212044
} else {
20222045
log.Debugf("Successful closing %s", c.CurrentFile.Name())
20232046
}
2047+
// Preserve file modification time
2048+
pathToFile := path.Join(
2049+
c.FilesToTransfer[c.FilesToTransferCurrentNum].FolderRemote,
2050+
c.FilesToTransfer[c.FilesToTransferCurrentNum].Name,
2051+
)
2052+
modTime := c.FilesToTransfer[c.FilesToTransferCurrentNum].ModTime
2053+
if !modTime.IsZero() {
2054+
errChtimes := os.Chtimes(pathToFile, time.Now(), modTime)
2055+
if errChtimes != nil {
2056+
log.Debugf("error setting modification time for %s: %v", pathToFile, errChtimes)
2057+
}
2058+
}
20242059
if c.Options.Stdout || c.Options.SendingText {
2025-
pathToFile := path.Join(
2026-
c.FilesToTransfer[c.FilesToTransferCurrentNum].FolderRemote,
2027-
c.FilesToTransfer[c.FilesToTransferCurrentNum].Name,
2028-
)
20292060
b, _ := os.ReadFile(pathToFile)
20302061
fmt.Print(string(b))
20312062
}

src/croc/croc_test.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,115 @@ func TestReceiverStdoutWithInvalidSecret(t *testing.T) {
396396
log.Debugf("Expected error occurred: %v", err)
397397
}
398398

399+
func TestCrocTimestampPreservation(t *testing.T) {
400+
log.SetLevel("trace")
401+
testFileName := "timestamp_test.txt"
402+
defer os.Remove(testFileName)
403+
time.Sleep(300 * time.Millisecond)
404+
405+
// Create a test file with specific content and timestamp
406+
content := []byte("test content for timestamp preservation")
407+
err := os.WriteFile(testFileName, content, 0o644)
408+
if err != nil {
409+
t.Fatalf("failed to create test file: %v", err)
410+
}
411+
412+
// Set a specific modification time (January 15, 2023 12:30:00)
413+
testTime := time.Date(2023, 1, 15, 12, 30, 0, 0, time.UTC)
414+
err = os.Chtimes(testFileName, time.Now(), testTime)
415+
if err != nil {
416+
t.Fatalf("failed to set test file timestamp: %v", err)
417+
}
418+
419+
// Verify the timestamp was set correctly
420+
fileInfo, err := os.Stat(testFileName)
421+
if err != nil {
422+
t.Fatalf("failed to stat test file: %v", err)
423+
}
424+
originalModTime := fileInfo.ModTime()
425+
log.Debugf("Original file modification time: %v", originalModTime)
426+
427+
log.Debug("setting up sender")
428+
sender, err := New(Options{
429+
IsSender: true,
430+
SharedSecret: "8125-timestamp-test",
431+
Debug: true,
432+
RelayAddress: "127.0.0.1:8281",
433+
RelayPorts: []string{"8281"},
434+
RelayPassword: "pass123",
435+
Stdout: false,
436+
NoPrompt: true,
437+
DisableLocal: true,
438+
Curve: "siec",
439+
Overwrite: true,
440+
GitIgnore: false,
441+
})
442+
if err != nil {
443+
panic(err)
444+
}
445+
446+
log.Debug("setting up receiver")
447+
receiver, err := New(Options{
448+
IsSender: false,
449+
SharedSecret: "8125-timestamp-test",
450+
Debug: true,
451+
RelayAddress: "127.0.0.1:8281",
452+
RelayPassword: "pass123",
453+
Stdout: false,
454+
NoPrompt: true,
455+
DisableLocal: true,
456+
Curve: "siec",
457+
Overwrite: true,
458+
})
459+
if err != nil {
460+
panic(err)
461+
}
462+
463+
var wg sync.WaitGroup
464+
wg.Add(2)
465+
go func() {
466+
filesInfo, emptyFolders, totalNumberFolders, errGet := GetFilesInfo([]string{testFileName}, false, false, []string{})
467+
if errGet != nil {
468+
t.Errorf("failed to get file info: %v", errGet)
469+
}
470+
err := sender.Send(filesInfo, emptyFolders, totalNumberFolders)
471+
if err != nil {
472+
t.Errorf("send failed: %v", err)
473+
}
474+
wg.Done()
475+
}()
476+
time.Sleep(100 * time.Millisecond)
477+
go func() {
478+
err := receiver.Receive()
479+
if err != nil {
480+
t.Errorf("receive failed: %v", err)
481+
}
482+
wg.Done()
483+
}()
484+
485+
wg.Wait()
486+
487+
// Verify the received file has the same timestamp
488+
receivedFileInfo, err := os.Stat(testFileName)
489+
if err != nil {
490+
t.Fatalf("failed to stat received file: %v", err)
491+
}
492+
receivedModTime := receivedFileInfo.ModTime()
493+
log.Debugf("Received file modification time: %v", receivedModTime)
494+
495+
// Allow for small time differences due to filesystem precision (up to 2 seconds)
496+
timeDiff := receivedModTime.Sub(originalModTime)
497+
if timeDiff < 0 {
498+
timeDiff = -timeDiff
499+
}
500+
if timeDiff > 2*time.Second {
501+
t.Errorf("timestamp not preserved: original=%v, received=%v, diff=%v",
502+
originalModTime, receivedModTime, timeDiff)
503+
} else {
504+
log.Debugf("Timestamp preserved successfully (diff=%v)", timeDiff)
505+
}
506+
}
507+
399508
func TestCleanUp(t *testing.T) {
400509
// windows allows files to be deleted only if they
401510
// are not open by another program so the remove actions

0 commit comments

Comments
 (0)