@poe-platform/safe-fs 0.1.69 → 0.1.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/safe-fs/contracts/filesystem.d.ts +11 -1
- package/dist/safe-fs/fs/directory-admission.d.ts +3 -0
- package/dist/safe-fs/fs/directory-admission.js +14 -0
- package/dist/safe-fs/fs/memory/index.d.ts +2 -2
- package/dist/safe-fs/fs/memory/index.js +3 -1
- package/dist/safe-fs/fs/mount/index.d.ts +2 -2
- package/dist/safe-fs/fs/mount/index.js +11 -1
- package/dist/safe-fs/fs/overlay/index.d.ts +2 -2
- package/dist/safe-fs/fs/overlay/index.js +20 -6
- package/dist/safe-fs/fs/readonly/index.d.ts +2 -2
- package/dist/safe-fs/fs/readonly/index.js +7 -1
- package/dist/safe-fs/fs/real/index.d.ts +2 -2
- package/dist/safe-fs/fs/real/index.js +27 -2
- package/dist/safe-fs/fs/s3/filesystem.d.ts +2 -2
- package/dist/safe-fs/fs/s3/filesystem.js +11 -1
- package/dist/safe-fs/fs/webdav/webdav.d.ts +2 -2
- package/dist/safe-fs/fs/webdav/webdav.js +13 -2
- package/package.json +1 -1
|
@@ -61,6 +61,16 @@ export interface FsOptions {
|
|
|
61
61
|
export interface ReadFileOptions extends FsOptions {
|
|
62
62
|
readonly maxBytes?: number;
|
|
63
63
|
}
|
|
64
|
+
export interface ReadDirectoryOptions extends FsOptions {
|
|
65
|
+
/**
|
|
66
|
+
* Optional per-listing entry admission limit, a nonnegative safe integer.
|
|
67
|
+
* Zero permits an empty listing; overflow rejects with EFBIG, never truncates.
|
|
68
|
+
* Omission preserves the adapter's existing limits and ordering. Composed
|
|
69
|
+
* adapters may conservatively count distinct candidates before visibility
|
|
70
|
+
* filtering. This is not a global traversal, host-allocation or work quota.
|
|
71
|
+
*/
|
|
72
|
+
readonly maxEntries?: number;
|
|
73
|
+
}
|
|
64
74
|
export interface WriteFileOptions extends FsOptions {
|
|
65
75
|
readonly flag?: "w" | "wx" | "a" | "ax";
|
|
66
76
|
readonly mode?: number;
|
|
@@ -93,7 +103,7 @@ export interface FileSystem {
|
|
|
93
103
|
stat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
94
104
|
lstat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
95
105
|
compareEntry?(path: string, peer: FileSystem, peerPath: string, options?: FsOptions): Promise<EntryComparison>;
|
|
96
|
-
readdir(path: string, options?:
|
|
106
|
+
readdir(path: string, options?: ReadDirectoryOptions): Promise<DirectoryEntry[]>;
|
|
97
107
|
mkdir(path: string, options?: MkdirOptions): Promise<void>;
|
|
98
108
|
rm(path: string, options?: RemoveOptions): Promise<void>;
|
|
99
109
|
rmdir?(path: string, options?: FsOptions): Promise<void>;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ReadDirectoryOptions } from "../contracts/filesystem.js";
|
|
2
|
+
export declare function directoryEntryLimit(options: ReadDirectoryOptions, path: string): number | undefined;
|
|
3
|
+
export declare function admitDirectoryEntries(count: number, limit: number | undefined, path: string): void;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { FsError } from "../contracts/errors.js";
|
|
2
|
+
export function directoryEntryLimit(options, path) {
|
|
3
|
+
options.signal?.throwIfAborted();
|
|
4
|
+
const limit = options.maxEntries;
|
|
5
|
+
if (limit !== undefined && (!Number.isSafeInteger(limit) || limit < 0)) {
|
|
6
|
+
throw new FsError("EINVAL", { syscall: "readdir", path, message: "directory entry limit must be a nonnegative safe integer" });
|
|
7
|
+
}
|
|
8
|
+
return limit;
|
|
9
|
+
}
|
|
10
|
+
export function admitDirectoryEntries(count, limit, path) {
|
|
11
|
+
if (limit !== undefined && count > limit) {
|
|
12
|
+
throw new FsError("EFBIG", { syscall: "readdir", path, message: "directory entry limit exceeded" });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, EntryComparison, FileStat, FileSystem, FsOptions, MkdirOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
|
|
1
|
+
import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, EntryComparison, FileStat, FileSystem, FsOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
|
|
2
2
|
import type { ByteSource } from "../../contracts/io.js";
|
|
3
3
|
export declare class MemoryFileSystem implements FileSystem {
|
|
4
4
|
readonly capabilities: Readonly<{
|
|
@@ -60,7 +60,7 @@ export declare class MemoryFileSystem implements FileSystem {
|
|
|
60
60
|
appendFile(path: string, data: Uint8Array, options?: AppendFileOptions): Promise<void>;
|
|
61
61
|
stat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
62
62
|
lstat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
63
|
-
readdir(path: string, options?:
|
|
63
|
+
readdir(path: string, options?: ReadDirectoryOptions): Promise<DirectoryEntry[]>;
|
|
64
64
|
mkdir(path: string, options?: MkdirOptions): Promise<void>;
|
|
65
65
|
rmdir(path: string, options?: FsOptions): Promise<void>;
|
|
66
66
|
rm(path: string, options?: RemoveOptions): Promise<void>;
|
|
@@ -2,6 +2,7 @@ import { FsError } from "../../contracts/errors.js";
|
|
|
2
2
|
import { assertCallbackAuthorityAllowed, compareEntries, registerEntryAuthority } from "../mount/comparison.js";
|
|
3
3
|
import { getOwnedS3Entry } from "../s3/registry.js";
|
|
4
4
|
import { getOwnedWebDavEntry } from "../webdav/resource-id.js";
|
|
5
|
+
import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
|
|
5
6
|
const typeModes = { file: 0o100000, directory: 0o040000, symlink: 0o120000 };
|
|
6
7
|
const ownedStats = new WeakMap();
|
|
7
8
|
const ownedStores = new WeakMap();
|
|
@@ -285,11 +286,12 @@ export class MemoryFileSystem {
|
|
|
285
286
|
return stat;
|
|
286
287
|
}
|
|
287
288
|
async readdir(path, options = {}) {
|
|
288
|
-
options
|
|
289
|
+
const limit = directoryEntryLimit(options, path);
|
|
289
290
|
const node = this.resolve(path, "readdir").node;
|
|
290
291
|
if (node.type !== "directory")
|
|
291
292
|
this.fail("ENOTDIR", "readdir", path);
|
|
292
293
|
this.permission(node, 4, "readdir", path);
|
|
294
|
+
admitDirectoryEntries(node.entries.size, limit, path);
|
|
293
295
|
node.atimeMs = Date.now();
|
|
294
296
|
return [...node.entries].map(([name, entry]) => ({ name, type: entry.type }))
|
|
295
297
|
.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, FileStat, FileSystem, FileSystemCapabilities, FsOptions, MkdirOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
|
|
1
|
+
import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, FileStat, FileSystem, FileSystemCapabilities, FsOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
|
|
2
2
|
import type { ByteSource } from "../../contracts/io.js";
|
|
3
3
|
export interface MountFileSystemOptions {
|
|
4
4
|
readonly root: FileSystem;
|
|
@@ -24,7 +24,7 @@ export declare class MountFileSystem implements FileSystem {
|
|
|
24
24
|
appendFile(path: string, data: Uint8Array, options?: AppendFileOptions): Promise<void>;
|
|
25
25
|
stat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
26
26
|
lstat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
27
|
-
readdir(path: string, options?:
|
|
27
|
+
readdir(path: string, options?: ReadDirectoryOptions): Promise<DirectoryEntry[]>;
|
|
28
28
|
mkdir(path: string, options?: MkdirOptions): Promise<void>;
|
|
29
29
|
rmdir(path: string, options?: FsOptions): Promise<void>;
|
|
30
30
|
rm(path: string, options?: RemoveOptions): Promise<void>;
|
|
@@ -2,6 +2,7 @@ import { FsError, isFsError, toFsError } from "../../contracts/errors.js";
|
|
|
2
2
|
import { readBytes } from "../../contracts/io.js";
|
|
3
3
|
import { finishCleanup } from "../../contracts/cleanup.js";
|
|
4
4
|
import { readOnlyCapabilities } from "../capabilities.js";
|
|
5
|
+
import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
|
|
5
6
|
import { normalizePath, validatePath } from "../../contracts/virtual-path.js";
|
|
6
7
|
import { compareIdentity } from "./identity.js";
|
|
7
8
|
import { compareEntries, registerEntryAuthority, registerEntryView } from "./comparison.js";
|
|
@@ -338,15 +339,22 @@ export class MountFileSystem {
|
|
|
338
339
|
}
|
|
339
340
|
readdir(path, options = {}) {
|
|
340
341
|
return this.operation("readdir", path, options, async () => {
|
|
342
|
+
const limit = directoryEntryLimit(options, path);
|
|
341
343
|
const location = await this.resolve(path, options);
|
|
342
344
|
if (location.stat?.type !== "directory")
|
|
343
345
|
fail("ENOTDIR");
|
|
344
346
|
const entries = new Map();
|
|
345
347
|
if (!location.synthetic) {
|
|
346
|
-
|
|
348
|
+
const children = await location.mount.backend.readdir(location.local, options);
|
|
349
|
+
options.signal?.throwIfAborted();
|
|
350
|
+
admitDirectoryEntries(children.length, limit, path);
|
|
351
|
+
for (const entry of children) {
|
|
352
|
+
options.signal?.throwIfAborted();
|
|
347
353
|
const { name, type } = entry;
|
|
348
354
|
if (!name || name === "." || name === ".." || name.includes("/") || name.includes("\0"))
|
|
349
355
|
fail("EIO");
|
|
356
|
+
if (!entries.has(name))
|
|
357
|
+
admitDirectoryEntries(entries.size + 1, limit, path);
|
|
350
358
|
entries.set(name, { name, type });
|
|
351
359
|
}
|
|
352
360
|
}
|
|
@@ -354,6 +362,8 @@ export class MountFileSystem {
|
|
|
354
362
|
if (mount.path !== location.path && within(location.path, mount.path)) {
|
|
355
363
|
const suffix = mount.path.slice(location.path === "/" ? 1 : location.path.length + 1);
|
|
356
364
|
const name = suffix.split("/")[0];
|
|
365
|
+
if (!entries.has(name))
|
|
366
|
+
admitDirectoryEntries(entries.size + 1, limit, path);
|
|
357
367
|
entries.set(name, { name, type: "directory" });
|
|
358
368
|
}
|
|
359
369
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ByteSource } from "../../contracts/io.js";
|
|
2
|
-
import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, FileStat, FileSystem, FileSystemCapabilities, FsOptions, MkdirOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
|
|
2
|
+
import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, FileStat, FileSystem, FileSystemCapabilities, FsOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
|
|
3
3
|
export interface OverlayFileSystemOptions {
|
|
4
4
|
readonly upper: FileSystem;
|
|
5
5
|
readonly lower: FileSystem;
|
|
@@ -49,7 +49,7 @@ export declare class OverlayFileSystem implements FileSystem {
|
|
|
49
49
|
stat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
50
50
|
lstat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
51
51
|
compareEntry(path: string, peer: FileSystem, peerPath: string, options?: FsOptions): Promise<import("../../contracts/filesystem.js").EntryComparison>;
|
|
52
|
-
readdir(path: string, options?:
|
|
52
|
+
readdir(path: string, options?: ReadDirectoryOptions): Promise<DirectoryEntry[]>;
|
|
53
53
|
realpath(path: string, options?: FsOptions): Promise<string>;
|
|
54
54
|
access(path: string, requestedMode?: number, options?: FsOptions): Promise<void>;
|
|
55
55
|
mkdir(path: string, options?: MkdirOptions): Promise<void>;
|
|
@@ -6,6 +6,7 @@ import { FsError, toFsError } from "../../contracts/errors.js";
|
|
|
6
6
|
import { dirname, isPathWithin, normalizePath, validatePath } from "../../contracts/virtual-path.js";
|
|
7
7
|
import { compareIdentity } from "../mount/identity.js";
|
|
8
8
|
import { compareEntries, registerEntryView } from "../mount/comparison.js";
|
|
9
|
+
import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
|
|
9
10
|
function snapshotStat(stat) {
|
|
10
11
|
const { type, size, allocatedBytes, mode, mtimeMs, atimeMs, ctimeMs, birthtimeMs, identityScope, ino, dev, nlink, uid, gid } = stat;
|
|
11
12
|
return {
|
|
@@ -343,21 +344,33 @@ export class OverlayFileSystem {
|
|
|
343
344
|
async required(path, options, followFinal = true) {
|
|
344
345
|
return (await this.resolve(path, options, followFinal)).entry;
|
|
345
346
|
}
|
|
346
|
-
async listing(entry, options) {
|
|
347
|
+
async listing(entry, options, limit) {
|
|
347
348
|
if (entry.stat.type !== "directory")
|
|
348
349
|
fail("ENOTDIR", entry.path);
|
|
349
350
|
this.permission(entry, 4);
|
|
350
351
|
const names = new Set();
|
|
352
|
+
const admit = (name) => {
|
|
353
|
+
options.signal?.throwIfAborted();
|
|
354
|
+
if (!names.has(name))
|
|
355
|
+
admitDirectoryEntries(names.size + 1, limit, entry.path);
|
|
356
|
+
names.add(name);
|
|
357
|
+
};
|
|
351
358
|
const upper = await this.maybeStat(this.#upper, entry.path, options);
|
|
352
359
|
if (upper?.type === "directory") {
|
|
353
|
-
|
|
354
|
-
|
|
360
|
+
const children = await this.#upper.readdir(entry.path, options);
|
|
361
|
+
options.signal?.throwIfAborted();
|
|
362
|
+
admitDirectoryEntries(children.length, limit, entry.path);
|
|
363
|
+
for (const child of children)
|
|
364
|
+
admit(child.name);
|
|
355
365
|
}
|
|
356
366
|
if (await this.lowerVisible(entry.path, options) && !this.opaque.has(entry.path)) {
|
|
357
367
|
const lower = await this.maybeStat(this.#lower, entry.path, options);
|
|
358
368
|
if (lower?.type === "directory") {
|
|
359
|
-
|
|
360
|
-
|
|
369
|
+
const children = await this.#lower.readdir(entry.path, options);
|
|
370
|
+
options.signal?.throwIfAborted();
|
|
371
|
+
admitDirectoryEntries(children.length, limit, entry.path);
|
|
372
|
+
for (const child of children)
|
|
373
|
+
admit(child.name);
|
|
361
374
|
}
|
|
362
375
|
}
|
|
363
376
|
const entries = [];
|
|
@@ -572,7 +585,8 @@ export class OverlayFileSystem {
|
|
|
572
585
|
return compareEntries(this, path, peer, peerPath, options);
|
|
573
586
|
}
|
|
574
587
|
async readdir(path, options = {}) {
|
|
575
|
-
|
|
588
|
+
const limit = directoryEntryLimit(options, path);
|
|
589
|
+
return this.run(options, async () => this.listing(await this.required(path, options), options, limit), false);
|
|
576
590
|
}
|
|
577
591
|
async realpath(path, options = {}) {
|
|
578
592
|
return this.run(options, async () => (await this.required(path, options)).path, false);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ByteSource } from "../../contracts/io.js";
|
|
2
|
-
import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, FileStat, FileSystem, FileSystemCapabilities, FsOptions, MkdirOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
|
|
2
|
+
import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, FileStat, FileSystem, FileSystemCapabilities, FsOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
|
|
3
3
|
export declare class ReadOnlyFileSystem implements FileSystem {
|
|
4
4
|
#private;
|
|
5
5
|
constructor(filesystem: FileSystem);
|
|
@@ -9,7 +9,7 @@ export declare class ReadOnlyFileSystem implements FileSystem {
|
|
|
9
9
|
stat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
10
10
|
lstat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
11
11
|
compareEntry(path: string, peer: FileSystem, peerPath: string, options?: FsOptions): Promise<import("../../contracts/filesystem.js").EntryComparison>;
|
|
12
|
-
readdir(path: string, options?:
|
|
12
|
+
readdir(path: string, options?: ReadDirectoryOptions): Promise<DirectoryEntry[]>;
|
|
13
13
|
realpath(path: string, options?: FsOptions): Promise<string>;
|
|
14
14
|
access(path: string, mode?: number, options?: FsOptions): Promise<void>;
|
|
15
15
|
readlink(path: string, options?: FsOptions): Promise<string>;
|
|
@@ -3,6 +3,7 @@ import { FsError } from "../../contracts/errors.js";
|
|
|
3
3
|
import { readBytes } from "../../contracts/io.js";
|
|
4
4
|
import { compareEntries, registerEntryView } from "../mount/comparison.js";
|
|
5
5
|
import { readOnlyCapabilities } from "../capabilities.js";
|
|
6
|
+
import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
|
|
6
7
|
function readOnly(syscall, path, dest) {
|
|
7
8
|
throw new FsError("EROFS", { syscall, path, ...(dest === undefined ? {} : { dest }) });
|
|
8
9
|
}
|
|
@@ -60,7 +61,12 @@ export class ReadOnlyFileSystem {
|
|
|
60
61
|
return compareEntries(this, path, peer, peerPath, options);
|
|
61
62
|
}
|
|
62
63
|
async readdir(path, options) {
|
|
63
|
-
|
|
64
|
+
const limit = options?.maxEntries === undefined ? undefined : directoryEntryLimit(options, path);
|
|
65
|
+
const entries = await this.#filesystem.readdir(path, options);
|
|
66
|
+
if (limit !== undefined)
|
|
67
|
+
options?.signal?.throwIfAborted();
|
|
68
|
+
admitDirectoryEntries(entries.length, limit, path);
|
|
69
|
+
return entries.map((entry) => ({ name: entry.name, type: entry.type }));
|
|
64
70
|
}
|
|
65
71
|
async realpath(path, options) {
|
|
66
72
|
return this.#filesystem.realpath(path, options);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AppendFileOptions, ByteSource, CopyFileOptions, DirectoryEntry, FileStat, FileSystem, FileSystemCapabilities, FsOptions, MkdirOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/index.js";
|
|
1
|
+
import type { AppendFileOptions, ByteSource, CopyFileOptions, DirectoryEntry, FileStat, FileSystem, FileSystemCapabilities, FsOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/index.js";
|
|
2
2
|
export interface RealFileSystemOptions {
|
|
3
3
|
/** An existing, absolute host directory. It is never created implicitly. */
|
|
4
4
|
readonly root: string;
|
|
@@ -55,7 +55,7 @@ export declare class RealFileSystem implements FileSystem {
|
|
|
55
55
|
appendFile(path: string, data: Uint8Array, options?: AppendFileOptions): Promise<void>;
|
|
56
56
|
stat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
57
57
|
lstat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
58
|
-
readdir(path: string, options?:
|
|
58
|
+
readdir(path: string, options?: ReadDirectoryOptions): Promise<DirectoryEntry[]>;
|
|
59
59
|
mkdir(path: string, options?: MkdirOptions): Promise<void>;
|
|
60
60
|
rmdir(path: string, options?: FsOptions): Promise<void>;
|
|
61
61
|
rm(path: string, options?: RemoveOptions): Promise<void>;
|
|
@@ -3,6 +3,7 @@ import * as native from "node:fs/promises";
|
|
|
3
3
|
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { nativeAllocatedBytes } from "./allocation.js";
|
|
5
5
|
import { finishCleanup } from "../../contracts/cleanup.js";
|
|
6
|
+
import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
|
|
6
7
|
import { FsError, collectBytes, isErrnoCode, toByteSource, toFsError, validatePath, } from "../../contracts/index.js";
|
|
7
8
|
function fileType(stats) {
|
|
8
9
|
if (stats.isFile())
|
|
@@ -273,11 +274,35 @@ export class RealFileSystem {
|
|
|
273
274
|
});
|
|
274
275
|
}
|
|
275
276
|
async readdir(path, options = {}) {
|
|
277
|
+
const limit = directoryEntryLimit(options, path);
|
|
276
278
|
return this.operation("readdir", path, options, async () => {
|
|
277
279
|
const target = await this.path(path, options);
|
|
278
280
|
options.signal?.throwIfAborted();
|
|
279
|
-
|
|
280
|
-
|
|
281
|
+
if (limit === undefined) {
|
|
282
|
+
const entries = await native.readdir(target, { withFileTypes: true });
|
|
283
|
+
return entries.map((entry) => ({ name: entry.name, type: fileType(entry) }));
|
|
284
|
+
}
|
|
285
|
+
const handle = await native.opendir(target, { bufferSize: 1 });
|
|
286
|
+
let failed = false;
|
|
287
|
+
try {
|
|
288
|
+
const entries = [];
|
|
289
|
+
while (true) {
|
|
290
|
+
options.signal?.throwIfAborted();
|
|
291
|
+
const entry = await handle.read();
|
|
292
|
+
options.signal?.throwIfAborted();
|
|
293
|
+
if (entry === null)
|
|
294
|
+
return entries;
|
|
295
|
+
admitDirectoryEntries(entries.length + 1, limit, path);
|
|
296
|
+
entries.push({ name: entry.name, type: fileType(entry) });
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
failed = true;
|
|
301
|
+
throw error;
|
|
302
|
+
}
|
|
303
|
+
finally {
|
|
304
|
+
await finishCleanup(() => handle.close(), failed);
|
|
305
|
+
}
|
|
281
306
|
});
|
|
282
307
|
}
|
|
283
308
|
async mkdir(path, options = {}) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { FsError } from "../../contracts/errors.js";
|
|
2
|
-
import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, EntryComparison, FileStat, FileSystem, FsOptions, MkdirOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
|
|
2
|
+
import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, EntryComparison, FileStat, FileSystem, FsOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
|
|
3
3
|
import type { ByteSource } from "../../contracts/io.js";
|
|
4
4
|
import type { S3Transport } from "./transport.js";
|
|
5
5
|
export interface S3FileSystemOptions {
|
|
@@ -93,7 +93,7 @@ export declare class S3FileSystem implements FileSystem {
|
|
|
93
93
|
private writeMetadata;
|
|
94
94
|
appendFile(path: string, data: Uint8Array, options?: AppendFileOptions): Promise<void>;
|
|
95
95
|
mkdir(input: string, options?: MkdirOptions): Promise<void>;
|
|
96
|
-
readdir(input: string, options?:
|
|
96
|
+
readdir(input: string, options?: ReadDirectoryOptions): Promise<DirectoryEntry[]>;
|
|
97
97
|
private tree;
|
|
98
98
|
rmdir(input: string, options?: FsOptions): Promise<void>;
|
|
99
99
|
rm(input: string, options?: RemoveOptions): Promise<void>;
|
|
@@ -5,6 +5,7 @@ import { collectBytes, readBytes } from "../../contracts/io.js";
|
|
|
5
5
|
import { compareEntries, registerEntryAuthority } from "../mount/comparison.js";
|
|
6
6
|
import { compareOwnedS3Entries, queryS3Head, recordS3Stat, registerS3EntryOwner } from "./authority.js";
|
|
7
7
|
import { encodeCopySource } from "./transport.js";
|
|
8
|
+
import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
|
|
8
9
|
export class S3RenameError extends FsError {
|
|
9
10
|
phase;
|
|
10
11
|
copiedKeys;
|
|
@@ -495,20 +496,29 @@ export class S3FileSystem {
|
|
|
495
496
|
}, this.requestOptions(options)), "EEXIST");
|
|
496
497
|
}
|
|
497
498
|
async readdir(input, options = {}) {
|
|
499
|
+
let limit;
|
|
500
|
+
if (options.maxEntries !== undefined) {
|
|
501
|
+
this.checkAbort(options, "readdir", input);
|
|
502
|
+
limit = directoryEntryLimit(options, input);
|
|
503
|
+
}
|
|
498
504
|
const path = this.path(input);
|
|
499
505
|
if ((await this.stat(input, options)).type !== "directory")
|
|
500
506
|
fail("ENOTDIR", "readdir", path);
|
|
501
507
|
const prefix = this.directoryKey(path);
|
|
502
508
|
const entries = new Map();
|
|
503
509
|
const add = (name, type) => {
|
|
510
|
+
this.checkAbort(options, "readdir", path);
|
|
504
511
|
if (!name || name.includes("/"))
|
|
505
512
|
fail("EIO", "readdir", path, "invalid direct child in delimited listing");
|
|
506
513
|
const existing = entries.get(name);
|
|
507
514
|
if (existing && existing.type !== type)
|
|
508
515
|
this.unsupported("file/prefix collisions", path);
|
|
516
|
+
if (!existing)
|
|
517
|
+
admitDirectoryEntries(entries.size + 1, limit, path);
|
|
509
518
|
entries.set(name, { name, type });
|
|
510
519
|
};
|
|
511
|
-
|
|
520
|
+
const maxKeys = limit === undefined ? this.pageSize : Math.min(this.pageSize, limit + 1);
|
|
521
|
+
for await (const page of this.pages(prefix, path, options, "/", maxKeys)) {
|
|
512
522
|
for (const item of page.Contents ?? []) {
|
|
513
523
|
if (item.Key === prefix)
|
|
514
524
|
continue;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { PlatformComparisonCallback } from "#safe-fs-platform";
|
|
2
2
|
import type { ByteSource } from "../../contracts/io.js";
|
|
3
|
-
import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, EntryComparison, FileStat, FileSystem, FsOptions, MkdirOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
|
|
3
|
+
import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, EntryComparison, FileStat, FileSystem, FsOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
|
|
4
4
|
export type WebDavFetch = (url: string, init: RequestInit) => Promise<Response>;
|
|
5
5
|
export interface WebDavAtomicEmptyDirectoryRequest {
|
|
6
6
|
readonly operation: "atomic-empty-rmdir/v1";
|
|
@@ -100,7 +100,7 @@ export declare class WebDavFileSystem implements FileSystem {
|
|
|
100
100
|
stat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
101
101
|
private statWithAncestors;
|
|
102
102
|
lstat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
103
|
-
readdir(path: string, options?:
|
|
103
|
+
readdir(path: string, options?: ReadDirectoryOptions): Promise<DirectoryEntry[]>;
|
|
104
104
|
readFile(path: string, options?: ReadFileOptions): Promise<Uint8Array>;
|
|
105
105
|
readStream(path: string, options?: ReadStreamOptions): AsyncGenerator<Uint8Array>;
|
|
106
106
|
private prepareWrite;
|
|
@@ -2,6 +2,7 @@ import { FsError, isErrnoCode, isFsError } from "../../contracts/errors.js";
|
|
|
2
2
|
import { composeAbortSignals } from "../../contracts/abort.js";
|
|
3
3
|
import { readBytes } from "../../contracts/io.js";
|
|
4
4
|
import { davChild, davChildren, parseXml, scalar } from "./xml.js";
|
|
5
|
+
import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
|
|
5
6
|
import { assertCallbackAuthorityAllowed, compareEntries, registerEntryAuthority } from "../mount/comparison.js";
|
|
6
7
|
import { compareWebDavResources, ownedResponseIdentifier, recordOwnedResourceStat, registerResourceQuery, resourceIdentifier } from "./resource-id.js";
|
|
7
8
|
const timestampNamespace = "urn:virtual-bash:metadata";
|
|
@@ -616,14 +617,16 @@ export class WebDavFileSystem {
|
|
|
616
617
|
this.etags.set(stat, validator);
|
|
617
618
|
return stat;
|
|
618
619
|
}
|
|
619
|
-
async entries(path, depth, options, collection = depth === "1") {
|
|
620
|
+
async entries(path, depth, options, collection = depth === "1", limit) {
|
|
620
621
|
return this.request("PROPFIND", path, options, {
|
|
621
622
|
headers: { Depth: depth, "Content-Type": "application/xml; charset=utf-8" }, body: propfindBody,
|
|
622
623
|
}, async (response, signal) => {
|
|
623
624
|
if (response.status !== 207)
|
|
624
625
|
this.httpError(response.status, "PROPFIND", path);
|
|
625
626
|
const result = new Map();
|
|
627
|
+
let children = 0;
|
|
626
628
|
for (const element of await this.multistatus(response, signal)) {
|
|
629
|
+
signal.throwIfAborted();
|
|
627
630
|
const href = davChild(element, "href");
|
|
628
631
|
if (!href)
|
|
629
632
|
throw new Error("missing DAV:href");
|
|
@@ -634,6 +637,8 @@ export class WebDavFileSystem {
|
|
|
634
637
|
}
|
|
635
638
|
if (result.has(member))
|
|
636
639
|
throw new Error("duplicate response href");
|
|
640
|
+
if (member !== path)
|
|
641
|
+
admitDirectoryEntries(++children, limit, path);
|
|
637
642
|
const stat = this.entryStat(element, member);
|
|
638
643
|
recordOwnedResourceStat(response, this, member, stat);
|
|
639
644
|
result.set(member, stat);
|
|
@@ -730,8 +735,14 @@ export class WebDavFileSystem {
|
|
|
730
735
|
return this.stat(path, options);
|
|
731
736
|
}
|
|
732
737
|
async readdir(path, options = {}) {
|
|
738
|
+
let limit;
|
|
739
|
+
if (options.maxEntries !== undefined) {
|
|
740
|
+
if (options.signal?.aborted)
|
|
741
|
+
fail("ECANCELED", "readdir", path);
|
|
742
|
+
limit = directoryEntryLimit(options, path);
|
|
743
|
+
}
|
|
733
744
|
const normalized = normalize(path);
|
|
734
|
-
const entries = await this.entries(normalized, "1", options);
|
|
745
|
+
const entries = await this.entries(normalized, "1", options, true, limit);
|
|
735
746
|
if (entries.get(normalized).type !== "directory")
|
|
736
747
|
fail("ENOTDIR", "readdir", path);
|
|
737
748
|
return [...entries].filter(([member]) => member !== normalized)
|