mirror of
https://github.com/denoland/deno.git
synced 2024-11-24 15:19:26 -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,
|
atime: None,
|
||||||
birthtime: None,
|
birthtime: None,
|
||||||
mtime: None,
|
mtime: None,
|
||||||
|
ctime: None,
|
||||||
blksize: 0,
|
blksize: 0,
|
||||||
size: 0,
|
size: 0,
|
||||||
dev: 0,
|
dev: 0,
|
||||||
|
@ -372,6 +373,7 @@ impl<'a> VfsEntryRef<'a> {
|
||||||
atime: None,
|
atime: None,
|
||||||
birthtime: None,
|
birthtime: None,
|
||||||
mtime: None,
|
mtime: None,
|
||||||
|
ctime: None,
|
||||||
blksize: 0,
|
blksize: 0,
|
||||||
size: file.len,
|
size: file.len,
|
||||||
dev: 0,
|
dev: 0,
|
||||||
|
@ -394,6 +396,7 @@ impl<'a> VfsEntryRef<'a> {
|
||||||
atime: None,
|
atime: None,
|
||||||
birthtime: None,
|
birthtime: None,
|
||||||
mtime: None,
|
mtime: None,
|
||||||
|
ctime: None,
|
||||||
blksize: 0,
|
blksize: 0,
|
||||||
size: 0,
|
size: 0,
|
||||||
dev: 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
|
* field from `stat` on Mac/BSD and `ftCreationTime` on Windows. This may
|
||||||
* not be available on all platforms. */
|
* not be available on all platforms. */
|
||||||
birthtime: Date | null;
|
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. */
|
/** ID of the device containing the file. */
|
||||||
dev: number;
|
dev: number;
|
||||||
/** Inode number.
|
/** Inode number.
|
||||||
|
@ -2979,8 +2983,7 @@ declare namespace Deno {
|
||||||
ino: number | null;
|
ino: number | null;
|
||||||
/** The underlying raw `st_mode` bits that contain the standard Unix
|
/** The underlying raw `st_mode` bits that contain the standard Unix
|
||||||
* permissions for this file/directory.
|
* permissions for this file/directory.
|
||||||
*
|
*/
|
||||||
* _Linux/Mac OS only._ */
|
|
||||||
mode: number | null;
|
mode: number | null;
|
||||||
/** Number of hard links pointing to this file.
|
/** Number of hard links pointing to this file.
|
||||||
*
|
*
|
||||||
|
|
|
@ -346,9 +346,10 @@ const { 0: statStruct, 1: statBuf } = createByteStruct({
|
||||||
mtime: "date",
|
mtime: "date",
|
||||||
atime: "date",
|
atime: "date",
|
||||||
birthtime: "date",
|
birthtime: "date",
|
||||||
|
ctime: "date",
|
||||||
dev: "u64",
|
dev: "u64",
|
||||||
ino: "?u64",
|
ino: "?u64",
|
||||||
mode: "?u64",
|
mode: "u64",
|
||||||
nlink: "?u64",
|
nlink: "?u64",
|
||||||
uid: "?u64",
|
uid: "?u64",
|
||||||
gid: "?u64",
|
gid: "?u64",
|
||||||
|
@ -377,9 +378,10 @@ function parseFileInfo(response) {
|
||||||
birthtime: response.birthtimeSet === true
|
birthtime: response.birthtimeSet === true
|
||||||
? new Date(response.birthtime)
|
? new Date(response.birthtime)
|
||||||
: null,
|
: null,
|
||||||
|
ctime: response.ctimeSet === true ? new Date(response.ctime) : null,
|
||||||
dev: response.dev,
|
dev: response.dev,
|
||||||
|
mode: response.mode,
|
||||||
ino: unix ? response.ino : null,
|
ino: unix ? response.ino : null,
|
||||||
mode: unix ? response.mode : null,
|
|
||||||
nlink: unix ? response.nlink : null,
|
nlink: unix ? response.nlink : null,
|
||||||
uid: unix ? response.uid : null,
|
uid: unix ? response.uid : null,
|
||||||
gid: unix ? response.gid : null,
|
gid: unix ? response.gid : null,
|
||||||
|
|
|
@ -229,6 +229,7 @@ impl FileSystem for InMemoryFs {
|
||||||
mtime: None,
|
mtime: None,
|
||||||
atime: None,
|
atime: None,
|
||||||
birthtime: None,
|
birthtime: None,
|
||||||
|
ctime: None,
|
||||||
dev: 0,
|
dev: 0,
|
||||||
ino: 0,
|
ino: 0,
|
||||||
mode: 0,
|
mode: 0,
|
||||||
|
@ -251,6 +252,7 @@ impl FileSystem for InMemoryFs {
|
||||||
mtime: None,
|
mtime: None,
|
||||||
atime: None,
|
atime: None,
|
||||||
birthtime: None,
|
birthtime: None,
|
||||||
|
ctime: None,
|
||||||
dev: 0,
|
dev: 0,
|
||||||
ino: 0,
|
ino: 0,
|
||||||
mode: 0,
|
mode: 0,
|
||||||
|
|
|
@ -1795,6 +1795,8 @@ create_struct_writer! {
|
||||||
atime: u64,
|
atime: u64,
|
||||||
birthtime_set: bool,
|
birthtime_set: bool,
|
||||||
birthtime: u64,
|
birthtime: u64,
|
||||||
|
ctime_set: bool,
|
||||||
|
ctime: u64,
|
||||||
// Following are only valid under Unix.
|
// Following are only valid under Unix.
|
||||||
dev: u64,
|
dev: u64,
|
||||||
ino: u64,
|
ino: u64,
|
||||||
|
@ -1826,6 +1828,8 @@ impl From<FsStat> for SerializableStat {
|
||||||
atime: stat.atime.unwrap_or(0),
|
atime: stat.atime.unwrap_or(0),
|
||||||
birthtime_set: stat.birthtime.is_some(),
|
birthtime_set: stat.birthtime.is_some(),
|
||||||
birthtime: stat.birthtime.unwrap_or(0),
|
birthtime: stat.birthtime.unwrap_or(0),
|
||||||
|
ctime_set: stat.ctime.is_some(),
|
||||||
|
ctime: stat.ctime.unwrap_or(0),
|
||||||
|
|
||||||
dev: stat.dev,
|
dev: stat.dev,
|
||||||
ino: stat.ino,
|
ino: stat.ino,
|
||||||
|
|
|
@ -821,24 +821,46 @@ fn stat_extra(
|
||||||
Ok(info.dwVolumeSerialNumber as u64)
|
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::Wdk::Storage::FileSystem::FILE_ALL_INFORMATION;
|
||||||
|
use windows_sys::Win32::Foundation::NTSTATUS;
|
||||||
|
|
||||||
unsafe fn query_file_information(
|
unsafe fn query_file_information(
|
||||||
handle: winapi::shared::ntdef::HANDLE,
|
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::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 info = std::mem::MaybeUninit::<FILE_ALL_INFORMATION>::zeroed();
|
||||||
|
let mut io_status_block =
|
||||||
|
std::mem::MaybeUninit::<IO_STATUS_BLOCK>::zeroed();
|
||||||
let status = NtQueryInformationFile(
|
let status = NtQueryInformationFile(
|
||||||
handle as _,
|
handle as _,
|
||||||
std::ptr::null_mut(),
|
io_status_block.as_mut_ptr(),
|
||||||
info.as_mut_ptr() as *mut _,
|
info.as_mut_ptr() as *mut _,
|
||||||
std::mem::size_of::<FILE_ALL_INFORMATION>() as _,
|
std::mem::size_of::<FILE_ALL_INFORMATION>() as _,
|
||||||
18, /* FileAllInformation */
|
18, /* FileAllInformation */
|
||||||
);
|
);
|
||||||
|
|
||||||
if status < 0 {
|
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())
|
Ok(info.assume_init())
|
||||||
|
@ -862,10 +884,13 @@ fn stat_extra(
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = get_dev(file_handle);
|
let result = get_dev(file_handle);
|
||||||
CloseHandle(file_handle);
|
|
||||||
fsstat.dev = result?;
|
fsstat.dev = result?;
|
||||||
|
|
||||||
if let Ok(file_info) = query_file_information(file_handle) {
|
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
|
if file_info.BasicInformation.FileAttributes
|
||||||
& winapi::um::winnt::FILE_ATTRIBUTE_REPARSE_POINT
|
& winapi::um::winnt::FILE_ATTRIBUTE_REPARSE_POINT
|
||||||
!= 0
|
!= 0
|
||||||
|
@ -898,6 +923,7 @@ fn stat_extra(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CloseHandle(file_handle);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
12
ext/io/fs.rs
12
ext/io/fs.rs
|
@ -94,6 +94,7 @@ pub struct FsStat {
|
||||||
pub mtime: Option<u64>,
|
pub mtime: Option<u64>,
|
||||||
pub atime: Option<u64>,
|
pub atime: Option<u64>,
|
||||||
pub birthtime: Option<u64>,
|
pub birthtime: Option<u64>,
|
||||||
|
pub ctime: Option<u64>,
|
||||||
|
|
||||||
pub dev: u64,
|
pub dev: u64,
|
||||||
pub ino: 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 {
|
Self {
|
||||||
is_file: metadata.is_file(),
|
is_file: metadata.is_file(),
|
||||||
is_directory: metadata.is_dir(),
|
is_directory: metadata.is_dir(),
|
||||||
|
@ -162,6 +173,7 @@ impl FsStat {
|
||||||
mtime: to_msec(metadata.modified()),
|
mtime: to_msec(metadata.modified()),
|
||||||
atime: to_msec(metadata.accessed()),
|
atime: to_msec(metadata.accessed()),
|
||||||
birthtime: to_msec(metadata.created()),
|
birthtime: to_msec(metadata.created()),
|
||||||
|
ctime: get_ctime(unix_or_zero!(ctime)),
|
||||||
|
|
||||||
dev: unix_or_zero!(dev),
|
dev: unix_or_zero!(dev),
|
||||||
ino: unix_or_zero!(ino),
|
ino: unix_or_zero!(ino),
|
||||||
|
|
|
@ -290,8 +290,8 @@ export function convertFileInfoToStats(origin: Deno.FileInfo): Stats {
|
||||||
isFIFO: () => false,
|
isFIFO: () => false,
|
||||||
isCharacterDevice: () => false,
|
isCharacterDevice: () => false,
|
||||||
isSocket: () => false,
|
isSocket: () => false,
|
||||||
ctime: origin.mtime,
|
ctime: origin.ctime,
|
||||||
ctimeMs: origin.mtime?.getTime() || null,
|
ctimeMs: origin.ctime?.getTime() || null,
|
||||||
});
|
});
|
||||||
|
|
||||||
return stats;
|
return stats;
|
||||||
|
@ -336,9 +336,9 @@ export function convertFileInfoToBigIntStats(
|
||||||
isFIFO: () => false,
|
isFIFO: () => false,
|
||||||
isCharacterDevice: () => false,
|
isCharacterDevice: () => false,
|
||||||
isSocket: () => false,
|
isSocket: () => false,
|
||||||
ctime: origin.mtime,
|
ctime: origin.ctime,
|
||||||
ctimeMs: origin.mtime ? BigInt(origin.mtime.getTime()) : null,
|
ctimeMs: origin.ctime ? BigInt(origin.ctime.getTime()) : null,
|
||||||
ctimeNs: origin.mtime ? BigInt(origin.mtime.getTime()) * 1000000n : null,
|
ctimeNs: origin.ctime ? BigInt(origin.ctime.getTime()) * 1000000n : null,
|
||||||
});
|
});
|
||||||
return stats;
|
return stats;
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,13 @@ Deno.test(
|
||||||
assert(
|
assert(
|
||||||
tempInfo.birthtime === null || now - tempInfo.birthtime.valueOf() < 1000,
|
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"));
|
const readmeInfoByUrl = Deno.statSync(pathToAbsoluteFileUrl("README.md"));
|
||||||
assert(readmeInfoByUrl.isFile);
|
assert(readmeInfoByUrl.isFile);
|
||||||
|
@ -65,6 +72,10 @@ Deno.test(
|
||||||
tempInfoByUrl.birthtime === null ||
|
tempInfoByUrl.birthtime === null ||
|
||||||
now - tempInfoByUrl.birthtime.valueOf() < 1000,
|
now - tempInfoByUrl.birthtime.valueOf() < 1000,
|
||||||
);
|
);
|
||||||
|
assert(
|
||||||
|
tempInfoByUrl.ctime !== null &&
|
||||||
|
now - tempInfoByUrl.ctime.valueOf() < 1000,
|
||||||
|
);
|
||||||
|
|
||||||
Deno.removeSync(tempFile, { recursive: true });
|
Deno.removeSync(tempFile, { recursive: true });
|
||||||
Deno.removeSync(tempFileForUrl, { recursive: true });
|
Deno.removeSync(tempFileForUrl, { recursive: true });
|
||||||
|
@ -171,6 +182,7 @@ Deno.test(
|
||||||
assert(
|
assert(
|
||||||
tempInfo.birthtime === null || now - tempInfo.birthtime.valueOf() < 1000,
|
tempInfo.birthtime === null || now - tempInfo.birthtime.valueOf() < 1000,
|
||||||
);
|
);
|
||||||
|
assert(tempInfo.ctime !== null && now - tempInfo.ctime.valueOf() < 1000);
|
||||||
|
|
||||||
const tempFileForUrl = await Deno.makeTempFile();
|
const tempFileForUrl = await Deno.makeTempFile();
|
||||||
const tempInfoByUrl = await Deno.stat(
|
const tempInfoByUrl = await Deno.stat(
|
||||||
|
@ -191,7 +203,10 @@ Deno.test(
|
||||||
tempInfoByUrl.birthtime === null ||
|
tempInfoByUrl.birthtime === null ||
|
||||||
now - tempInfoByUrl.birthtime.valueOf() < 1000,
|
now - tempInfoByUrl.birthtime.valueOf() < 1000,
|
||||||
);
|
);
|
||||||
|
assert(
|
||||||
|
tempInfoByUrl.ctime !== null &&
|
||||||
|
now - tempInfoByUrl.ctime.valueOf() < 1000,
|
||||||
|
);
|
||||||
Deno.removeSync(tempFile, { recursive: true });
|
Deno.removeSync(tempFile, { recursive: true });
|
||||||
Deno.removeSync(tempFileForUrl, { recursive: true });
|
Deno.removeSync(tempFileForUrl, { recursive: true });
|
||||||
},
|
},
|
||||||
|
@ -271,7 +286,6 @@ Deno.test(
|
||||||
const s = Deno.statSync(filename);
|
const s = Deno.statSync(filename);
|
||||||
assert(s.dev !== 0);
|
assert(s.dev !== 0);
|
||||||
assert(s.ino === null);
|
assert(s.ino === null);
|
||||||
assert(s.mode === null);
|
|
||||||
assert(s.nlink === null);
|
assert(s.nlink === null);
|
||||||
assert(s.uid === null);
|
assert(s.uid === null);
|
||||||
assert(s.gid === 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.atime?.getTime(), expected.atime?.getTime());
|
||||||
assertEquals(actual.mtime?.getTime(), expected.mtime?.getTime());
|
assertEquals(actual.mtime?.getTime(), expected.mtime?.getTime());
|
||||||
assertEquals(actual.birthtime?.getTime(), expected.birthtime?.getTime());
|
assertEquals(actual.birthtime?.getTime(), expected.birthtime?.getTime());
|
||||||
|
assertEquals(actual.ctime?.getTime(), expected.ctime?.getTime());
|
||||||
assertEquals(actual.atimeMs ?? undefined, expected.atime?.getTime());
|
assertEquals(actual.atimeMs ?? undefined, expected.atime?.getTime());
|
||||||
assertEquals(actual.mtimeMs ?? undefined, expected.mtime?.getTime());
|
assertEquals(actual.mtimeMs ?? undefined, expected.mtime?.getTime());
|
||||||
assertEquals(actual.birthtimeMs ?? undefined, expected.birthtime?.getTime());
|
assertEquals(actual.birthtimeMs ?? undefined, expected.birthtime?.getTime());
|
||||||
|
assertEquals(actual.ctimeMs ?? undefined, expected.ctime?.getTime());
|
||||||
assertEquals(actual.isFile(), expected.isFile);
|
assertEquals(actual.isFile(), expected.isFile);
|
||||||
assertEquals(actual.isDirectory(), expected.isDirectory);
|
assertEquals(actual.isDirectory(), expected.isDirectory);
|
||||||
assertEquals(actual.isSymbolicLink(), expected.isSymlink);
|
assertEquals(actual.isSymbolicLink(), expected.isSymlink);
|
||||||
|
@ -49,6 +51,7 @@ export function assertStatsBigInt(
|
||||||
assertEquals(actual.atime?.getTime(), expected.atime?.getTime());
|
assertEquals(actual.atime?.getTime(), expected.atime?.getTime());
|
||||||
assertEquals(actual.mtime?.getTime(), expected.mtime?.getTime());
|
assertEquals(actual.mtime?.getTime(), expected.mtime?.getTime());
|
||||||
assertEquals(actual.birthtime?.getTime(), expected.birthtime?.getTime());
|
assertEquals(actual.birthtime?.getTime(), expected.birthtime?.getTime());
|
||||||
|
assertEquals(actual.ctime?.getTime(), expected.ctime?.getTime());
|
||||||
assertEquals(
|
assertEquals(
|
||||||
actual.atimeMs === null ? undefined : Number(actual.atimeMs),
|
actual.atimeMs === null ? undefined : Number(actual.atimeMs),
|
||||||
expected.atime?.getTime(),
|
expected.atime?.getTime(),
|
||||||
|
@ -61,6 +64,10 @@ export function assertStatsBigInt(
|
||||||
actual.birthtimeMs === null ? undefined : Number(actual.birthtimeMs),
|
actual.birthtimeMs === null ? undefined : Number(actual.birthtimeMs),
|
||||||
expected.birthtime?.getTime(),
|
expected.birthtime?.getTime(),
|
||||||
);
|
);
|
||||||
|
assertEquals(
|
||||||
|
actual.ctimeMs === null ? undefined : Number(actual.ctimeMs),
|
||||||
|
expected.ctime?.getTime(),
|
||||||
|
);
|
||||||
assertEquals(actual.atimeNs === null, actual.atime === null);
|
assertEquals(actual.atimeNs === null, actual.atime === null);
|
||||||
assertEquals(actual.mtimeNs === null, actual.mtime === null);
|
assertEquals(actual.mtimeNs === null, actual.mtime === null);
|
||||||
assertEquals(actual.birthtimeNs === null, actual.birthtime === null);
|
assertEquals(actual.birthtimeNs === null, actual.birthtime === null);
|
||||||
|
|
Loading…
Reference in a new issue