mirror of
https://github.com/denoland/deno.git
synced 2024-11-21 15:04:11 -05:00
feat(ext/fs): add ctime to Deno.stats and use it in node compat layer (#24801)
This PR fixes #24453, by introducing a ctime (using ctime for UNIX and ChangeTime for Windows) to Deno.stats. Co-authored-by: Yoshiya Hinosawa <stibium121@gmail.com>
This commit is contained in:
parent
43812ee8ff
commit
7becd83a38
10 changed files with 88 additions and 15 deletions
|
@ -350,6 +350,7 @@ impl<'a> VfsEntryRef<'a> {
|
|||
atime: None,
|
||||
birthtime: None,
|
||||
mtime: None,
|
||||
ctime: None,
|
||||
blksize: 0,
|
||||
size: 0,
|
||||
dev: 0,
|
||||
|
@ -372,6 +373,7 @@ impl<'a> VfsEntryRef<'a> {
|
|||
atime: None,
|
||||
birthtime: None,
|
||||
mtime: None,
|
||||
ctime: None,
|
||||
blksize: 0,
|
||||
size: file.len,
|
||||
dev: 0,
|
||||
|
@ -394,6 +396,7 @@ impl<'a> VfsEntryRef<'a> {
|
|||
atime: None,
|
||||
birthtime: None,
|
||||
mtime: None,
|
||||
ctime: None,
|
||||
blksize: 0,
|
||||
size: 0,
|
||||
dev: 0,
|
||||
|
|
7
cli/tsc/dts/lib.deno.ns.d.ts
vendored
7
cli/tsc/dts/lib.deno.ns.d.ts
vendored
|
@ -2971,6 +2971,10 @@ declare namespace Deno {
|
|||
* field from `stat` on Mac/BSD and `ftCreationTime` on Windows. This may
|
||||
* not be available on all platforms. */
|
||||
birthtime: Date | null;
|
||||
/** The last change time of the file. This corresponds to the `ctime`
|
||||
* field from `stat` on Mac/BSD and `ChangeTime` on Windows. This may
|
||||
* not be available on all platforms. */
|
||||
ctime: Date | null;
|
||||
/** ID of the device containing the file. */
|
||||
dev: number;
|
||||
/** Inode number.
|
||||
|
@ -2979,8 +2983,7 @@ declare namespace Deno {
|
|||
ino: number | null;
|
||||
/** The underlying raw `st_mode` bits that contain the standard Unix
|
||||
* permissions for this file/directory.
|
||||
*
|
||||
* _Linux/Mac OS only._ */
|
||||
*/
|
||||
mode: number | null;
|
||||
/** Number of hard links pointing to this file.
|
||||
*
|
||||
|
|
|
@ -346,9 +346,10 @@ const { 0: statStruct, 1: statBuf } = createByteStruct({
|
|||
mtime: "date",
|
||||
atime: "date",
|
||||
birthtime: "date",
|
||||
ctime: "date",
|
||||
dev: "u64",
|
||||
ino: "?u64",
|
||||
mode: "?u64",
|
||||
mode: "u64",
|
||||
nlink: "?u64",
|
||||
uid: "?u64",
|
||||
gid: "?u64",
|
||||
|
@ -377,9 +378,10 @@ function parseFileInfo(response) {
|
|||
birthtime: response.birthtimeSet === true
|
||||
? new Date(response.birthtime)
|
||||
: null,
|
||||
ctime: response.ctimeSet === true ? new Date(response.ctime) : null,
|
||||
dev: response.dev,
|
||||
mode: response.mode,
|
||||
ino: unix ? response.ino : null,
|
||||
mode: unix ? response.mode : null,
|
||||
nlink: unix ? response.nlink : null,
|
||||
uid: unix ? response.uid : null,
|
||||
gid: unix ? response.gid : null,
|
||||
|
|
|
@ -229,6 +229,7 @@ impl FileSystem for InMemoryFs {
|
|||
mtime: None,
|
||||
atime: None,
|
||||
birthtime: None,
|
||||
ctime: None,
|
||||
dev: 0,
|
||||
ino: 0,
|
||||
mode: 0,
|
||||
|
@ -251,6 +252,7 @@ impl FileSystem for InMemoryFs {
|
|||
mtime: None,
|
||||
atime: None,
|
||||
birthtime: None,
|
||||
ctime: None,
|
||||
dev: 0,
|
||||
ino: 0,
|
||||
mode: 0,
|
||||
|
|
|
@ -1795,6 +1795,8 @@ create_struct_writer! {
|
|||
atime: u64,
|
||||
birthtime_set: bool,
|
||||
birthtime: u64,
|
||||
ctime_set: bool,
|
||||
ctime: u64,
|
||||
// Following are only valid under Unix.
|
||||
dev: u64,
|
||||
ino: u64,
|
||||
|
@ -1826,6 +1828,8 @@ impl From<FsStat> for SerializableStat {
|
|||
atime: stat.atime.unwrap_or(0),
|
||||
birthtime_set: stat.birthtime.is_some(),
|
||||
birthtime: stat.birthtime.unwrap_or(0),
|
||||
ctime_set: stat.ctime.is_some(),
|
||||
ctime: stat.ctime.unwrap_or(0),
|
||||
|
||||
dev: stat.dev,
|
||||
ino: stat.ino,
|
||||
|
|
|
@ -821,24 +821,46 @@ fn stat_extra(
|
|||
Ok(info.dwVolumeSerialNumber as u64)
|
||||
}
|
||||
|
||||
const WINDOWS_TICK: i64 = 10_000; // 100-nanosecond intervals in a millisecond
|
||||
const SEC_TO_UNIX_EPOCH: i64 = 11_644_473_600; // Seconds between Windows epoch and Unix epoch
|
||||
|
||||
fn windows_time_to_unix_time_msec(windows_time: &i64) -> i64 {
|
||||
let milliseconds_since_windows_epoch = windows_time / WINDOWS_TICK;
|
||||
milliseconds_since_windows_epoch - SEC_TO_UNIX_EPOCH * 1000
|
||||
}
|
||||
|
||||
use windows_sys::Wdk::Storage::FileSystem::FILE_ALL_INFORMATION;
|
||||
use windows_sys::Win32::Foundation::NTSTATUS;
|
||||
|
||||
unsafe fn query_file_information(
|
||||
handle: winapi::shared::ntdef::HANDLE,
|
||||
) -> std::io::Result<FILE_ALL_INFORMATION> {
|
||||
) -> Result<FILE_ALL_INFORMATION, NTSTATUS> {
|
||||
use windows_sys::Wdk::Storage::FileSystem::NtQueryInformationFile;
|
||||
use windows_sys::Win32::Foundation::RtlNtStatusToDosError;
|
||||
use windows_sys::Win32::Foundation::ERROR_MORE_DATA;
|
||||
use windows_sys::Win32::System::IO::IO_STATUS_BLOCK;
|
||||
|
||||
let mut info = std::mem::MaybeUninit::<FILE_ALL_INFORMATION>::zeroed();
|
||||
let mut io_status_block =
|
||||
std::mem::MaybeUninit::<IO_STATUS_BLOCK>::zeroed();
|
||||
let status = NtQueryInformationFile(
|
||||
handle as _,
|
||||
std::ptr::null_mut(),
|
||||
io_status_block.as_mut_ptr(),
|
||||
info.as_mut_ptr() as *mut _,
|
||||
std::mem::size_of::<FILE_ALL_INFORMATION>() as _,
|
||||
18, /* FileAllInformation */
|
||||
);
|
||||
|
||||
if status < 0 {
|
||||
return Err(std::io::Error::last_os_error());
|
||||
let converted_status = RtlNtStatusToDosError(status);
|
||||
|
||||
// If error more data is returned, then it means that the buffer is too small to get full filename information
|
||||
// to have that we should retry. However, since we only use BasicInformation and StandardInformation, it is fine to ignore it
|
||||
// since struct is populated with other data anyway.
|
||||
// https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-ntqueryinformationfile#remarksdd
|
||||
if converted_status != ERROR_MORE_DATA {
|
||||
return Err(converted_status as NTSTATUS);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(info.assume_init())
|
||||
|
@ -862,10 +884,13 @@ fn stat_extra(
|
|||
}
|
||||
|
||||
let result = get_dev(file_handle);
|
||||
CloseHandle(file_handle);
|
||||
fsstat.dev = result?;
|
||||
|
||||
if let Ok(file_info) = query_file_information(file_handle) {
|
||||
fsstat.ctime = Some(windows_time_to_unix_time_msec(
|
||||
&file_info.BasicInformation.ChangeTime,
|
||||
) as u64);
|
||||
|
||||
if file_info.BasicInformation.FileAttributes
|
||||
& winapi::um::winnt::FILE_ATTRIBUTE_REPARSE_POINT
|
||||
!= 0
|
||||
|
@ -898,6 +923,7 @@ fn stat_extra(
|
|||
}
|
||||
}
|
||||
|
||||
CloseHandle(file_handle);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
12
ext/io/fs.rs
12
ext/io/fs.rs
|
@ -94,6 +94,7 @@ pub struct FsStat {
|
|||
pub mtime: Option<u64>,
|
||||
pub atime: Option<u64>,
|
||||
pub birthtime: Option<u64>,
|
||||
pub ctime: Option<u64>,
|
||||
|
||||
pub dev: u64,
|
||||
pub ino: u64,
|
||||
|
@ -153,6 +154,16 @@ impl FsStat {
|
|||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn get_ctime(ctime_or_0: i64) -> Option<u64> {
|
||||
if ctime_or_0 > 0 {
|
||||
// ctime return seconds since epoch, but we need milliseconds
|
||||
return Some(ctime_or_0 as u64 * 1000);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
Self {
|
||||
is_file: metadata.is_file(),
|
||||
is_directory: metadata.is_dir(),
|
||||
|
@ -162,6 +173,7 @@ impl FsStat {
|
|||
mtime: to_msec(metadata.modified()),
|
||||
atime: to_msec(metadata.accessed()),
|
||||
birthtime: to_msec(metadata.created()),
|
||||
ctime: get_ctime(unix_or_zero!(ctime)),
|
||||
|
||||
dev: unix_or_zero!(dev),
|
||||
ino: unix_or_zero!(ino),
|
||||
|
|
|
@ -290,8 +290,8 @@ export function convertFileInfoToStats(origin: Deno.FileInfo): Stats {
|
|||
isFIFO: () => false,
|
||||
isCharacterDevice: () => false,
|
||||
isSocket: () => false,
|
||||
ctime: origin.mtime,
|
||||
ctimeMs: origin.mtime?.getTime() || null,
|
||||
ctime: origin.ctime,
|
||||
ctimeMs: origin.ctime?.getTime() || null,
|
||||
});
|
||||
|
||||
return stats;
|
||||
|
@ -336,9 +336,9 @@ export function convertFileInfoToBigIntStats(
|
|||
isFIFO: () => false,
|
||||
isCharacterDevice: () => false,
|
||||
isSocket: () => false,
|
||||
ctime: origin.mtime,
|
||||
ctimeMs: origin.mtime ? BigInt(origin.mtime.getTime()) : null,
|
||||
ctimeNs: origin.mtime ? BigInt(origin.mtime.getTime()) * 1000000n : null,
|
||||
ctime: origin.ctime,
|
||||
ctimeMs: origin.ctime ? BigInt(origin.ctime.getTime()) : null,
|
||||
ctimeNs: origin.ctime ? BigInt(origin.ctime.getTime()) * 1000000n : null,
|
||||
});
|
||||
return stats;
|
||||
}
|
||||
|
|
|
@ -31,6 +31,13 @@ Deno.test(
|
|||
assert(
|
||||
tempInfo.birthtime === null || now - tempInfo.birthtime.valueOf() < 1000,
|
||||
);
|
||||
assert(tempInfo.ctime !== null && now - tempInfo.ctime.valueOf() < 1000);
|
||||
const mode = tempInfo.mode! & 0o777;
|
||||
if (Deno.build.os === "windows") {
|
||||
assertEquals(mode, 0o666);
|
||||
} else {
|
||||
assertEquals(mode, 0o600);
|
||||
}
|
||||
|
||||
const readmeInfoByUrl = Deno.statSync(pathToAbsoluteFileUrl("README.md"));
|
||||
assert(readmeInfoByUrl.isFile);
|
||||
|
@ -65,6 +72,10 @@ Deno.test(
|
|||
tempInfoByUrl.birthtime === null ||
|
||||
now - tempInfoByUrl.birthtime.valueOf() < 1000,
|
||||
);
|
||||
assert(
|
||||
tempInfoByUrl.ctime !== null &&
|
||||
now - tempInfoByUrl.ctime.valueOf() < 1000,
|
||||
);
|
||||
|
||||
Deno.removeSync(tempFile, { recursive: true });
|
||||
Deno.removeSync(tempFileForUrl, { recursive: true });
|
||||
|
@ -171,6 +182,7 @@ Deno.test(
|
|||
assert(
|
||||
tempInfo.birthtime === null || now - tempInfo.birthtime.valueOf() < 1000,
|
||||
);
|
||||
assert(tempInfo.ctime !== null && now - tempInfo.ctime.valueOf() < 1000);
|
||||
|
||||
const tempFileForUrl = await Deno.makeTempFile();
|
||||
const tempInfoByUrl = await Deno.stat(
|
||||
|
@ -191,7 +203,10 @@ Deno.test(
|
|||
tempInfoByUrl.birthtime === null ||
|
||||
now - tempInfoByUrl.birthtime.valueOf() < 1000,
|
||||
);
|
||||
|
||||
assert(
|
||||
tempInfoByUrl.ctime !== null &&
|
||||
now - tempInfoByUrl.ctime.valueOf() < 1000,
|
||||
);
|
||||
Deno.removeSync(tempFile, { recursive: true });
|
||||
Deno.removeSync(tempFileForUrl, { recursive: true });
|
||||
},
|
||||
|
@ -271,7 +286,6 @@ Deno.test(
|
|||
const s = Deno.statSync(filename);
|
||||
assert(s.dev !== 0);
|
||||
assert(s.ino === null);
|
||||
assert(s.mode === null);
|
||||
assert(s.nlink === null);
|
||||
assert(s.uid === null);
|
||||
assert(s.gid === null);
|
||||
|
|
|
@ -18,9 +18,11 @@ export function assertStats(actual: Stats, expected: Deno.FileInfo) {
|
|||
assertEquals(actual.atime?.getTime(), expected.atime?.getTime());
|
||||
assertEquals(actual.mtime?.getTime(), expected.mtime?.getTime());
|
||||
assertEquals(actual.birthtime?.getTime(), expected.birthtime?.getTime());
|
||||
assertEquals(actual.ctime?.getTime(), expected.ctime?.getTime());
|
||||
assertEquals(actual.atimeMs ?? undefined, expected.atime?.getTime());
|
||||
assertEquals(actual.mtimeMs ?? undefined, expected.mtime?.getTime());
|
||||
assertEquals(actual.birthtimeMs ?? undefined, expected.birthtime?.getTime());
|
||||
assertEquals(actual.ctimeMs ?? undefined, expected.ctime?.getTime());
|
||||
assertEquals(actual.isFile(), expected.isFile);
|
||||
assertEquals(actual.isDirectory(), expected.isDirectory);
|
||||
assertEquals(actual.isSymbolicLink(), expected.isSymlink);
|
||||
|
@ -49,6 +51,7 @@ export function assertStatsBigInt(
|
|||
assertEquals(actual.atime?.getTime(), expected.atime?.getTime());
|
||||
assertEquals(actual.mtime?.getTime(), expected.mtime?.getTime());
|
||||
assertEquals(actual.birthtime?.getTime(), expected.birthtime?.getTime());
|
||||
assertEquals(actual.ctime?.getTime(), expected.ctime?.getTime());
|
||||
assertEquals(
|
||||
actual.atimeMs === null ? undefined : Number(actual.atimeMs),
|
||||
expected.atime?.getTime(),
|
||||
|
@ -61,6 +64,10 @@ export function assertStatsBigInt(
|
|||
actual.birthtimeMs === null ? undefined : Number(actual.birthtimeMs),
|
||||
expected.birthtime?.getTime(),
|
||||
);
|
||||
assertEquals(
|
||||
actual.ctimeMs === null ? undefined : Number(actual.ctimeMs),
|
||||
expected.ctime?.getTime(),
|
||||
);
|
||||
assertEquals(actual.atimeNs === null, actual.atime === null);
|
||||
assertEquals(actual.mtimeNs === null, actual.mtime === null);
|
||||
assertEquals(actual.birthtimeNs === null, actual.birthtime === null);
|
||||
|
|
Loading…
Reference in a new issue