@zenfs/core
Advanced tools
Comparing version 0.11.2 to 0.12.0
@@ -1,5 +0,5 @@ | ||
import { NoSyncFile } from '../file.js'; | ||
import type { FileSystemMetadata } from '../filesystem.js'; | ||
import { Stats } from '../stats.js'; | ||
import { type ListingTree, type IndexFileInode, AsyncIndexFS } from './Index.js'; | ||
import { IndexFS } from './index/fs.js'; | ||
import type { IndexData } from './index/index.js'; | ||
/** | ||
@@ -13,3 +13,3 @@ * Configuration options for FetchFS. | ||
*/ | ||
index?: string | ListingTree; | ||
index?: string | IndexData; | ||
/** Used as the URL prefix for fetched files. | ||
@@ -21,49 +21,36 @@ * Default: Fetch files relative to the index. | ||
/** | ||
* A simple filesystem backed by HTTP using the fetch API. | ||
* A simple filesystem backed by HTTP using the `fetch` API. | ||
* | ||
* | ||
* Listings objects look like the following: | ||
* Index objects look like the following: | ||
* | ||
* ```json | ||
* { | ||
* "home": { | ||
* "jvilk": { | ||
* "someFile.txt": null, | ||
* "someDir": { | ||
* // Empty directory | ||
* } | ||
* } | ||
* } | ||
* "version": 1, | ||
* "entries": { | ||
* "/home": { ... }, | ||
* "/home/jvilk": { ... }, | ||
* "/home/james": { ... } | ||
* } | ||
* } | ||
* ``` | ||
* | ||
* This example has the folder `/home/jvilk` with subfile `someFile.txt` and subfolder `someDir`. | ||
* Each entry contains the stats associated with the file. | ||
*/ | ||
export declare class FetchFS extends AsyncIndexFS<Stats> { | ||
readonly prefixUrl: string; | ||
protected _init: Promise<void>; | ||
protected _initialize(index: string | ListingTree): Promise<void>; | ||
export declare class FetchFS extends IndexFS { | ||
readonly baseUrl: string; | ||
ready(): Promise<void>; | ||
constructor({ index, baseUrl }: FetchOptions); | ||
metadata(): FileSystemMetadata; | ||
empty(): void; | ||
/** | ||
* Special function: Preload the given file into the index. | ||
* Preload the given file into the index. | ||
* @param path | ||
* @param buffer | ||
*/ | ||
preloadFile(path: string, buffer: Uint8Array): void; | ||
protected statFileInode(inode: IndexFileInode<Stats>, path: string): Promise<Stats>; | ||
protected openFileInode(inode: IndexFileInode<Stats>, path: string, flag: string): Promise<NoSyncFile<this>>; | ||
private _getRemotePath; | ||
preload(path: string, buffer: Uint8Array): void; | ||
/** | ||
* Asynchronously download the given file. | ||
* @todo Be lazier about actually requesting the data? | ||
*/ | ||
protected _fetchFile(path: string, type: 'buffer'): Promise<Uint8Array>; | ||
protected _fetchFile(path: string, type: 'json'): Promise<object>; | ||
protected _fetchFile(path: string, type: 'buffer' | 'json'): Promise<object>; | ||
/** | ||
* Only requests the HEAD content, for the file size. | ||
*/ | ||
protected _fetchSize(path: string): Promise<number>; | ||
protected getData(path: string, stats: Stats): Promise<Uint8Array>; | ||
protected getDataSync(path: string, stats: Stats): Uint8Array; | ||
} | ||
@@ -70,0 +57,0 @@ export declare const Fetch: { |
@@ -1,5 +0,3 @@ | ||
import { ErrnoError, Errno } from '../error.js'; | ||
import { NoSyncFile } from '../file.js'; | ||
import { Stats } from '../stats.js'; | ||
import { FileIndex, AsyncIndexFS } from './Index.js'; | ||
import { Errno, ErrnoError } from '../error.js'; | ||
import { IndexFS } from './index/fs.js'; | ||
async function fetchFile(path, type) { | ||
@@ -27,54 +25,35 @@ const response = await fetch(path).catch(e => { | ||
/** | ||
* Asynchronously retrieves the size of the given file in bytes. | ||
* @hidden | ||
*/ | ||
async function fetchSize(path) { | ||
const response = await fetch(path, { method: 'HEAD' }).catch(e => { | ||
throw new ErrnoError(Errno.EIO, e.message); | ||
}); | ||
if (!response.ok) { | ||
throw new ErrnoError(Errno.EIO, 'fetch failed: HEAD response returned code ' + response.status); | ||
} | ||
return parseInt(response.headers.get('Content-Length') || '-1', 10); | ||
} | ||
/** | ||
* A simple filesystem backed by HTTP using the fetch API. | ||
* A simple filesystem backed by HTTP using the `fetch` API. | ||
* | ||
* | ||
* Listings objects look like the following: | ||
* Index objects look like the following: | ||
* | ||
* ```json | ||
* { | ||
* "home": { | ||
* "jvilk": { | ||
* "someFile.txt": null, | ||
* "someDir": { | ||
* // Empty directory | ||
* } | ||
* } | ||
* } | ||
* "version": 1, | ||
* "entries": { | ||
* "/home": { ... }, | ||
* "/home/jvilk": { ... }, | ||
* "/home/james": { ... } | ||
* } | ||
* } | ||
* ``` | ||
* | ||
* This example has the folder `/home/jvilk` with subfile `someFile.txt` and subfolder `someDir`. | ||
* Each entry contains the stats associated with the file. | ||
*/ | ||
export class FetchFS extends AsyncIndexFS { | ||
async _initialize(index) { | ||
if (typeof index != 'string') { | ||
this._index = FileIndex.FromListing(index); | ||
export class FetchFS extends IndexFS { | ||
async ready() { | ||
if (this._isInitialized) { | ||
return; | ||
} | ||
try { | ||
const response = await fetch(index); | ||
this._index = FileIndex.FromListing((await response.json())); | ||
await super.ready(); | ||
/** | ||
* Iterate over all of the files and cache their contents | ||
*/ | ||
for (const [path, stats] of this.index.files()) { | ||
await this.getData(path, stats); | ||
} | ||
catch (e) { | ||
throw new ErrnoError(Errno.EINVAL, 'Invalid or unavailable file listing tree'); | ||
} | ||
} | ||
async ready() { | ||
await this._init; | ||
} | ||
constructor({ index = 'index.json', baseUrl = '' }) { | ||
super({}); | ||
super(typeof index != 'string' ? index : fetchFile(index, 'json')); | ||
// prefix url must end in a directory separator. | ||
@@ -84,4 +63,3 @@ if (baseUrl.at(-1) != '/') { | ||
} | ||
this.prefixUrl = baseUrl; | ||
this._init = this._initialize(index); | ||
this.baseUrl = baseUrl; | ||
} | ||
@@ -95,60 +73,35 @@ metadata() { | ||
} | ||
empty() { | ||
for (const file of this._index.files()) { | ||
delete file.data.fileData; | ||
} | ||
} | ||
/** | ||
* Special function: Preload the given file into the index. | ||
* Preload the given file into the index. | ||
* @param path | ||
* @param buffer | ||
*/ | ||
preloadFile(path, buffer) { | ||
const inode = this._index.get(path); | ||
if (!inode) { | ||
preload(path, buffer) { | ||
const stats = this.index.get(path); | ||
if (!stats) { | ||
throw ErrnoError.With('ENOENT', path, 'preloadFile'); | ||
} | ||
if (!inode.isFile()) { | ||
if (!stats.isFile()) { | ||
throw ErrnoError.With('EISDIR', path, 'preloadFile'); | ||
} | ||
const stats = inode.data; | ||
stats.size = buffer.length; | ||
stats.fileData = buffer; | ||
} | ||
async statFileInode(inode, path) { | ||
const stats = inode.data; | ||
// At this point, a non-opened file will still have default stats from the listing. | ||
if (stats.size < 0) { | ||
stats.size = await this._fetchSize(path); | ||
} | ||
return stats; | ||
} | ||
async openFileInode(inode, path, flag) { | ||
const stats = inode.data; | ||
// Use existing file contents. This maintains the previously-used flag. | ||
/** | ||
* @todo Be lazier about actually requesting the data? | ||
*/ | ||
async getData(path, stats) { | ||
if (stats.fileData) { | ||
return new NoSyncFile(this, path, flag, new Stats(stats), stats.fileData); | ||
return stats.fileData; | ||
} | ||
// @todo be lazier about actually requesting the file | ||
const data = await this._fetchFile(path, 'buffer'); | ||
// we don't initially have file sizes | ||
stats.size = data.length; | ||
const data = await fetchFile(this.baseUrl + (path.startsWith('/') ? path.slice(1) : path), 'buffer'); | ||
stats.fileData = data; | ||
return new NoSyncFile(this, path, flag, new Stats(stats), data); | ||
return data; | ||
} | ||
_getRemotePath(filePath) { | ||
if (filePath.charAt(0) === '/') { | ||
filePath = filePath.slice(1); | ||
getDataSync(path, stats) { | ||
if (stats.fileData) { | ||
return stats.fileData; | ||
} | ||
return this.prefixUrl + filePath; | ||
throw new ErrnoError(Errno.ENODATA, '', path, 'getData'); | ||
} | ||
_fetchFile(path, type) { | ||
return fetchFile(this._getRemotePath(path), type); | ||
} | ||
/** | ||
* Only requests the HEAD content, for the file size. | ||
*/ | ||
_fetchSize(path) { | ||
return fetchSize(this._getRemotePath(path)); | ||
} | ||
} | ||
@@ -155,0 +108,0 @@ export const Fetch = { |
@@ -360,3 +360,3 @@ /// <reference types="node" resolution-mode="require"/> | ||
export declare class NoSyncFile<T extends FileSystem> extends PreloadFile<T> { | ||
constructor(_fs: T, _path: string, _flag: string, _stat: Stats, contents?: Uint8Array); | ||
constructor(fs: T, path: string, flag: string, stats: Stats, contents?: Uint8Array); | ||
/** | ||
@@ -363,0 +363,0 @@ * Asynchronous sync. Doesn't do anything, simply calls the cb. |
@@ -477,4 +477,4 @@ import { ErrnoError, Errno } from './error.js'; | ||
export class NoSyncFile extends PreloadFile { | ||
constructor(_fs, _path, _flag, _stat, contents) { | ||
super(_fs, _path, _flag, _stat, contents); | ||
constructor(fs, path, flag, stats, contents) { | ||
super(fs, path, flag, stats, contents); | ||
} | ||
@@ -481,0 +481,0 @@ /** |
@@ -5,3 +5,3 @@ export * from './error.js'; | ||
export * from './backends/memory.js'; | ||
export * from './backends/Index.js'; | ||
export * from './backends/index/fs.js'; | ||
export * from './backends/locked.js'; | ||
@@ -8,0 +8,0 @@ export * from './backends/overlay.js'; |
@@ -5,3 +5,3 @@ export * from './error.js'; | ||
export * from './backends/memory.js'; | ||
export * from './backends/Index.js'; | ||
export * from './backends/index/fs.js'; | ||
export * from './backends/locked.js'; | ||
@@ -8,0 +8,0 @@ export * from './backends/overlay.js'; |
@@ -15,3 +15,3 @@ /// <reference types="node" resolution-mode="require"/> | ||
*/ | ||
export interface StatsLike { | ||
export interface StatsLike<T extends number | bigint = number | bigint> { | ||
/** | ||
@@ -21,3 +21,3 @@ * Size of the item in bytes. | ||
*/ | ||
size: number | bigint; | ||
size: T; | ||
/** | ||
@@ -27,31 +27,31 @@ * Unix-style file mode (e.g. 0o644) that includes the item type | ||
*/ | ||
mode: number | bigint; | ||
mode: T; | ||
/** | ||
* time of last access, in milliseconds since epoch | ||
*/ | ||
atimeMs: number | bigint; | ||
atimeMs: T; | ||
/** | ||
* time of last modification, in milliseconds since epoch | ||
*/ | ||
mtimeMs: number | bigint; | ||
mtimeMs: T; | ||
/** | ||
* time of last time file status was changed, in milliseconds since epoch | ||
*/ | ||
ctimeMs: number | bigint; | ||
ctimeMs: T; | ||
/** | ||
* time of file creation, in milliseconds since epoch | ||
*/ | ||
birthtimeMs: number | bigint; | ||
birthtimeMs: T; | ||
/** | ||
* the id of the user that owns the file | ||
*/ | ||
uid: number | bigint; | ||
uid: T; | ||
/** | ||
* the id of the group that owns the file | ||
*/ | ||
gid: number | bigint; | ||
gid: T; | ||
/** | ||
* the ino | ||
*/ | ||
ino: number | bigint; | ||
ino: T; | ||
} | ||
@@ -58,0 +58,0 @@ /** |
{ | ||
"name": "@zenfs/core", | ||
"version": "0.11.2", | ||
"version": "0.12.0", | ||
"description": "A filesystem in your browser", | ||
@@ -5,0 +5,0 @@ "main": "dist/index.js", |
@@ -5,3 +5,3 @@ #!/usr/bin/env node | ||
import { join } from 'path/posix'; | ||
import { resolve } from 'path'; | ||
import { relative, resolve } from 'path'; | ||
import { minimatch } from 'minimatch'; | ||
@@ -42,6 +42,8 @@ | ||
function pathToPosix(path) { | ||
function fixSlash(path) { | ||
return path.replaceAll('\\', '/'); | ||
} | ||
const resolvedRoot = root || '.'; | ||
const colors = { | ||
@@ -71,24 +73,27 @@ reset: 0, | ||
function listing(path, seen = new Set()) { | ||
const entries = new Map(); | ||
function computeEntries(path) { | ||
try { | ||
if (options.verbose) console.log(`${color('blue', 'list')} ${path}`); | ||
if (options.ignore.some(pattern => minimatch(path, pattern))) { | ||
if (!options.quiet) console.log(`${color('yellow', 'skip')} ${path}`); | ||
return; | ||
} | ||
const stats = statSync(path); | ||
entries.set('/' + relative(resolvedRoot, path), stats); | ||
if (stats.isFile()) { | ||
if (options.verbose) console.log(`${color('green', 'file')} ${path}`); | ||
return null; | ||
if (options.verbose) { | ||
console.log(`${color('green', 'file')} ${path}`); | ||
} | ||
return; | ||
} | ||
const entries = {}; | ||
for (const file of readdirSync(path)) { | ||
const full = join(path, file); | ||
if (options.ignore.some(pattern => minimatch(full, pattern))) { | ||
if (!options.quiet) console.log(`${color('yellow', 'skip')} ${full}`); | ||
continue; | ||
} | ||
entries[file] = listing(full, seen); | ||
computeEntries(join(path, file)); | ||
} | ||
if (options.verbose) console.log(`${color('bright_green', ' dir')} ${path}`); | ||
return entries; | ||
if (options.verbose) { | ||
console.log(`${color('bright_green', ' dir')} ${path}`); | ||
} | ||
} catch (e) { | ||
@@ -101,5 +106,12 @@ if (!options.quiet) { | ||
const rootListing = listing(pathToPosix(root)); | ||
if (!options.quiet) console.log('Generated listing for ' + pathToPosix(resolve(root))); | ||
computeEntries(resolvedRoot); | ||
if (!options.quiet) { | ||
console.log('Generated listing for ' + fixSlash(resolve(root))); | ||
} | ||
writeFileSync(options.output, JSON.stringify(rootListing)); | ||
const index = { | ||
version: 1, | ||
entries: Object.fromEntries(entries), | ||
}; | ||
writeFileSync(options.output, JSON.stringify(index)); |
@@ -1,7 +0,7 @@ | ||
import { ErrnoError, Errno } from '../error.js'; | ||
import { NoSyncFile } from '../file.js'; | ||
import { Errno, ErrnoError } from '../error.js'; | ||
import type { FileSystemMetadata } from '../filesystem.js'; | ||
import { Stats } from '../stats.js'; | ||
import { type ListingTree, FileIndex, type IndexFileInode, AsyncIndexFS } from './Index.js'; | ||
import type { Backend } from './backend.js'; | ||
import { IndexFS } from './index/fs.js'; | ||
import type { IndexData } from './index/index.js'; | ||
@@ -41,16 +41,2 @@ /** | ||
/** | ||
* Asynchronously retrieves the size of the given file in bytes. | ||
* @hidden | ||
*/ | ||
async function fetchSize(path: string): Promise<number> { | ||
const response = await fetch(path, { method: 'HEAD' }).catch(e => { | ||
throw new ErrnoError(Errno.EIO, e.message); | ||
}); | ||
if (!response.ok) { | ||
throw new ErrnoError(Errno.EIO, 'fetch failed: HEAD response returned code ' + response.status); | ||
} | ||
return parseInt(response.headers.get('Content-Length') || '-1', 10); | ||
} | ||
/** | ||
* Configuration options for FetchFS. | ||
@@ -63,3 +49,3 @@ */ | ||
*/ | ||
index?: string | ListingTree; | ||
index?: string | IndexData; | ||
@@ -73,47 +59,38 @@ /** Used as the URL prefix for fetched files. | ||
/** | ||
* A simple filesystem backed by HTTP using the fetch API. | ||
* A simple filesystem backed by HTTP using the `fetch` API. | ||
* | ||
* | ||
* Listings objects look like the following: | ||
* Index objects look like the following: | ||
* | ||
* ```json | ||
* { | ||
* "home": { | ||
* "jvilk": { | ||
* "someFile.txt": null, | ||
* "someDir": { | ||
* // Empty directory | ||
* } | ||
* } | ||
* } | ||
* "version": 1, | ||
* "entries": { | ||
* "/home": { ... }, | ||
* "/home/jvilk": { ... }, | ||
* "/home/james": { ... } | ||
* } | ||
* } | ||
* ``` | ||
* | ||
* This example has the folder `/home/jvilk` with subfile `someFile.txt` and subfolder `someDir`. | ||
* Each entry contains the stats associated with the file. | ||
*/ | ||
export class FetchFS extends AsyncIndexFS<Stats> { | ||
public readonly prefixUrl: string; | ||
export class FetchFS extends IndexFS { | ||
public readonly baseUrl: string; | ||
protected _init: Promise<void>; | ||
protected async _initialize(index: string | ListingTree): Promise<void> { | ||
if (typeof index != 'string') { | ||
this._index = FileIndex.FromListing(index); | ||
public async ready(): Promise<void> { | ||
if (this._isInitialized) { | ||
return; | ||
} | ||
try { | ||
const response = await fetch(index); | ||
this._index = FileIndex.FromListing((await response.json()) as ListingTree); | ||
} catch (e) { | ||
throw new ErrnoError(Errno.EINVAL, 'Invalid or unavailable file listing tree'); | ||
await super.ready(); | ||
/** | ||
* Iterate over all of the files and cache their contents | ||
*/ | ||
for (const [path, stats] of this.index.files()) { | ||
await this.getData(path, stats); | ||
} | ||
} | ||
public async ready(): Promise<void> { | ||
await this._init; | ||
} | ||
constructor({ index = 'index.json', baseUrl = '' }: FetchOptions) { | ||
super({}); | ||
super(typeof index != 'string' ? index : fetchFile<IndexData>(index, 'json')); | ||
@@ -124,5 +101,3 @@ // prefix url must end in a directory separator. | ||
} | ||
this.prefixUrl = baseUrl; | ||
this._init = this._initialize(index); | ||
this.baseUrl = baseUrl; | ||
} | ||
@@ -138,22 +113,15 @@ | ||
public empty(): void { | ||
for (const file of this._index.files()) { | ||
delete file.data!.fileData; | ||
} | ||
} | ||
/** | ||
* Special function: Preload the given file into the index. | ||
* Preload the given file into the index. | ||
* @param path | ||
* @param buffer | ||
*/ | ||
public preloadFile(path: string, buffer: Uint8Array): void { | ||
const inode = this._index.get(path)!; | ||
if (!inode) { | ||
public preload(path: string, buffer: Uint8Array): void { | ||
const stats = this.index.get(path); | ||
if (!stats) { | ||
throw ErrnoError.With('ENOENT', path, 'preloadFile'); | ||
} | ||
if (!inode.isFile()) { | ||
if (!stats.isFile()) { | ||
throw ErrnoError.With('EISDIR', path, 'preloadFile'); | ||
} | ||
const stats = inode.data!; | ||
stats.size = buffer.length; | ||
@@ -163,49 +131,22 @@ stats.fileData = buffer; | ||
protected async statFileInode(inode: IndexFileInode<Stats>, path: string): Promise<Stats> { | ||
const stats = inode.data!; | ||
// At this point, a non-opened file will still have default stats from the listing. | ||
if (stats.size < 0) { | ||
stats.size = await this._fetchSize(path); | ||
/** | ||
* @todo Be lazier about actually requesting the data? | ||
*/ | ||
protected async getData(path: string, stats: Stats): Promise<Uint8Array> { | ||
if (stats.fileData) { | ||
return stats.fileData; | ||
} | ||
return stats; | ||
const data = await fetchFile(this.baseUrl + (path.startsWith('/') ? path.slice(1) : path), 'buffer'); | ||
stats.fileData = data; | ||
return data; | ||
} | ||
protected async openFileInode(inode: IndexFileInode<Stats>, path: string, flag: string): Promise<NoSyncFile<this>> { | ||
const stats = inode.data!; | ||
// Use existing file contents. This maintains the previously-used flag. | ||
protected getDataSync(path: string, stats: Stats): Uint8Array { | ||
if (stats.fileData) { | ||
return new NoSyncFile(this, path, flag, new Stats(stats), stats.fileData); | ||
return stats.fileData; | ||
} | ||
// @todo be lazier about actually requesting the file | ||
const data = await this._fetchFile(path, 'buffer'); | ||
// we don't initially have file sizes | ||
stats.size = data.length; | ||
stats.fileData = data; | ||
return new NoSyncFile(this, path, flag, new Stats(stats), data); | ||
} | ||
private _getRemotePath(filePath: string): string { | ||
if (filePath.charAt(0) === '/') { | ||
filePath = filePath.slice(1); | ||
} | ||
return this.prefixUrl + filePath; | ||
throw new ErrnoError(Errno.ENODATA, '', path, 'getData'); | ||
} | ||
/** | ||
* Asynchronously download the given file. | ||
*/ | ||
protected _fetchFile(path: string, type: 'buffer'): Promise<Uint8Array>; | ||
protected _fetchFile(path: string, type: 'json'): Promise<object>; | ||
protected _fetchFile(path: string, type: 'buffer' | 'json'): Promise<object>; | ||
protected _fetchFile(path: string, type: 'buffer' | 'json'): Promise<object> { | ||
return fetchFile(this._getRemotePath(path), type); | ||
} | ||
/** | ||
* Only requests the HEAD content, for the file size. | ||
*/ | ||
protected _fetchSize(path: string): Promise<number> { | ||
return fetchSize(this._getRemotePath(path)); | ||
} | ||
} | ||
@@ -212,0 +153,0 @@ |
@@ -704,4 +704,4 @@ import type { FileReadResult } from 'node:fs/promises'; | ||
export class NoSyncFile<T extends FileSystem> extends PreloadFile<T> { | ||
constructor(_fs: T, _path: string, _flag: string, _stat: Stats, contents?: Uint8Array) { | ||
super(_fs, _path, _flag, _stat, contents); | ||
constructor(fs: T, path: string, flag: string, stats: Stats, contents?: Uint8Array) { | ||
super(fs, path, flag, stats, contents); | ||
} | ||
@@ -708,0 +708,0 @@ /** |
@@ -5,3 +5,3 @@ export * from './error.js'; | ||
export * from './backends/memory.js'; | ||
export * from './backends/Index.js'; | ||
export * from './backends/index/fs.js'; | ||
export * from './backends/locked.js'; | ||
@@ -8,0 +8,0 @@ export * from './backends/overlay.js'; |
@@ -17,3 +17,3 @@ import type * as Node from 'fs'; | ||
*/ | ||
export interface StatsLike { | ||
export interface StatsLike<T extends number | bigint = number | bigint> { | ||
/** | ||
@@ -23,3 +23,3 @@ * Size of the item in bytes. | ||
*/ | ||
size: number | bigint; | ||
size: T; | ||
/** | ||
@@ -29,31 +29,31 @@ * Unix-style file mode (e.g. 0o644) that includes the item type | ||
*/ | ||
mode: number | bigint; | ||
mode: T; | ||
/** | ||
* time of last access, in milliseconds since epoch | ||
*/ | ||
atimeMs: number | bigint; | ||
atimeMs: T; | ||
/** | ||
* time of last modification, in milliseconds since epoch | ||
*/ | ||
mtimeMs: number | bigint; | ||
mtimeMs: T; | ||
/** | ||
* time of last time file status was changed, in milliseconds since epoch | ||
*/ | ||
ctimeMs: number | bigint; | ||
ctimeMs: T; | ||
/** | ||
* time of file creation, in milliseconds since epoch | ||
*/ | ||
birthtimeMs: number | bigint; | ||
birthtimeMs: T; | ||
/** | ||
* the id of the user that owns the file | ||
*/ | ||
uid: number | bigint; | ||
uid: T; | ||
/** | ||
* the id of the group that owns the file | ||
*/ | ||
gid: number | bigint; | ||
gid: T; | ||
/** | ||
* the ino | ||
*/ | ||
ino: number | bigint; | ||
ino: T; | ||
} | ||
@@ -60,0 +60,0 @@ |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
104
2
1841374
20281