From f71bebab4401bd81564f796d4633f202c48a9e3a Mon Sep 17 00:00:00 2001 From: Nick Craig-Wood Date: Sun, 24 May 2026 17:21:26 +0100 Subject: [PATCH] serve sftp: use the requested atime when setting file times When a SETSTAT request set the access and modification times, the handler passed the modification time for both, discarding the requested access time. Pass the requested access time through instead. The VFS currently ignores the atime, but it might use it one day. --- cmd/serve/sftp/handler.go | 5 +++-- cmd/serve/sftp/handler_test.go | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/cmd/serve/sftp/handler.go b/cmd/serve/sftp/handler.go index 04adaf432..1b3a1ab58 100644 --- a/cmd/serve/sftp/handler.go +++ b/cmd/serve/sftp/handler.go @@ -76,8 +76,9 @@ func (v vfsHandler) Filecmd(r *sftp.Request) error { } } if flags.Acmodtime { - modTime := time.Unix(int64(attr.Mtime), 0) - err := v.Chtimes(r.Filepath, modTime, modTime) + atime := time.Unix(int64(attr.Atime), 0) + mtime := time.Unix(int64(attr.Mtime), 0) + err := v.Chtimes(r.Filepath, atime, mtime) if err != nil { return err } diff --git a/cmd/serve/sftp/handler_test.go b/cmd/serve/sftp/handler_test.go index f85af9543..32cdec212 100644 --- a/cmd/serve/sftp/handler_test.go +++ b/cmd/serve/sftp/handler_test.go @@ -12,6 +12,7 @@ import ( "os" "strings" "testing" + "time" "github.com/pkg/sftp" _ "github.com/rclone/rclone/backend/local" @@ -183,6 +184,24 @@ func TestStatVFS(t *testing.T) { assert.Equal(t, uint64(255), st.Namemax) } +// Test that a SETSTAT request setting the modification time is honoured, even +// when the time happens to be the Unix epoch +func TestSetstatMtime(t *testing.T) { + vfsOpt := vfscommon.Opt + vfsOpt.CacheMode = vfscommon.CacheModeOff + client := startTestServer(t, &vfsOpt) + + const fileName = "file.bin" + require.NoError(t, writeFile(client, fileName, "hello")) + + epoch := time.Unix(0, 0) + require.NoError(t, client.Chtimes(fileName, epoch, epoch)) + + fi, err := client.Stat(fileName) + require.NoError(t, err) + assert.True(t, fi.ModTime().Equal(epoch), "mtime not applied: got %v want %v", fi.ModTime(), epoch) +} + // writeFile writes contents to fileName via the client truncating any existing // data, the way a normal upload does. func writeFile(client *sftp.Client, fileName, contents string) error {