diff --git a/sshfs.c b/sshfs.c index 3bd4ec5..ddb4a52 100644 --- a/sshfs.c +++ b/sshfs.c @@ -2873,21 +2873,36 @@ static void sshfs_inc_modifver(void) pthread_mutex_unlock(&sshfs.lock); } +static int sshfs_getattr(const char *path, struct stat *stbuf, + struct fuse_file_info *fi); + +/* + * SSH_FILEXFER_ATTR_ACMODTIME always sets both atime and mtime, so a time the + * caller wants left alone (UTIME_OMIT) has to be sent as its current value. + * Treating it as "now" makes an atime-only update (e.g. Finder recording that + * a file was previewed) overwrite the file's modification time. + */ +static time_t utimens_resolve(const struct timespec *ts, time_t cur, time_t now) +{ + if (ts->tv_nsec == UTIME_OMIT) + return cur; + if (ts->tv_nsec == UTIME_NOW) + return now; + return ts->tv_sec; +} + static int sshfs_utimens(const char *path, const struct timespec tv[2], struct fuse_file_info *fi) { - (void) fi; int err; struct buffer buf; struct sshfs_file *sf = NULL; - time_t asec = tv[0].tv_sec, msec = tv[1].tv_sec; + struct stat cur; + time_t now = time(NULL); + time_t asec, msec; - struct timeval now; - gettimeofday(&now, NULL); - if (asec == 0) - asec = now.tv_sec; - if (msec == 0) - msec = now.tv_sec; + if (tv[0].tv_nsec == UTIME_OMIT && tv[1].tv_nsec == UTIME_OMIT) + return 0; if (fi != NULL) { sf = get_sshfs_file(fi); @@ -2895,6 +2910,15 @@ static int sshfs_utimens(const char *path, const struct timespec tv[2], return -EIO; } + memset(&cur, 0, sizeof(cur)); + if (tv[0].tv_nsec == UTIME_OMIT || tv[1].tv_nsec == UTIME_OMIT) { + err = sshfs_getattr(path, &cur, fi); + if (err) + return err; + } + asec = utimens_resolve(&tv[0], cur.st_atime, now); + msec = utimens_resolve(&tv[1], cur.st_mtime, now); + buf_init(&buf, 0); if (sf == NULL) buf_add_path(&buf, path); diff --git a/test/test_sshfs.py b/test/test_sshfs.py index c4cc64f..c5b2d06 100755 --- a/test/test_sshfs.py +++ b/test/test_sshfs.py @@ -162,6 +162,7 @@ def test_sshfs( # file timestamps. tst_utimens(mnt_dir, tol=1) tst_utimens_now(mnt_dir) + tst_utimens_omit(mnt_dir) tst_link(mnt_dir, cache_timeout) tst_truncate_path(mnt_dir) @@ -698,6 +699,23 @@ def tst_utimens_now(mnt_dir): assert fstat.st_mtime != 0 +def tst_utimens_omit(mnt_dir): + # Updating only the access time (UTIME_OMIT for mtime) must leave the + # modification time alone instead of setting it to "now". + fullname = pjoin(mnt_dir, name_generator()) + + fd = os.open(fullname, os.O_CREAT | os.O_RDWR) + os.close(fd) + old = int(os.lstat(fullname).st_mtime) - 1000 + os.utime(fullname, (old, old)) + + subprocess.check_call(["touch", "-a", fullname]) + + fstat = os.lstat(fullname) + assert fstat.st_mtime == old + assert fstat.st_atime > old + + def tst_passthrough(src_dir, mnt_dir, cache_timeout): name = name_generator() src_name = pjoin(src_dir, name)