diff --git a/__test__/index.spec.js b/__test__/index.spec.js index b92ad91..6ac9d28 100644 --- a/__test__/index.spec.js +++ b/__test__/index.spec.js @@ -511,6 +511,13 @@ if (testResolve) { } }); } +ava_1.default.serial('should return error when getting file for deleted file handle', async (t) => { + const rootHandle = await getRootHandle(); + const fileHandle = await rootHandle.getFileHandle('transient', { create: true }); + await rootHandle.removeEntry(fileHandle.name); + const err = await t.throwsAsync(fileHandle.getFile()); + t.is(err?.message, 'File "transient" not found'); +}); ava_1.default.serial('should return file for file handle', async (t) => { const rootHandle = await getRootHandle(); const fileHandle = await rootHandle.getFileHandle('annar'); @@ -568,6 +575,13 @@ ava_1.default.serial('should return stream for file', async (t) => { const y = await reader.read(); t.true(y.done); }); +ava_1.default.serial('should return error when creating writable for deleted file handle', async (t) => { + const rootHandle = await getRootHandle(); + const fileHandle = await rootHandle.getFileHandle('fleeting', { create: true }); + await rootHandle.removeEntry(fileHandle.name); + const err = await t.throwsAsync(fileHandle.createWritable()); + t.is(err?.message, 'File "fleeting" not found'); +}); ava_1.default.serial('should succeed when streaming file larger than max_read_size', async (t) => { const rootHandle = await getRootHandle(); const fileHandle = await rootHandle.getFileHandle('writable-stream-larger-than-max-read-size', { create: true }); @@ -1185,6 +1199,62 @@ ava_1.default.serial('should handle getting directories concurrently', async (t) t.is(quatre.name, 'quatre'); } }); +ava_1.default.serial('should handle getting stats for a directory', async (t) => { + const rootHandle = await getRootHandle(); + const rootStats = await rootHandle.stat(); + t.assert(rootStats, 'root dir stats not returned'); + if (!node_process_1.default.env.TEST_USING_MOCKS) { + t.assert(rootStats.inode, 'root dir stats do not include inode'); + t.not(rootStats.creationTime, 0); + } + else { + t.assert(!rootStats.inode, 'root dir stats include indode'); + } + t.assert(rootStats.modifiedTime >= rootStats.creationTime, `root dir stats have creation time greater than modified time: ${JSON.stringify(rootStats)}`); + t.assert(rootStats.accessedTime >= rootStats.creationTime, `root dir stats have creation time greater than accessed time: ${JSON.stringify(rootStats)}`); + const dirHandle = await rootHandle.getDirectoryHandle('subdir-for-statting', { create: true }); + const dirStats = await dirHandle.stat(); + t.assert(dirStats, 'subdir stats not returned'); + if (!node_process_1.default.env.TEST_USING_MOCKS) { + t.assert(dirStats.inode, 'subdir stats do not include inode'); + t.not(dirStats.creationTime, 0); + } + else { + t.assert(!dirStats.inode, 'subdir stats include indode'); + } + t.not(dirStats.creationTime, 0); + t.assert(dirStats.modifiedTime >= dirStats.creationTime, `subdir stats have creation time greater than modified time: ${JSON.stringify(dirStats)}`); + t.assert(dirStats.accessedTime >= dirStats.creationTime, `subdir stats have creation time greater than accessed time: ${JSON.stringify(dirStats)}`); +}); +ava_1.default.serial('should handle getting stats for a file', async (t) => { + const rootHandle = await getRootHandle(); + const dirHandle = await rootHandle.getDirectoryHandle('first'); + const dirStats = await dirHandle.stat(); + t.assert(dirStats, 'dir stats not returned'); + if (!node_process_1.default.env.TEST_USING_MOCKS) { + t.assert(dirStats.inode, 'dir stats do not include inode'); + t.not(dirStats.creationTime, 0); + } + else { + t.assert(!dirStats.inode, 'dir stats include indode'); + } + t.not(dirStats.creationTime, 0); + t.assert(dirStats.modifiedTime >= dirStats.creationTime, `dir stats have creation time greater than modified time: ${JSON.stringify(dirStats)}`); + t.assert(dirStats.accessedTime >= dirStats.creationTime, `dir stats have creation time greater than accessed time: ${JSON.stringify(dirStats)}`); + const fileHandle = await rootHandle.getFileHandle('annar'); + const fileStats = await fileHandle.stat(); + t.assert(fileStats, 'file stats not returned'); + if (!node_process_1.default.env.TEST_USING_MOCKS) { + t.assert(fileStats.inode, 'file stats do not include inode'); + t.not(fileStats.creationTime, 0); + } + else { + t.assert(!fileStats.inode, 'file stats include indode'); + } + t.not(fileStats.creationTime, 0); + t.assert(fileStats.modifiedTime >= fileStats.creationTime, `file stats have creation time greater than modified time: ${JSON.stringify(fileStats)}`); + t.assert(fileStats.accessedTime >= fileStats.creationTime, `file stats have creation time greater than accessed time: ${JSON.stringify(fileStats)}`); +}); if (!node_process_1.default.env.TEST_USING_MOCKS) { ava_1.default.serial.skip('should handle watch', async (t) => { const sleep = async (ms) => { return new Promise((resolve) => setTimeout(resolve, ms)); }; diff --git a/__test__/index.spec.ts b/__test__/index.spec.ts index 1ea0138..92131e1 100644 --- a/__test__/index.spec.ts +++ b/__test__/index.spec.ts @@ -564,6 +564,14 @@ if (testResolve) { } +test.serial('should return error when getting file for deleted file handle', async (t) => { + const rootHandle = await getRootHandle(); + const fileHandle = await rootHandle.getFileHandle('transient', {create: true}); + await rootHandle.removeEntry(fileHandle.name); + const err = await t.throwsAsync(fileHandle.getFile()); + t.is(err?.message, 'File "transient" not found'); +}) + test.serial('should return file for file handle', async (t) => { const rootHandle = await getRootHandle(); const fileHandle = await rootHandle.getFileHandle('annar'); @@ -626,6 +634,14 @@ test.serial('should return stream for file', async (t) => { t.true(y.done); }) +test.serial('should return error when creating writable for deleted file handle', async (t) => { + const rootHandle = await getRootHandle(); + const fileHandle = await rootHandle.getFileHandle('fleeting', {create: true}); + await rootHandle.removeEntry(fileHandle.name); + const err = await t.throwsAsync(fileHandle.createWritable()); + t.is(err?.message, 'File "fleeting" not found'); +}) + test.serial('should succeed when streaming file larger than max_read_size', async (t) => { const rootHandle = await getRootHandle(); const fileHandle = await rootHandle.getFileHandle('writable-stream-larger-than-max-read-size', {create: true}) as SmbFileHandle; @@ -1292,6 +1308,60 @@ test.serial('should handle getting directories concurrently', async (t) => { } }) +test.serial('should handle getting stats for a directory', async (t) => { + const rootHandle = await getRootHandle() as any as SmbDirectoryHandle; + const rootStats = await rootHandle.stat(); + t.assert(rootStats, 'root dir stats not returned'); + if (!process.env.TEST_USING_MOCKS) { + t.assert(rootStats.inode, 'root dir stats do not include inode'); + t.not(rootStats.creationTime, 0); + } else { + t.assert(!rootStats.inode, 'root dir stats include indode'); + } + t.assert(rootStats.modifiedTime >= rootStats.creationTime, `root dir stats have creation time greater than modified time: ${JSON.stringify(rootStats)}`); + t.assert(rootStats.accessedTime >= rootStats.creationTime, `root dir stats have creation time greater than accessed time: ${JSON.stringify(rootStats)}`); + const dirHandle = await rootHandle.getDirectoryHandle('subdir-for-statting', {create: true}) as any as SmbDirectoryHandle; + const dirStats = await dirHandle.stat(); + t.assert(dirStats, 'subdir stats not returned'); + if (!process.env.TEST_USING_MOCKS) { + t.assert(dirStats.inode, 'subdir stats do not include inode'); + t.not(dirStats.creationTime, 0); + } else { + t.assert(!dirStats.inode, 'subdir stats include indode'); + } + t.not(dirStats.creationTime, 0); + t.assert(dirStats.modifiedTime >= dirStats.creationTime, `subdir stats have creation time greater than modified time: ${JSON.stringify(dirStats)}`); + t.assert(dirStats.accessedTime >= dirStats.creationTime, `subdir stats have creation time greater than accessed time: ${JSON.stringify(dirStats)}`); +}) + +test.serial('should handle getting stats for a file', async (t) => { + const rootHandle = await getRootHandle(); + const dirHandle = await rootHandle.getDirectoryHandle('first') as any as SmbDirectoryHandle; + const dirStats = await dirHandle.stat(); + t.assert(dirStats, 'dir stats not returned'); + if (!process.env.TEST_USING_MOCKS) { + t.assert(dirStats.inode, 'dir stats do not include inode'); + t.not(dirStats.creationTime, 0); + } else { + t.assert(!dirStats.inode, 'dir stats include indode'); + } + t.not(dirStats.creationTime, 0); + t.assert(dirStats.modifiedTime >= dirStats.creationTime, `dir stats have creation time greater than modified time: ${JSON.stringify(dirStats)}`); + t.assert(dirStats.accessedTime >= dirStats.creationTime, `dir stats have creation time greater than accessed time: ${JSON.stringify(dirStats)}`); + const fileHandle = await rootHandle.getFileHandle('annar') as any as SmbFileHandle; + const fileStats = await fileHandle.stat(); + t.assert(fileStats, 'file stats not returned'); + if (!process.env.TEST_USING_MOCKS) { + t.assert(fileStats.inode, 'file stats do not include inode'); + t.not(fileStats.creationTime, 0); + } else { + t.assert(!fileStats.inode, 'file stats include indode'); + } + t.not(fileStats.creationTime, 0); + t.assert(fileStats.modifiedTime >= fileStats.creationTime, `file stats have creation time greater than modified time: ${JSON.stringify(fileStats)}`); + t.assert(fileStats.accessedTime >= fileStats.creationTime, `file stats have creation time greater than accessed time: ${JSON.stringify(fileStats)}`); +}) + if (!process.env.TEST_USING_MOCKS) { test.serial.skip('should handle watch', async (t) => { const sleep = async (ms: number) => { return new Promise((resolve) => setTimeout(resolve, ms)); }; diff --git a/indax.cjs b/indax.cjs index 0b6748b..7157414 100644 --- a/indax.cjs +++ b/indax.cjs @@ -44,6 +44,9 @@ class SmbHandle { async requestPermission(perm) { return this._jsh.requestPermission(perm); } + async stat() { + return this._jsh.stat(); + } } exports.SmbHandle = SmbHandle; class SmbDirectoryHandle extends SmbHandle { diff --git a/indax.ts b/indax.ts index 6120453..b72fb8f 100644 --- a/indax.ts +++ b/indax.ts @@ -22,12 +22,14 @@ import { JsSmbGetFileOptions, JsSmbRemoveOptions, JsSmbCreateWritableOptions, + JsSmbStat, JsSmbHandle, JsSmbDirectoryHandle, JsSmbFileHandle, JsSmbWritableFileStream, } from './index'; +type SmbStat = JsSmbStat; type SmbHandlePermissionDescriptor = JsSmbHandlePermissionDescriptor; // @ts-ignore type SmbCreateWritableOptions = FileSystemCreateWritableOptions; @@ -60,6 +62,9 @@ export class SmbHandle implements FileSystemHandle { async requestPermission(perm: SmbHandlePermissionDescriptor): Promise { return this._jsh.requestPermission(perm) as Promise; } + async stat(): Promise { + return this._jsh.stat() as Promise; + } } export class SmbDirectoryHandle extends SmbHandle implements FileSystemDirectoryHandle { diff --git a/index.d.ts b/index.d.ts index b18f0e8..63cbdf6 100644 --- a/index.d.ts +++ b/index.d.ts @@ -18,6 +18,13 @@ export interface JsSmbRemoveOptions { export interface JsSmbCreateWritableOptions { keepExistingData: boolean } +export interface JsSmbStat { + readonly inode?: bigint + readonly size: bigint + readonly creationTime: bigint + readonly modifiedTime: bigint + readonly accessedTime: bigint +} export interface JsSmbNotifyChange { path: string action: string @@ -38,6 +45,7 @@ export declare class JsSmbHandle { isSameEntry(other: JsSmbHandle): boolean queryPermission(perm: JsSmbHandlePermissionDescriptor): Promise requestPermission(perm: JsSmbHandlePermissionDescriptor): Promise + stat(): Promise } export declare class JsSmbDirectoryHandle { [Symbol.asyncIterator]: JsSmbDirectoryHandle['entries'] diff --git a/libsmb2-rs/src/lib.rs b/libsmb2-rs/src/lib.rs index f6e0b67..7eab17f 100644 --- a/libsmb2-rs/src/lib.rs +++ b/libsmb2-rs/src/lib.rs @@ -180,10 +180,12 @@ pub struct DirEntry { pub atime: u64, pub mtime: u64, pub ctime: u64, + pub btime: u64, pub nlink: u32, pub atime_nsec: u64, pub mtime_nsec: u64, pub ctime_nsec: u64, + pub btime_nsec: u64, } #[derive(Clone)] @@ -1126,10 +1128,12 @@ impl Iterator for SmbDirectory { atime: (stat).smb2_atime, mtime: (stat).smb2_mtime, ctime: (stat).smb2_ctime, + btime: (stat).smb2_btime, nlink: (stat).smb2_nlink, atime_nsec: (stat).smb2_atime_nsec, mtime_nsec: (stat).smb2_mtime_nsec, ctime_nsec: (stat).smb2_ctime_nsec, + btime_nsec: (stat).smb2_btime_nsec, })) } } diff --git a/src/lib.rs b/src/lib.rs index 7c7f452..641d54b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,6 +26,8 @@ use std::{path::Path, sync::{mpsc::{channel, Receiver, Sender}, Arc, RwLock, RwL mod smb; use smb::{VFSEntryType, VFSFileNotificationOperation, VFSNotifyChangeCallback, VFSWatchMode, VFS}; +use crate::smb::VFSStat; + /* See https://wicg.github.io/file-system-access/ @@ -334,6 +336,32 @@ impl Default for JsSmbCreateWritableOptions { } } +#[napi(object)] +pub struct JsSmbStat { + #[napi(readonly, ts_type="bigint")] + pub inode: Option, + #[napi(readonly, ts_type="bigint")] + pub size: i64, + #[napi(readonly, ts_type="bigint")] + pub creation_time: i64, + #[napi(readonly, ts_type="bigint")] + pub modified_time: i64, + #[napi(readonly, ts_type="bigint")] + pub accessed_time: i64 +} + +impl From for JsSmbStat { + fn from(value: VFSStat) -> Self { + JsSmbStat { + inode: (value.ino != 0).then_some(value.ino as i64), + size: value.size as i64, + creation_time: ((value.btime * 1_000_000_000) + value.btime_nsec) as i64, + modified_time: ((value.mtime * 1_000_000_000) + value.mtime_nsec) as i64, + accessed_time: ((value.atime * 1_000_000_000) + value.atime_nsec) as i64, + } + } +} + #[derive(Clone)] #[napi] pub struct JsSmbHandle { @@ -412,6 +440,14 @@ impl JsSmbHandle { }*/ self.query_permission(perm).await } + + #[napi] + pub async fn stat(&self) -> Result { + let smb = &self.smb; + let my_smb = using_rwlock!(smb); + let smb_stat = my_smb.stat(&self.path)?; + Ok(smb_stat.into()) + } } impl FromNapiValue for JsSmbHandle { @@ -760,7 +796,7 @@ impl JsSmbFileHandle { let smb = &self.handle.smb; let my_smb = using_rwlock!(smb); let smb_stat = my_smb.stat(self.handle.path.as_str())?; - Ok(JsSmbFile{handle: self.handle.clone(), size: smb_stat.size as i64, type_, last_modified: (smb_stat.mtime * 1000) as i64, name: self.name.clone()}) + Ok(JsSmbFile{handle: self.handle.clone(), size: smb_stat.size as i64, type_, last_modified: ((smb_stat.mtime * 1000) + (smb_stat.mtime_nsec / 1000000)) as i64, name: self.name.clone()}) } #[napi] diff --git a/src/smb/libsmb.rs b/src/smb/libsmb.rs index 96de11c..d2f5bea 100644 --- a/src/smb/libsmb.rs +++ b/src/smb/libsmb.rs @@ -113,9 +113,11 @@ impl VFS for SMBConnection { atime: res.smb2_atime, mtime: res.smb2_mtime, ctime: res.smb2_ctime, + btime: res.smb2_btime, atime_nsec: res.smb2_atime_nsec, mtime_nsec: res.smb2_mtime_nsec, ctime_nsec: res.smb2_ctime_nsec, + btime_nsec: res.smb2_btime_nsec, }) } @@ -288,10 +290,12 @@ impl Iterator for SMBDirectory2 { atime: Time{seconds: entry.atime as u32, nseconds: entry.atime_nsec}, mtime: Time{seconds: entry.mtime as u32, nseconds: entry.mtime_nsec}, ctime: Time{seconds: entry.ctime as u32, nseconds: entry.ctime_nsec}, + btime: Time{seconds: entry.btime as u32, nseconds: entry.btime_nsec}, nlink: entry.nlink, atime_nsec: entry.atime_nsec, mtime_nsec: entry.mtime_nsec, ctime_nsec: entry.ctime_nsec, + btime_nsec: entry.btime_nsec, })) } } @@ -315,9 +319,11 @@ impl VFSFile for SMBFile2 { atime: res.smb2_atime, mtime: res.smb2_mtime, ctime: res.smb2_ctime, + btime: res.smb2_btime, atime_nsec: res.smb2_atime_nsec, mtime_nsec: res.smb2_mtime_nsec, ctime_nsec: res.smb2_ctime_nsec, + btime_nsec: res.smb2_btime_nsec, }) } diff --git a/src/smb/mock.rs b/src/smb/mock.rs index ae06ab1..007bcd7 100644 --- a/src/smb/mock.rs +++ b/src/smb/mock.rs @@ -51,6 +51,7 @@ pub(super) struct SMBConnection { impl SMBConnection { pub(super) fn connect(_url: String) -> Result> { let mut mocks = Mocks{dirs: BTreeSet::new(), files: BTreeMap::new()}; + let _ = mocks.dirs.insert("/".into()); let _ = mocks.dirs.insert("/first/".into()); let _ = mocks.dirs.insert("/quatre/".into()); let _ = mocks.files.insert("/3".into(), Vec::new()); @@ -77,6 +78,9 @@ impl VFS for SMBConnection { let size = if let Some(c) = mocks.files.get(&path.to_string()) { Some(c.len() as u64) } else { + if !mocks.dirs.contains(&path.to_string()) { + return Err(Error::new(std::io::ErrorKind::Other, "entry not found")); + } None }; /*let mode = if size.is_some() { @@ -92,9 +96,11 @@ impl VFS for SMBConnection { atime: 1658159058723, mtime: 1658159058723, ctime: 1658159058720, + btime: 1658159058718, atime_nsec: Default::default(), mtime_nsec: Default::default(), ctime_nsec: Default::default(), + btime_nsec: Default::default(), }) } @@ -189,10 +195,12 @@ impl Iterator for SMBSDirectory2 { atime: Time{seconds: 1658159058, nseconds: 0}, mtime: Time{seconds: 1658159058, nseconds: 0}, ctime: Time{seconds: 1658159055, nseconds: 0}, + btime: Time{seconds: 1658159053, nseconds: 0}, nlink: Default::default(), atime_nsec: Default::default(), mtime_nsec: Default::default(), ctime_nsec: Default::default(), + btime_nsec: Default::default(), }); } } @@ -208,10 +216,12 @@ impl Iterator for SMBSDirectory2 { atime: Time{seconds: 1658159058, nseconds: 0}, mtime: Time{seconds: 1658159058, nseconds: 0}, ctime: Time{seconds: 1658159055, nseconds: 0}, + btime: Time{seconds: 1658159053, nseconds: 0}, nlink: Default::default(), atime_nsec: Default::default(), mtime_nsec: Default::default(), ctime_nsec: Default::default(), + btime_nsec: Default::default(), }); } } @@ -254,9 +264,11 @@ impl VFSFile for SMBFile2 { atime: 1658159058723, mtime: 1658159058723, ctime: 1658159058720, + btime: 1658159058718, atime_nsec: Default::default(), mtime_nsec: Default::default(), ctime_nsec: Default::default(), + btime_nsec: Default::default(), }) } diff --git a/src/smb/mod.rs b/src/smb/mod.rs index a46f752..bea0b4b 100644 --- a/src/smb/mod.rs +++ b/src/smb/mod.rs @@ -130,16 +130,18 @@ impl From for VFSEntryType { #[derive(Debug, Clone)] pub struct VFSDirEntry { pub path: String, - pub inode: u64, pub d_type: VFSEntryType, + pub inode: u64, + pub nlink: u32, pub size: u64, pub atime: Time, pub mtime: Time, pub ctime: Time, - pub nlink: u32, + pub btime: Time, pub atime_nsec: u64, pub mtime_nsec: u64, pub ctime_nsec: u64, + pub btime_nsec: u64, } #[allow(dead_code)] @@ -151,9 +153,11 @@ pub struct VFSStat { pub atime: u64, pub mtime: u64, pub ctime: u64, + pub btime: u64, pub atime_nsec: u64, pub mtime_nsec: u64, pub ctime_nsec: u64, + pub btime_nsec: u64, } pub(crate) fn connect(url: String) -> Result> {