From ee4e6a1ef9f51beaaef5e189302afe1db68ff6c1 Mon Sep 17 00:00:00 2001 From: Bert Belder Date: Mon, 27 Apr 2020 20:09:56 +0200 Subject: [PATCH] Rename FileInfo time fields and represent them as Date objects (#4932) This patch also increases the resolution of reported file times to sub-millisecond precision. --- cli/js/lib.deno.ns.d.ts | 10 ++-- cli/js/ops/fs/stat.ts | 18 ++++---- cli/js/tests/stat_test.ts | 81 +++++++++++++++++++++------------ cli/js/tests/utime_test.ts | 68 +++++++++++---------------- cli/ops/fs.rs | 31 ++++++++----- std/archive/tar.ts | 7 ++- std/fs/copy.ts | 36 +++++++-------- std/fs/copy_test.ts | 24 +++++----- std/node/_fs/_fs_dirent_test.ts | 6 +-- 9 files changed, 147 insertions(+), 134 deletions(-) diff --git a/cli/js/lib.deno.ns.d.ts b/cli/js/lib.deno.ns.d.ts index 9813b11fbd3273..7a04776822e75a 100644 --- a/cli/js/lib.deno.ns.d.ts +++ b/cli/js/lib.deno.ns.d.ts @@ -1316,15 +1316,15 @@ declare namespace Deno { /** The last modification time of the file. This corresponds to the `mtime` * field from `stat` on Linux/Mac OS and `ftLastWriteTime` on Windows. This * may not be available on all platforms. */ - modified: number | null; + mtime: Date | null; /** The last access time of the file. This corresponds to the `atime` * field from `stat` on Unix and `ftLastAccessTime` on Windows. This may not * be available on all platforms. */ - accessed: number | null; + atime: Date | null; /** The creation time of the file. This corresponds to the `birthtime` - * field from `stat` on Mac/BSD and `ftCreationTime` on Windows. This may not - * be available on all platforms. */ - created: number | null; + * field from `stat` on Mac/BSD and `ftCreationTime` on Windows. This may + * not be available on all platforms. */ + birthtime: Date | null; /** ID of the device containing the file. * * _Linux/Mac OS only._ */ diff --git a/cli/js/ops/fs/stat.ts b/cli/js/ops/fs/stat.ts index 6b7e5ea93098b8..7f28614d2419f6 100644 --- a/cli/js/ops/fs/stat.ts +++ b/cli/js/ops/fs/stat.ts @@ -4,9 +4,9 @@ import { build } from "../../build.ts"; export interface FileInfo { size: number; - modified: number | null; - accessed: number | null; - created: number | null; + mtime: Date | null; + atime: Date | null; + birthtime: Date | null; dev: number | null; ino: number | null; mode: number | null; @@ -26,9 +26,9 @@ export interface StatResponse { isDirectory: boolean; isSymlink: boolean; size: number; - modified: number; - accessed: number; - created: number; + mtime: number | null; + atime: number | null; + birthtime: number | null; // Null for stat(), but exists for readdir(). name: string | null; // Unix only members @@ -51,9 +51,9 @@ export function parseFileInfo(response: StatResponse): FileInfo { isDirectory: response.isDirectory, isSymlink: response.isSymlink, size: response.size, - modified: response.modified ? response.modified : null, - accessed: response.accessed ? response.accessed : null, - created: response.created ? response.created : null, + mtime: response.mtime != null ? new Date(response.mtime) : null, + atime: response.atime != null ? new Date(response.atime) : null, + birthtime: response.birthtime != null ? new Date(response.birthtime) : null, // Only non-null if on Unix dev: isUnix ? response.dev : null, ino: isUnix ? response.ino : null, diff --git a/cli/js/tests/stat_test.ts b/cli/js/tests/stat_test.ts index e4f4ae61eb604b..70ec5dc2e7caef 100644 --- a/cli/js/tests/stat_test.ts +++ b/cli/js/tests/stat_test.ts @@ -1,21 +1,31 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. import { unitTest, assert, assertEquals } from "./test_util.ts"; -// TODO Add tests for modified, accessed, and created fields once there is a way -// to create temp files. -unitTest({ perms: { read: true } }, function statSyncSuccess(): void { - const packageInfo = Deno.statSync("README.md"); - assert(packageInfo.isFile); - assert(!packageInfo.isSymlink); - - const modulesInfo = Deno.statSync("cli/tests/symlink_to_subdir"); - assert(modulesInfo.isDirectory); - assert(!modulesInfo.isSymlink); - - const testsInfo = Deno.statSync("cli/tests"); - assert(testsInfo.isDirectory); - assert(!testsInfo.isSymlink); -}); +unitTest( + { perms: { read: true, write: true } }, + function statSyncSuccess(): void { + const packageInfo = Deno.statSync("README.md"); + assert(packageInfo.isFile); + assert(!packageInfo.isSymlink); + + const modulesInfo = Deno.statSync("cli/tests/symlink_to_subdir"); + assert(modulesInfo.isDirectory); + assert(!modulesInfo.isSymlink); + + const testsInfo = Deno.statSync("cli/tests"); + assert(testsInfo.isDirectory); + assert(!testsInfo.isSymlink); + + const tempFile = Deno.makeTempFileSync(); + const tempInfo = Deno.statSync(tempFile); + const now = Date.now(); + assert(tempInfo.atime !== null && now - tempInfo.atime.valueOf() < 1000); + assert(tempInfo.mtime !== null && now - tempInfo.mtime.valueOf() < 1000); + assert( + tempInfo.birthtime === null || now - tempInfo.birthtime.valueOf() < 1000 + ); + } +); unitTest({ perms: { read: false } }, function statSyncPerm(): void { let caughtError = false; @@ -83,21 +93,32 @@ unitTest({ perms: { read: true } }, function lstatSyncNotFound(): void { assertEquals(badInfo, undefined); }); -unitTest({ perms: { read: true } }, async function statSuccess(): Promise< - void -> { - const packageInfo = await Deno.stat("README.md"); - assert(packageInfo.isFile); - assert(!packageInfo.isSymlink); - - const modulesInfo = await Deno.stat("cli/tests/symlink_to_subdir"); - assert(modulesInfo.isDirectory); - assert(!modulesInfo.isSymlink); - - const testsInfo = await Deno.stat("cli/tests"); - assert(testsInfo.isDirectory); - assert(!testsInfo.isSymlink); -}); +unitTest( + { perms: { read: true, write: true } }, + async function statSuccess(): Promise { + const packageInfo = await Deno.stat("README.md"); + assert(packageInfo.isFile); + assert(!packageInfo.isSymlink); + + const modulesInfo = await Deno.stat("cli/tests/symlink_to_subdir"); + assert(modulesInfo.isDirectory); + assert(!modulesInfo.isSymlink); + + const testsInfo = await Deno.stat("cli/tests"); + assert(testsInfo.isDirectory); + assert(!testsInfo.isSymlink); + + const tempFile = await Deno.makeTempFile(); + const tempInfo = await Deno.stat(tempFile); + const now = Date.now(); + assert(tempInfo.atime !== null && now - tempInfo.atime.valueOf() < 1000); + assert(tempInfo.mtime !== null && now - tempInfo.mtime.valueOf() < 1000); + + assert( + tempInfo.birthtime === null || now - tempInfo.birthtime.valueOf() < 1000 + ); + } +); unitTest({ perms: { read: false } }, async function statPerm(): Promise { let caughtError = false; diff --git a/cli/js/tests/utime_test.ts b/cli/js/tests/utime_test.ts index a81600e540429a..63727ae6235353 100644 --- a/cli/js/tests/utime_test.ts +++ b/cli/js/tests/utime_test.ts @@ -4,9 +4,9 @@ import { unitTest, assert } from "./test_util.ts"; // Allow 10 second difference. // Note this might not be enough for FAT (but we are not testing on such fs). // eslint-disable-next-line @typescript-eslint/no-explicit-any -function assertFuzzyTimestampEquals(t1: any, t2: number): void { - assert(typeof t1 === "number"); - assert(Math.abs(t1 - t2) < 10); +function assertFuzzyTimestampEquals(t1: Date | null, t2: Date): void { + assert(t1 instanceof Date); + assert(Math.abs(t1.valueOf() - t2.valueOf()) < 10_000); } unitTest( @@ -23,8 +23,8 @@ unitTest( Deno.utimeSync(filename, atime, mtime); const fileInfo = Deno.statSync(filename); - assertFuzzyTimestampEquals(fileInfo.accessed, atime); - assertFuzzyTimestampEquals(fileInfo.modified, mtime); + assertFuzzyTimestampEquals(fileInfo.atime, new Date(atime * 1000)); + assertFuzzyTimestampEquals(fileInfo.mtime, new Date(mtime * 1000)); } ); @@ -38,8 +38,8 @@ unitTest( Deno.utimeSync(testDir, atime, mtime); const dirInfo = Deno.statSync(testDir); - assertFuzzyTimestampEquals(dirInfo.accessed, atime); - assertFuzzyTimestampEquals(dirInfo.modified, mtime); + assertFuzzyTimestampEquals(dirInfo.atime, new Date(atime * 1000)); + assertFuzzyTimestampEquals(dirInfo.mtime, new Date(mtime * 1000)); } ); @@ -48,13 +48,13 @@ unitTest( function utimeSyncDateSuccess(): void { const testDir = Deno.makeTempDirSync(); - const atime = 1000; - const mtime = 50000; - Deno.utimeSync(testDir, new Date(atime * 1000), new Date(mtime * 1000)); + const atime = new Date(1000_000); + const mtime = new Date(50000_000); + Deno.utimeSync(testDir, atime, mtime); const dirInfo = Deno.statSync(testDir); - assertFuzzyTimestampEquals(dirInfo.accessed, atime); - assertFuzzyTimestampEquals(dirInfo.modified, mtime); + assertFuzzyTimestampEquals(dirInfo.atime, atime); + assertFuzzyTimestampEquals(dirInfo.mtime, mtime); } ); @@ -71,15 +71,8 @@ unitTest( Deno.utimeSync(filename, atime, mtime); const fileInfo = Deno.statSync(filename); - // atime and mtime must be scaled by a factor of 1000 to be recorded in seconds - assertFuzzyTimestampEquals( - fileInfo.accessed, - Math.trunc(atime.valueOf() / 1000) - ); - assertFuzzyTimestampEquals( - fileInfo.modified, - Math.trunc(mtime.valueOf() / 1000) - ); + assertFuzzyTimestampEquals(fileInfo.atime, atime); + assertFuzzyTimestampEquals(fileInfo.mtime, mtime); } ); @@ -95,8 +88,8 @@ unitTest( Deno.utimeSync(testDir, atime, mtime); const dirInfo = Deno.statSync(testDir); - assertFuzzyTimestampEquals(dirInfo.accessed, atime); - assertFuzzyTimestampEquals(dirInfo.modified, mtime); + assertFuzzyTimestampEquals(dirInfo.atime, new Date(atime * 1000)); + assertFuzzyTimestampEquals(dirInfo.mtime, new Date(mtime * 1000)); } ); @@ -148,8 +141,8 @@ unitTest( await Deno.utime(filename, atime, mtime); const fileInfo = Deno.statSync(filename); - assertFuzzyTimestampEquals(fileInfo.accessed, atime); - assertFuzzyTimestampEquals(fileInfo.modified, mtime); + assertFuzzyTimestampEquals(fileInfo.atime, new Date(atime * 1000)); + assertFuzzyTimestampEquals(fileInfo.mtime, new Date(mtime * 1000)); } ); @@ -163,8 +156,8 @@ unitTest( await Deno.utime(testDir, atime, mtime); const dirInfo = Deno.statSync(testDir); - assertFuzzyTimestampEquals(dirInfo.accessed, atime); - assertFuzzyTimestampEquals(dirInfo.modified, mtime); + assertFuzzyTimestampEquals(dirInfo.atime, new Date(atime * 1000)); + assertFuzzyTimestampEquals(dirInfo.mtime, new Date(mtime * 1000)); } ); @@ -173,13 +166,13 @@ unitTest( async function utimeDateSuccess(): Promise { const testDir = Deno.makeTempDirSync(); - const atime = 1000; - const mtime = 50000; - await Deno.utime(testDir, new Date(atime * 1000), new Date(mtime * 1000)); + const atime = new Date(100_000); + const mtime = new Date(5000_000); + await Deno.utime(testDir, atime, mtime); const dirInfo = Deno.statSync(testDir); - assertFuzzyTimestampEquals(dirInfo.accessed, atime); - assertFuzzyTimestampEquals(dirInfo.modified, mtime); + assertFuzzyTimestampEquals(dirInfo.atime, atime); + assertFuzzyTimestampEquals(dirInfo.mtime, mtime); } ); @@ -197,15 +190,8 @@ unitTest( await Deno.utime(filename, atime, mtime); const fileInfo = Deno.statSync(filename); - // The dates must be scaled by a factored of 1000 to make them seconds - assertFuzzyTimestampEquals( - fileInfo.accessed, - Math.trunc(atime.valueOf() / 1000) - ); - assertFuzzyTimestampEquals( - fileInfo.modified, - Math.trunc(mtime.valueOf() / 1000) - ); + assertFuzzyTimestampEquals(fileInfo.atime, atime); + assertFuzzyTimestampEquals(fileInfo.mtime, mtime); } ); diff --git a/cli/ops/fs.rs b/cli/ops/fs.rs index 5ab53481092df2..84ac4ed51371d1 100644 --- a/cli/ops/fs.rs +++ b/cli/ops/fs.rs @@ -12,7 +12,9 @@ use deno_core::ZeroCopyBuf; use futures::future::FutureExt; use std::convert::From; use std::env::{current_dir, set_current_dir, temp_dir}; +use std::io; use std::path::{Path, PathBuf}; +use std::time::SystemTime; use std::time::UNIX_EPOCH; use rand::{thread_rng, Rng}; @@ -441,14 +443,19 @@ fn op_copy_file( }) } -macro_rules! to_seconds { - ($time:expr) => {{ - // Unwrap is safe here as if the file is before the unix epoch - // something is very wrong. - $time - .and_then(|t| Ok(t.duration_since(UNIX_EPOCH).unwrap().as_secs())) - .unwrap_or(0) - }}; +fn to_msec(maybe_time: Result) -> serde_json::Value { + match maybe_time { + Ok(time) => { + let msec = time + .duration_since(UNIX_EPOCH) + .map(|t| t.as_secs_f64() * 1000f64) + .unwrap_or_else(|err| err.duration().as_secs_f64() * -1000f64); + serde_json::Number::from_f64(msec) + .map(serde_json::Value::Number) + .unwrap_or(serde_json::Value::Null) + } + Err(_) => serde_json::Value::Null, + } } #[inline(always)] @@ -477,10 +484,10 @@ fn get_stat_json( "isDirectory": metadata.is_dir(), "isSymlink": metadata.file_type().is_symlink(), "size": metadata.len(), - // In seconds. Available on both Unix or Windows. - "modified":to_seconds!(metadata.modified()), - "accessed":to_seconds!(metadata.accessed()), - "created":to_seconds!(metadata.created()), + // In milliseconds, like JavaScript. Available on both Unix or Windows. + "mtime": to_msec(metadata.modified()), + "atime": to_msec(metadata.accessed()), + "birthtime": to_msec(metadata.created()), // Following are only valid under Unix. "dev": usm!(dev), "ino": usm!(ino), diff --git a/std/archive/tar.ts b/std/archive/tar.ts index 699b982a926e9d..28c4bbd9a907d6 100644 --- a/std/archive/tar.ts +++ b/std/archive/tar.ts @@ -317,10 +317,9 @@ export class Tar { const mode = opts.fileMode || (info && info.mode) || parseInt("777", 8) & 0xfff, - mtime = - opts.mtime || - (info && info.modified) || - Math.floor(new Date().getTime() / 1000), + mtime = Math.floor( + opts.mtime ?? (info?.mtime ?? new Date()).valueOf() / 1000 + ), uid = opts.uid || 0, gid = opts.gid || 0; if (typeof opts.owner === "string" && opts.owner.length >= 32) { diff --git a/std/fs/copy.ts b/std/fs/copy.ts index 05ce4b12cdeab4..27b3c2a3cd3721 100644 --- a/std/fs/copy.ts +++ b/std/fs/copy.ts @@ -85,9 +85,9 @@ async function copyFile( await Deno.copyFile(src, dest); if (options.preserveTimestamps) { const statInfo = await Deno.stat(src); - assert(statInfo.accessed != null, `statInfo.accessed is unavailable`); - assert(statInfo.modified != null, `statInfo.modified is unavailable`); - await Deno.utime(dest, statInfo.accessed, statInfo.modified); + assert(statInfo.atime instanceof Date, `statInfo.atime is unavailable`); + assert(statInfo.mtime instanceof Date, `statInfo.mtime is unavailable`); + await Deno.utime(dest, statInfo.atime, statInfo.mtime); } } /* copy file to dest synchronously */ @@ -96,9 +96,9 @@ function copyFileSync(src: string, dest: string, options: CopyOptions): void { Deno.copyFileSync(src, dest); if (options.preserveTimestamps) { const statInfo = Deno.statSync(src); - assert(statInfo.accessed != null, `statInfo.accessed is unavailable`); - assert(statInfo.modified != null, `statInfo.modified is unavailable`); - Deno.utimeSync(dest, statInfo.accessed, statInfo.modified); + assert(statInfo.atime instanceof Date, `statInfo.atime is unavailable`); + assert(statInfo.mtime instanceof Date, `statInfo.mtime is unavailable`); + Deno.utimeSync(dest, statInfo.atime, statInfo.mtime); } } @@ -114,9 +114,9 @@ async function copySymLink( await Deno.symlink(originSrcFilePath, dest, type); if (options.preserveTimestamps) { const statInfo = await Deno.lstat(src); - assert(statInfo.accessed != null, `statInfo.accessed is unavailable`); - assert(statInfo.modified != null, `statInfo.modified is unavailable`); - await Deno.utime(dest, statInfo.accessed, statInfo.modified); + assert(statInfo.atime instanceof Date, `statInfo.atime is unavailable`); + assert(statInfo.mtime instanceof Date, `statInfo.mtime is unavailable`); + await Deno.utime(dest, statInfo.atime, statInfo.mtime); } } @@ -132,9 +132,9 @@ function copySymlinkSync( Deno.symlinkSync(originSrcFilePath, dest, type); if (options.preserveTimestamps) { const statInfo = Deno.lstatSync(src); - assert(statInfo.accessed != null, `statInfo.accessed is unavailable`); - assert(statInfo.modified != null, `statInfo.modified is unavailable`); - Deno.utimeSync(dest, statInfo.accessed, statInfo.modified); + assert(statInfo.atime instanceof Date, `statInfo.atime is unavailable`); + assert(statInfo.mtime instanceof Date, `statInfo.mtime is unavailable`); + Deno.utimeSync(dest, statInfo.atime, statInfo.mtime); } } @@ -152,9 +152,9 @@ async function copyDir( if (options.preserveTimestamps) { const srcStatInfo = await Deno.stat(src); - assert(srcStatInfo.accessed != null, `statInfo.accessed is unavailable`); - assert(srcStatInfo.modified != null, `statInfo.modified is unavailable`); - await Deno.utime(dest, srcStatInfo.accessed, srcStatInfo.modified); + assert(srcStatInfo.atime instanceof Date, `statInfo.atime is unavailable`); + assert(srcStatInfo.mtime instanceof Date, `statInfo.mtime is unavailable`); + await Deno.utime(dest, srcStatInfo.atime, srcStatInfo.mtime); } for await (const file of Deno.readdir(src)) { @@ -180,9 +180,9 @@ function copyDirSync(src: string, dest: string, options: CopyOptions): void { if (options.preserveTimestamps) { const srcStatInfo = Deno.statSync(src); - assert(srcStatInfo.accessed != null, `statInfo.accessed is unavailable`); - assert(srcStatInfo.modified != null, `statInfo.modified is unavailable`); - Deno.utimeSync(dest, srcStatInfo.accessed, srcStatInfo.modified); + assert(srcStatInfo.atime instanceof Date, `statInfo.atime is unavailable`); + assert(srcStatInfo.mtime instanceof Date, `statInfo.mtime is unavailable`); + Deno.utimeSync(dest, srcStatInfo.atime, srcStatInfo.mtime); } for (const file of Deno.readdirSync(src)) { diff --git a/std/fs/copy_test.ts b/std/fs/copy_test.ts index 65e36a5fc829d1..5323ac9e4fbb60 100644 --- a/std/fs/copy_test.ts +++ b/std/fs/copy_test.ts @@ -143,8 +143,8 @@ testCopy( const srcStatInfo = await Deno.stat(srcFile); - assert(typeof srcStatInfo.accessed === "number"); - assert(typeof srcStatInfo.modified === "number"); + assert(srcStatInfo.atime instanceof Date); + assert(srcStatInfo.mtime instanceof Date); // Copy with overwrite and preserve timestamps options. await copy(srcFile, destFile, { @@ -154,10 +154,10 @@ testCopy( const destStatInfo = await Deno.stat(destFile); - assert(typeof destStatInfo.accessed === "number"); - assert(typeof destStatInfo.modified === "number"); - assertEquals(destStatInfo.accessed, srcStatInfo.accessed); - assertEquals(destStatInfo.modified, srcStatInfo.modified); + assert(destStatInfo.atime instanceof Date); + assert(destStatInfo.mtime instanceof Date); + assertEquals(destStatInfo.atime, srcStatInfo.atime); + assertEquals(destStatInfo.mtime, srcStatInfo.mtime); } ); @@ -327,8 +327,8 @@ testCopySync( const srcStatInfo = Deno.statSync(srcFile); - assert(typeof srcStatInfo.accessed === "number"); - assert(typeof srcStatInfo.modified === "number"); + assert(srcStatInfo.atime instanceof Date); + assert(srcStatInfo.mtime instanceof Date); // Copy with overwrite and preserve timestamps options. copySync(srcFile, destFile, { @@ -338,12 +338,12 @@ testCopySync( const destStatInfo = Deno.statSync(destFile); - assert(typeof destStatInfo.accessed === "number"); - assert(typeof destStatInfo.modified === "number"); + assert(destStatInfo.atime instanceof Date); + assert(destStatInfo.mtime instanceof Date); // TODO: Activate test when https://github.com/denoland/deno/issues/2411 // is fixed - // assertEquals(destStatInfo.accessed, srcStatInfo.accessed); - // assertEquals(destStatInfo.modified, srcStatInfo.modified); + // assertEquals(destStatInfo.atime, srcStatInfo.atime); + // assertEquals(destStatInfo.mtime, srcStatInfo.mtime); } ); diff --git a/std/node/_fs/_fs_dirent_test.ts b/std/node/_fs/_fs_dirent_test.ts index 548fa6b8af54ba..5288cb3db17843 100644 --- a/std/node/_fs/_fs_dirent_test.ts +++ b/std/node/_fs/_fs_dirent_test.ts @@ -7,9 +7,9 @@ class DirEntryMock implements Deno.DirEntry { isDirectory = false; isSymlink = false; size = -1; - modified = -1; - accessed = -1; - created = -1; + mtime = new Date(-1); + atime = new Date(-1); + birthtime = new Date(-1); name = ""; dev = -1; ino = -1;