@poe-platform/safe-fs 0.1.53 → 0.1.55
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 +22 -0
- package/dist/safe-fs/fs/capabilities.d.ts +4 -0
- package/dist/safe-fs/fs/capabilities.js +25 -0
- package/dist/safe-fs/fs/memory/index.d.ts +22 -0
- package/dist/safe-fs/fs/memory/index.js +5 -0
- package/dist/safe-fs/fs/mount/index.d.ts +1 -0
- package/dist/safe-fs/fs/mount/index.js +41 -4
- package/dist/safe-fs/fs/overlay/index.js +30 -3
- package/dist/safe-fs/fs/quota/index.js +5 -0
- package/dist/safe-fs/fs/readonly/index.d.ts +1 -0
- package/dist/safe-fs/fs/readonly/index.js +7 -1
- package/dist/safe-fs/fs/real/index.js +5 -0
- package/dist/safe-fs/fs/s3/filesystem.d.ts +22 -0
- package/dist/safe-fs/fs/s3/filesystem.js +12 -0
- package/dist/safe-fs/fs/webdav/webdav.d.ts +25 -0
- package/dist/safe-fs/fs/webdav/webdav.js +86 -52
- package/package.json +1 -1
|
@@ -23,7 +23,28 @@ export interface DirectoryEntry {
|
|
|
23
23
|
}
|
|
24
24
|
export interface FileSystemCapabilities {
|
|
25
25
|
readonly readOnly?: boolean;
|
|
26
|
+
readonly read?: boolean;
|
|
27
|
+
readonly stat?: boolean;
|
|
28
|
+
readonly readdir?: boolean;
|
|
29
|
+
readonly realpath?: boolean;
|
|
30
|
+
readonly access?: boolean;
|
|
31
|
+
readonly write?: boolean;
|
|
26
32
|
readonly append?: boolean;
|
|
33
|
+
readonly exclusiveCreate?: boolean;
|
|
34
|
+
readonly explicitDirectories?: boolean;
|
|
35
|
+
readonly implicitDirectories?: boolean;
|
|
36
|
+
readonly mkdir?: boolean;
|
|
37
|
+
readonly recursiveMkdir?: boolean;
|
|
38
|
+
readonly remove?: boolean;
|
|
39
|
+
readonly removeDirectory?: boolean;
|
|
40
|
+
readonly recursiveRemove?: boolean;
|
|
41
|
+
readonly rename?: boolean;
|
|
42
|
+
readonly copy?: boolean;
|
|
43
|
+
readonly exclusiveCopy?: boolean;
|
|
44
|
+
readonly readlink?: boolean;
|
|
45
|
+
readonly truncate?: boolean;
|
|
46
|
+
readonly streamingAppend?: boolean;
|
|
47
|
+
readonly randomAccessWrite?: boolean;
|
|
27
48
|
readonly symlinks?: boolean;
|
|
28
49
|
readonly hardlinks?: boolean;
|
|
29
50
|
readonly permissions?: boolean;
|
|
@@ -65,6 +86,7 @@ export interface ReadStreamOptions extends FsOptions {
|
|
|
65
86
|
}
|
|
66
87
|
export interface FileSystem {
|
|
67
88
|
readonly capabilities: FileSystemCapabilities;
|
|
89
|
+
capabilitiesFor?(path: string, options?: FsOptions): Promise<FileSystemCapabilities>;
|
|
68
90
|
readFile(path: string, options?: ReadFileOptions): Promise<Uint8Array>;
|
|
69
91
|
writeFile(path: string, data: Uint8Array, options?: WriteFileOptions): Promise<void>;
|
|
70
92
|
appendFile(path: string, data: Uint8Array, options?: AppendFileOptions): Promise<void>;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { FileSystemCapabilities } from "../contracts/filesystem.js";
|
|
2
|
+
export declare function requireCapabilities(...values: readonly (boolean | undefined)[]): boolean | undefined;
|
|
3
|
+
export declare function readOnlyCapabilities(capabilities: FileSystemCapabilities): FileSystemCapabilities;
|
|
4
|
+
export declare function quotaCapabilities(capabilities: FileSystemCapabilities): FileSystemCapabilities;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function requireCapabilities(...values) {
|
|
2
|
+
return values.some(value => value === false) ? false : values.every(value => value === true) ? true : undefined;
|
|
3
|
+
}
|
|
4
|
+
export function readOnlyCapabilities(capabilities) {
|
|
5
|
+
const inspection = Object.fromEntries([
|
|
6
|
+
"read", "stat", "readdir", "realpath", "access", "readlink", "explicitDirectories", "implicitDirectories",
|
|
7
|
+
"symlinks", "streamingRead",
|
|
8
|
+
].filter(name => capabilities[name] !== undefined).map(name => [name, capabilities[name]]));
|
|
9
|
+
return Object.freeze({
|
|
10
|
+
...inspection, readOnly: true, write: false, append: false, exclusiveCreate: false,
|
|
11
|
+
mkdir: false, recursiveMkdir: false, remove: false, removeDirectory: false, recursiveRemove: false,
|
|
12
|
+
rename: false, copy: false, exclusiveCopy: false, truncate: false, streamingAppend: false,
|
|
13
|
+
randomAccessWrite: false, hardlinks: false, permissions: false, timestamps: false,
|
|
14
|
+
atomicRename: false, streamingWrite: false,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
export function quotaCapabilities(capabilities) {
|
|
18
|
+
const streamingWrite = requireCapabilities(capabilities.write, capabilities.append, !capabilities.readOnly);
|
|
19
|
+
const streamingAppend = requireCapabilities(capabilities.append, !capabilities.readOnly);
|
|
20
|
+
const { streamingWrite: ignoredWrite, streamingAppend: ignoredAppend, ...rest } = capabilities;
|
|
21
|
+
return Object.freeze({ ...rest,
|
|
22
|
+
...(streamingWrite === undefined ? {} : { streamingWrite }),
|
|
23
|
+
...(streamingAppend === undefined ? {} : { streamingAppend }),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -2,6 +2,28 @@ import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, EntryCompariso
|
|
|
2
2
|
import type { ByteSource } from "../../contracts/io.js";
|
|
3
3
|
export declare class MemoryFileSystem implements FileSystem {
|
|
4
4
|
readonly capabilities: Readonly<{
|
|
5
|
+
read: true;
|
|
6
|
+
stat: true;
|
|
7
|
+
readdir: true;
|
|
8
|
+
realpath: true;
|
|
9
|
+
access: true;
|
|
10
|
+
write: true;
|
|
11
|
+
append: true;
|
|
12
|
+
exclusiveCreate: true;
|
|
13
|
+
explicitDirectories: true;
|
|
14
|
+
implicitDirectories: false;
|
|
15
|
+
mkdir: true;
|
|
16
|
+
recursiveMkdir: true;
|
|
17
|
+
remove: true;
|
|
18
|
+
removeDirectory: true;
|
|
19
|
+
recursiveRemove: true;
|
|
20
|
+
rename: true;
|
|
21
|
+
copy: true;
|
|
22
|
+
exclusiveCopy: true;
|
|
23
|
+
readlink: true;
|
|
24
|
+
truncate: true;
|
|
25
|
+
streamingAppend: true;
|
|
26
|
+
randomAccessWrite: true;
|
|
5
27
|
readOnly: false;
|
|
6
28
|
symlinks: true;
|
|
7
29
|
hardlinks: true;
|
|
@@ -48,6 +48,11 @@ const compareOwnedMemory = async (own, peer, options) => {
|
|
|
48
48
|
};
|
|
49
49
|
export class MemoryFileSystem {
|
|
50
50
|
capabilities = Object.freeze({
|
|
51
|
+
read: true, stat: true, readdir: true, realpath: true, access: true,
|
|
52
|
+
write: true, append: true, exclusiveCreate: true, explicitDirectories: true, implicitDirectories: false,
|
|
53
|
+
mkdir: true, recursiveMkdir: true, remove: true, removeDirectory: true, recursiveRemove: true,
|
|
54
|
+
rename: true, copy: true, exclusiveCopy: true, readlink: true, truncate: true,
|
|
55
|
+
streamingAppend: true, randomAccessWrite: true,
|
|
51
56
|
readOnly: false,
|
|
52
57
|
symlinks: true,
|
|
53
58
|
hardlinks: true,
|
|
@@ -9,6 +9,7 @@ export declare class MountFileSystem implements FileSystem {
|
|
|
9
9
|
private readonly mounts;
|
|
10
10
|
constructor(options: MountFileSystemOptions);
|
|
11
11
|
private select;
|
|
12
|
+
capabilitiesFor(path: string, options?: FsOptions): Promise<FileSystemCapabilities>;
|
|
12
13
|
private protected;
|
|
13
14
|
private error;
|
|
14
15
|
private operation;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
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
|
+
import { readOnlyCapabilities } from "../capabilities.js";
|
|
4
5
|
import { normalizePath, validatePath } from "../../contracts/virtual-path.js";
|
|
5
6
|
import { compareIdentity } from "./identity.js";
|
|
6
7
|
import { compareEntries, registerEntryAuthority, registerEntryView } from "./comparison.js";
|
|
@@ -79,14 +80,32 @@ export class MountFileSystem {
|
|
|
79
80
|
const streamingWrite = streaming("streamingWrite", "writeStream");
|
|
80
81
|
const append = all("append") ? true
|
|
81
82
|
: mounts.every(({ backend }) => backend.capabilities.append === false) ? false : undefined;
|
|
83
|
+
const common = (capability) => {
|
|
84
|
+
const optional = {
|
|
85
|
+
symlinks: ["symlink", "readlink"], hardlinks: ["link"], permissions: ["chmod"], timestamps: ["utimes"], readlink: ["readlink"],
|
|
86
|
+
};
|
|
87
|
+
const values = mounts.map(({ backend }) => {
|
|
88
|
+
if (backend.capabilities.readOnly === true
|
|
89
|
+
&& !["read", "stat", "readdir", "realpath", "access", "readlink", "explicitDirectories", "implicitDirectories"].includes(capability))
|
|
90
|
+
return false;
|
|
91
|
+
const declared = backend.capabilities[capability];
|
|
92
|
+
return declared === true && optional[capability]?.some(method => typeof backend[method] !== "function") ? false : declared;
|
|
93
|
+
});
|
|
94
|
+
if (["rename", "copy", "exclusiveCopy"].includes(capability) && mounts.length > 1)
|
|
95
|
+
return undefined;
|
|
96
|
+
return values.every(value => value === true) ? true : values.every(value => value === false) ? false : undefined;
|
|
97
|
+
};
|
|
98
|
+
const semantics = Object.fromEntries([
|
|
99
|
+
"read", "stat", "readdir", "realpath", "access",
|
|
100
|
+
"write", "append", "exclusiveCreate", "explicitDirectories", "implicitDirectories", "mkdir", "recursiveMkdir",
|
|
101
|
+
"remove", "removeDirectory", "recursiveRemove", "rename", "copy", "exclusiveCopy", "readlink", "truncate",
|
|
102
|
+
"streamingAppend", "randomAccessWrite", "symlinks", "hardlinks", "permissions", "timestamps",
|
|
103
|
+
].map(capability => [capability, common(capability)]).filter(([, value]) => value !== undefined));
|
|
82
104
|
this.capabilities = Object.freeze({
|
|
83
105
|
get snapshotRmdir() { return mounts.some(({ backend }) => backend.capabilities.snapshotRmdir === true); },
|
|
84
106
|
readOnly: all("readOnly"),
|
|
85
107
|
...(append === undefined ? {} : { append }),
|
|
86
|
-
|
|
87
|
-
hardlinks: all("hardlinks", ["link"]),
|
|
88
|
-
permissions: all("permissions", ["chmod"]),
|
|
89
|
-
timestamps: all("timestamps", ["utimes"]),
|
|
108
|
+
...semantics,
|
|
90
109
|
atomicRename: mounts.length === 1 && all("atomicRename"),
|
|
91
110
|
...(streamingRead === undefined ? {} : { streamingRead }),
|
|
92
111
|
...(streamingWrite === undefined ? {} : { streamingWrite }),
|
|
@@ -95,6 +114,24 @@ export class MountFileSystem {
|
|
|
95
114
|
select(path) {
|
|
96
115
|
return this.mounts.find((mount) => within(mount.path, path));
|
|
97
116
|
}
|
|
117
|
+
async capabilitiesFor(path, options = {}) {
|
|
118
|
+
return this.operation("capabilitiesFor", path, options, async () => {
|
|
119
|
+
const location = await this.resolve(path, options, { allowMissing: true });
|
|
120
|
+
const capabilities = await location.mount.backend.capabilitiesFor?.(location.local, options)
|
|
121
|
+
?? location.mount.backend.capabilities;
|
|
122
|
+
if (location.synthetic)
|
|
123
|
+
return readOnlyCapabilities(capabilities);
|
|
124
|
+
if (this.mounts.length === 1 || capabilities.readOnly === true)
|
|
125
|
+
return Object.freeze({ ...capabilities });
|
|
126
|
+
const { rename: ignoredRename, copy: ignoredCopy, exclusiveCopy: ignoredExclusiveCopy, ...selected } = capabilities;
|
|
127
|
+
const cannotPublish = capabilities.write === false && capabilities.streamingWrite === false && capabilities.exclusiveCreate === false;
|
|
128
|
+
return Object.freeze({
|
|
129
|
+
...selected,
|
|
130
|
+
...(capabilities.copy === false && capabilities.exclusiveCopy === false && cannotPublish ? { copy: false } : {}),
|
|
131
|
+
...(capabilities.exclusiveCopy === false && capabilities.exclusiveCreate === false && capabilities.streamingWrite === false ? { exclusiveCopy: false } : {}),
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
}
|
|
98
135
|
protected(path) {
|
|
99
136
|
return path === "/" || this.mounts.some((mount) => within(path, mount.path));
|
|
100
137
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { platform } from "#safe-fs-platform";
|
|
2
|
+
import { requireCapabilities } from "../capabilities.js";
|
|
2
3
|
import { finishCleanup } from "../../contracts/cleanup.js";
|
|
3
4
|
import { collectBytes, readBytes } from "../../contracts/io.js";
|
|
4
5
|
import { FsError, toFsError } from "../../contracts/errors.js";
|
|
@@ -83,9 +84,35 @@ export class OverlayFileSystem {
|
|
|
83
84
|
? readable.every((capability) => capability === true) ? true : undefined : false;
|
|
84
85
|
const append = !writable || this.#upper.capabilities.append === false ? false
|
|
85
86
|
: this.#upper.capabilities.append === true ? true : undefined;
|
|
87
|
+
const upper = this.#upper.capabilities;
|
|
88
|
+
const mutation = requireCapabilities(writable, upper.mkdir, upper.rename, upper.remove, upper.exclusiveCreate, upper.timestamps, upper.permissions);
|
|
89
|
+
const effectiveAppend = requireCapabilities(append, mutation);
|
|
90
|
+
const effectiveStreamingWrite = requireCapabilities(streamingWrite, mutation);
|
|
91
|
+
const semantics = Object.fromEntries([
|
|
92
|
+
...["read", "stat", "readdir", "realpath", "access"].map(capability => [capability,
|
|
93
|
+
requireCapabilities(upper[capability], this.#lower.capabilities[capability])]),
|
|
94
|
+
["write", requireCapabilities(mutation, upper.write)],
|
|
95
|
+
["exclusiveCreate", requireCapabilities(mutation, upper.exclusiveCreate)],
|
|
96
|
+
["mkdir", requireCapabilities(mutation, upper.mkdir)],
|
|
97
|
+
["recursiveMkdir", requireCapabilities(mutation, upper.recursiveMkdir)],
|
|
98
|
+
["remove", requireCapabilities(mutation, upper.remove)],
|
|
99
|
+
["removeDirectory", requireCapabilities(mutation, upper.removeDirectory)],
|
|
100
|
+
["recursiveRemove", requireCapabilities(mutation, upper.recursiveRemove)],
|
|
101
|
+
["rename", requireCapabilities(mutation, upper.rename, upper.write)],
|
|
102
|
+
["copy", requireCapabilities(mutation, upper.write)],
|
|
103
|
+
["exclusiveCopy", requireCapabilities(mutation, upper.exclusiveCreate)],
|
|
104
|
+
["truncate", requireCapabilities(mutation, upper.truncate)],
|
|
105
|
+
["streamingAppend", requireCapabilities(mutation, streamingWrite, upper.streamingAppend)],
|
|
106
|
+
["randomAccessWrite", requireCapabilities(mutation, upper.randomAccessWrite)],
|
|
107
|
+
["explicitDirectories", requireCapabilities(upper.explicitDirectories, this.#lower.capabilities.explicitDirectories)],
|
|
108
|
+
].filter(([, value]) => value !== undefined));
|
|
86
109
|
this.capabilities = Object.freeze({
|
|
87
|
-
|
|
88
|
-
|
|
110
|
+
...semantics,
|
|
111
|
+
implicitDirectories: false,
|
|
112
|
+
readlink: upper.readlink === true && this.#lower.capabilities.readlink === true ? true
|
|
113
|
+
: upper.readlink === false && this.#lower.capabilities.readlink === false ? false : undefined,
|
|
114
|
+
...(upper.readOnly === undefined ? {} : { readOnly: upper.readOnly }),
|
|
115
|
+
...(effectiveAppend === undefined ? {} : { append: effectiveAppend }),
|
|
89
116
|
atomicRename: false,
|
|
90
117
|
hardlinks: false,
|
|
91
118
|
symlinks: writable && this.#upper.capabilities.symlinks === true
|
|
@@ -94,7 +121,7 @@ export class OverlayFileSystem {
|
|
|
94
121
|
permissions: writable && this.#upper.capabilities.permissions === true && typeof this.#upper.chmod === "function",
|
|
95
122
|
timestamps: writable && this.#upper.capabilities.timestamps === true && typeof this.#upper.utimes === "function",
|
|
96
123
|
...(streamingRead === undefined ? {} : { streamingRead }),
|
|
97
|
-
...(
|
|
124
|
+
...(effectiveStreamingWrite === undefined ? {} : { streamingWrite: effectiveStreamingWrite }),
|
|
98
125
|
});
|
|
99
126
|
Object.defineProperty(this, "capabilities", { writable: false, configurable: false });
|
|
100
127
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { quotaCapabilities } from "../capabilities.js";
|
|
1
2
|
export class FileSystemQuotaError extends Error {
|
|
2
3
|
maxBytes;
|
|
3
4
|
constructor(maxBytes) {
|
|
@@ -99,6 +100,10 @@ export function withFileSystemQuota(fs, options) {
|
|
|
99
100
|
};
|
|
100
101
|
return new Proxy(fs, {
|
|
101
102
|
get(target, property) {
|
|
103
|
+
if (property === "capabilities")
|
|
104
|
+
return quotaCapabilities(target.capabilities);
|
|
105
|
+
if (property === "capabilitiesFor")
|
|
106
|
+
return async (path, fsOptions) => quotaCapabilities(await target.capabilitiesFor?.(path, fsOptions) ?? target.capabilities);
|
|
102
107
|
const replacement = Reflect.get(mutations, property);
|
|
103
108
|
if (typeof replacement === "function")
|
|
104
109
|
return replacement;
|
|
@@ -4,6 +4,7 @@ export declare class ReadOnlyFileSystem implements FileSystem {
|
|
|
4
4
|
#private;
|
|
5
5
|
constructor(filesystem: FileSystem);
|
|
6
6
|
get capabilities(): FileSystemCapabilities;
|
|
7
|
+
capabilitiesFor(path: string, options?: FsOptions): Promise<FileSystemCapabilities>;
|
|
7
8
|
readFile(path: string, options?: ReadFileOptions): Promise<Uint8Array>;
|
|
8
9
|
stat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
9
10
|
lstat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
@@ -2,6 +2,7 @@ import { ACCESS_MODES } from "../../contracts/filesystem.js";
|
|
|
2
2
|
import { FsError } from "../../contracts/errors.js";
|
|
3
3
|
import { readBytes } from "../../contracts/io.js";
|
|
4
4
|
import { compareEntries, registerEntryView } from "../mount/comparison.js";
|
|
5
|
+
import { readOnlyCapabilities } from "../capabilities.js";
|
|
5
6
|
function readOnly(syscall, path, dest) {
|
|
6
7
|
throw new FsError("EROFS", { syscall, path, ...(dest === undefined ? {} : { dest }) });
|
|
7
8
|
}
|
|
@@ -26,7 +27,8 @@ export class ReadOnlyFileSystem {
|
|
|
26
27
|
this.#filesystem = filesystem;
|
|
27
28
|
registerEntryView(this, async (path) => ({ filesystem: this.#filesystem, path, readOnly: true }));
|
|
28
29
|
const streamingRead = typeof filesystem.readStream === "function" ? filesystem.capabilities.streamingRead : false;
|
|
29
|
-
this.#capabilities =
|
|
30
|
+
this.#capabilities = readOnlyCapabilities({
|
|
31
|
+
...filesystem.capabilities,
|
|
30
32
|
readOnly: true,
|
|
31
33
|
append: false,
|
|
32
34
|
symlinks: filesystem.capabilities.symlinks === true && typeof filesystem.readlink === "function",
|
|
@@ -41,6 +43,10 @@ export class ReadOnlyFileSystem {
|
|
|
41
43
|
get capabilities() {
|
|
42
44
|
return this.#capabilities;
|
|
43
45
|
}
|
|
46
|
+
async capabilitiesFor(path, options) {
|
|
47
|
+
const capabilities = await this.#filesystem.capabilitiesFor?.(path, options) ?? this.#filesystem.capabilities;
|
|
48
|
+
return readOnlyCapabilities(capabilities);
|
|
49
|
+
}
|
|
44
50
|
async readFile(path, options) {
|
|
45
51
|
return new Uint8Array(await this.#filesystem.readFile(path, options));
|
|
46
52
|
}
|
|
@@ -80,6 +80,11 @@ function nativeError(error) {
|
|
|
80
80
|
*/
|
|
81
81
|
export class RealFileSystem {
|
|
82
82
|
capabilities = Object.freeze({
|
|
83
|
+
read: true, stat: true, readdir: true, realpath: true, access: true,
|
|
84
|
+
write: true, append: true, exclusiveCreate: true, explicitDirectories: true, implicitDirectories: false,
|
|
85
|
+
mkdir: true, recursiveMkdir: true, remove: true, removeDirectory: true, recursiveRemove: true,
|
|
86
|
+
rename: true, copy: true, exclusiveCopy: true, readlink: true, truncate: true,
|
|
87
|
+
streamingAppend: true, randomAccessWrite: true,
|
|
83
88
|
readOnly: false, symlinks: true, hardlinks: true, permissions: true,
|
|
84
89
|
timestamps: true, atomicRename: true, streamingRead: true, streamingWrite: true,
|
|
85
90
|
});
|
|
@@ -22,6 +22,28 @@ export declare class S3RenameError extends FsError {
|
|
|
22
22
|
}
|
|
23
23
|
export declare class S3FileSystem implements FileSystem {
|
|
24
24
|
readonly capabilities: Readonly<{
|
|
25
|
+
read: true;
|
|
26
|
+
stat: true;
|
|
27
|
+
readdir: true;
|
|
28
|
+
realpath: true;
|
|
29
|
+
access: true;
|
|
30
|
+
write: true;
|
|
31
|
+
explicitDirectories: true;
|
|
32
|
+
implicitDirectories: true;
|
|
33
|
+
mkdir: true;
|
|
34
|
+
recursiveMkdir: true;
|
|
35
|
+
remove: true;
|
|
36
|
+
removeDirectory: true;
|
|
37
|
+
recursiveRemove: true;
|
|
38
|
+
copy: true;
|
|
39
|
+
readlink: false;
|
|
40
|
+
exclusiveCopy: boolean;
|
|
41
|
+
append: boolean;
|
|
42
|
+
exclusiveCreate: boolean;
|
|
43
|
+
truncate: boolean;
|
|
44
|
+
rename: boolean;
|
|
45
|
+
streamingAppend: boolean;
|
|
46
|
+
randomAccessWrite: false;
|
|
25
47
|
readOnly: boolean;
|
|
26
48
|
symlinks: false;
|
|
27
49
|
hardlinks: false;
|
|
@@ -104,6 +104,18 @@ export class S3FileSystem {
|
|
|
104
104
|
this.maxStreamBytes = validateLimit(options.maxStreamBytes ?? 5_000_000_000, "maxStreamBytes", 0, 5_000_000_000);
|
|
105
105
|
this.maxListEntries = validateLimit(options.maxListEntries ?? 100_000, "maxListEntries", 1);
|
|
106
106
|
this.capabilities = Object.freeze({
|
|
107
|
+
read: true, stat: true, readdir: true, realpath: true, access: true,
|
|
108
|
+
write: true, explicitDirectories: true, implicitDirectories: true, mkdir: true, recursiveMkdir: true,
|
|
109
|
+
remove: true, removeDirectory: true, recursiveRemove: true, copy: true, readlink: false,
|
|
110
|
+
exclusiveCopy: options.transport.capabilities?.conditionalCopy === true,
|
|
111
|
+
append: options.transport.capabilities?.conditionalPut === true,
|
|
112
|
+
exclusiveCreate: options.transport.capabilities?.conditionalPut === true,
|
|
113
|
+
truncate: options.transport.capabilities?.conditionalPut === true,
|
|
114
|
+
rename: this.allowRename && options.transport.capabilities?.conditionalDelete === true
|
|
115
|
+
&& (options.transport.capabilities?.conditionalCopy === true || options.transport.capabilities?.conditionalPut === true),
|
|
116
|
+
streamingAppend: options.transport.capabilities?.streamingWrite === true
|
|
117
|
+
&& options.transport.capabilities?.conditionalPut === true && typeof options.transport.putObjectStream === "function",
|
|
118
|
+
randomAccessWrite: false,
|
|
107
119
|
readOnly: options.readOnly ?? false,
|
|
108
120
|
symlinks: false, hardlinks: false, permissions: false,
|
|
109
121
|
timestamps: options.transport.capabilities?.conditionalCopy === true || options.transport.capabilities?.conditionalPut === true,
|
|
@@ -26,6 +26,7 @@ export interface WebDavFileSystemOptions {
|
|
|
26
26
|
readonly maxResponseBytes?: number;
|
|
27
27
|
readonly maxXmlBytes?: number;
|
|
28
28
|
readonly maxEntries?: number;
|
|
29
|
+
/** Per-request and aggregate stat/write-preflight walk timeout; defaults to 30,000 ms. */
|
|
29
30
|
readonly timeoutMs?: number;
|
|
30
31
|
readonly overwritePolicy?: "lock" | "etag";
|
|
31
32
|
readonly atomicEmptyDirectory?: WebDavAtomicEmptyDirectoryBinding;
|
|
@@ -33,6 +34,28 @@ export interface WebDavFileSystemOptions {
|
|
|
33
34
|
}
|
|
34
35
|
export declare class WebDavFileSystem implements FileSystem {
|
|
35
36
|
readonly capabilities: Readonly<{
|
|
37
|
+
read: true;
|
|
38
|
+
stat: true;
|
|
39
|
+
readdir: true;
|
|
40
|
+
realpath: true;
|
|
41
|
+
access: true;
|
|
42
|
+
write: true;
|
|
43
|
+
append: true;
|
|
44
|
+
exclusiveCreate: true;
|
|
45
|
+
explicitDirectories: true;
|
|
46
|
+
implicitDirectories: false;
|
|
47
|
+
mkdir: true;
|
|
48
|
+
recursiveMkdir: true;
|
|
49
|
+
remove: true;
|
|
50
|
+
recursiveRemove: true;
|
|
51
|
+
rename: true;
|
|
52
|
+
copy: true;
|
|
53
|
+
exclusiveCopy: true;
|
|
54
|
+
readlink: false;
|
|
55
|
+
truncate: false;
|
|
56
|
+
randomAccessWrite: false;
|
|
57
|
+
removeDirectory: boolean;
|
|
58
|
+
streamingAppend: boolean;
|
|
36
59
|
symlinks: false;
|
|
37
60
|
hardlinks: false;
|
|
38
61
|
permissions: false;
|
|
@@ -50,6 +73,7 @@ export declare class WebDavFileSystem implements FileSystem {
|
|
|
50
73
|
private readonly maxXmlBytes;
|
|
51
74
|
private readonly maxEntries;
|
|
52
75
|
private readonly timeoutMs;
|
|
76
|
+
private readonly walkDeadlines;
|
|
53
77
|
private readonly overwritePolicy;
|
|
54
78
|
private readonly configuredComparison;
|
|
55
79
|
private readonly atomicEmptyDirectory;
|
|
@@ -72,6 +96,7 @@ export declare class WebDavFileSystem implements FileSystem {
|
|
|
72
96
|
private mutation;
|
|
73
97
|
private unsupported;
|
|
74
98
|
private maybeStat;
|
|
99
|
+
private withWalkDeadline;
|
|
75
100
|
stat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
76
101
|
lstat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
77
102
|
readdir(path: string, options?: FsOptions): Promise<DirectoryEntry[]>;
|
|
@@ -87,7 +87,7 @@ function normalize(path) {
|
|
|
87
87
|
}
|
|
88
88
|
return `/${segments.join("/")}`;
|
|
89
89
|
}
|
|
90
|
-
function
|
|
90
|
+
function validateDirectoryWalkPath(path, syscall = "access") {
|
|
91
91
|
if (typeof path !== "string")
|
|
92
92
|
fail("EINVAL", "resolve", String(path), "invalid WebDAV path");
|
|
93
93
|
let bytes = 0;
|
|
@@ -105,7 +105,7 @@ function validateDirectoryAccessPath(path) {
|
|
|
105
105
|
if (point > 0xffff)
|
|
106
106
|
offset++;
|
|
107
107
|
if (bytes > 65_536 || components > 256) {
|
|
108
|
-
fail("ENAMETOOLONG",
|
|
108
|
+
fail("ENAMETOOLONG", syscall, path, "directory access exceeds the 64KiB path or 256 component limit");
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
}
|
|
@@ -140,6 +140,11 @@ function statusCode(element) {
|
|
|
140
140
|
}
|
|
141
141
|
export class WebDavFileSystem {
|
|
142
142
|
capabilities = Object.freeze({
|
|
143
|
+
read: true, stat: true, readdir: true, realpath: true, access: true,
|
|
144
|
+
write: true, append: true, exclusiveCreate: true, explicitDirectories: true, implicitDirectories: false,
|
|
145
|
+
mkdir: true, recursiveMkdir: true, remove: true, recursiveRemove: true, rename: true, copy: true,
|
|
146
|
+
exclusiveCopy: true, readlink: false, truncate: false, randomAccessWrite: false,
|
|
147
|
+
removeDirectory: false, streamingAppend: false,
|
|
143
148
|
symlinks: false, hardlinks: false, permissions: false, timestamps: true,
|
|
144
149
|
atomicRename: false, streamingRead: true, streamingWrite: true,
|
|
145
150
|
});
|
|
@@ -152,6 +157,7 @@ export class WebDavFileSystem {
|
|
|
152
157
|
maxXmlBytes;
|
|
153
158
|
maxEntries;
|
|
154
159
|
timeoutMs;
|
|
160
|
+
walkDeadlines = new WeakMap();
|
|
155
161
|
overwritePolicy;
|
|
156
162
|
configuredComparison;
|
|
157
163
|
atomicEmptyDirectory;
|
|
@@ -200,6 +206,10 @@ export class WebDavFileSystem {
|
|
|
200
206
|
}
|
|
201
207
|
this.transport = options.fetch === globalThis.fetch ? options.fetch.bind(globalThis) : options.fetch;
|
|
202
208
|
this.requestStreamSupport = options.requestStreamSupport ?? (options.fetch === globalThis.fetch ? "native" : false);
|
|
209
|
+
this.capabilities = Object.freeze({ ...this.capabilities,
|
|
210
|
+
removeDirectory: this.atomicEmptyDirectory !== undefined,
|
|
211
|
+
streamingAppend: this.requestStreamSupport !== false,
|
|
212
|
+
});
|
|
203
213
|
this.maxResponseBytes = positive(options.maxResponseBytes ?? 64 * 1024 * 1024, "maxResponseBytes");
|
|
204
214
|
this.maxXmlBytes = positive(options.maxXmlBytes ?? 2 * 1024 * 1024, "maxXmlBytes");
|
|
205
215
|
this.maxEntries = positive(options.maxEntries ?? 10_000, "maxEntries");
|
|
@@ -311,7 +321,10 @@ export class WebDavFileSystem {
|
|
|
311
321
|
async *requestStream(method, path, options, init, consume, collection = false, received) {
|
|
312
322
|
if (options.signal?.aborted)
|
|
313
323
|
fail("ECANCELED", method, path);
|
|
314
|
-
const
|
|
324
|
+
const walk = this.walkDeadlines.get(options);
|
|
325
|
+
const deadline = walk?.deadline ?? createRequestTimeout(this.timeoutMs);
|
|
326
|
+
if (walk)
|
|
327
|
+
walk.deadline = deadline;
|
|
315
328
|
const timeout = deadline.signal;
|
|
316
329
|
try {
|
|
317
330
|
const headers = new Headers(this.headers);
|
|
@@ -400,7 +413,8 @@ export class WebDavFileSystem {
|
|
|
400
413
|
}
|
|
401
414
|
}
|
|
402
415
|
finally {
|
|
403
|
-
|
|
416
|
+
if (!walk)
|
|
417
|
+
deadline.dispose();
|
|
404
418
|
}
|
|
405
419
|
}
|
|
406
420
|
async bytes(response, limit, signal) {
|
|
@@ -670,27 +684,44 @@ export class WebDavFileSystem {
|
|
|
670
684
|
throw error;
|
|
671
685
|
}
|
|
672
686
|
}
|
|
687
|
+
async withWalkDeadline(options, walk) {
|
|
688
|
+
if (this.walkDeadlines.has(options))
|
|
689
|
+
return walk(options);
|
|
690
|
+
const scopedOptions = { ...options };
|
|
691
|
+
const owned = {};
|
|
692
|
+
this.walkDeadlines.set(scopedOptions, owned);
|
|
693
|
+
try {
|
|
694
|
+
return await walk(scopedOptions);
|
|
695
|
+
}
|
|
696
|
+
finally {
|
|
697
|
+
owned.deadline?.dispose();
|
|
698
|
+
this.walkDeadlines.delete(scopedOptions);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
673
701
|
async stat(path, options = {}) {
|
|
702
|
+
validateDirectoryWalkPath(path, "stat");
|
|
674
703
|
const normalized = normalize(path);
|
|
675
704
|
const collection = requiresCollection(path);
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
return stat;
|
|
681
|
-
}
|
|
682
|
-
catch (error) {
|
|
683
|
-
if (!isFsError(error, "ENOENT"))
|
|
684
|
-
throw error;
|
|
685
|
-
let parent = "";
|
|
686
|
-
for (const segment of normalized.slice(1).split("/").slice(0, -1)) {
|
|
687
|
-
parent += `/${segment}`;
|
|
688
|
-
const ancestor = (await this.entries(parent, "0", options)).get(parent);
|
|
689
|
-
if (ancestor.type !== "directory")
|
|
705
|
+
return this.withWalkDeadline(options, async (scopedOptions) => {
|
|
706
|
+
try {
|
|
707
|
+
const stat = (await this.entries(normalized, "0", scopedOptions, collection)).get(normalized);
|
|
708
|
+
if (collection && stat.type !== "directory")
|
|
690
709
|
fail("ENOTDIR", "stat", path);
|
|
710
|
+
return stat;
|
|
691
711
|
}
|
|
692
|
-
|
|
693
|
-
|
|
712
|
+
catch (error) {
|
|
713
|
+
if (!isFsError(error, "ENOENT"))
|
|
714
|
+
throw error;
|
|
715
|
+
let parent = "";
|
|
716
|
+
for (const segment of normalized.slice(1).split("/").slice(0, -1)) {
|
|
717
|
+
parent += `/${segment}`;
|
|
718
|
+
const ancestor = (await this.entries(parent, "0", scopedOptions)).get(parent);
|
|
719
|
+
if (ancestor.type !== "directory")
|
|
720
|
+
fail("ENOTDIR", "stat", path);
|
|
721
|
+
}
|
|
722
|
+
throw error;
|
|
723
|
+
}
|
|
724
|
+
});
|
|
694
725
|
}
|
|
695
726
|
async lstat(path, options = {}) {
|
|
696
727
|
return this.stat(path, options);
|
|
@@ -742,6 +773,7 @@ export class WebDavFileSystem {
|
|
|
742
773
|
}.bind(this));
|
|
743
774
|
}
|
|
744
775
|
async prepareWrite(path, options) {
|
|
776
|
+
validateDirectoryWalkPath(path, "writeFile");
|
|
745
777
|
const normalized = normalize(path);
|
|
746
778
|
if (options.mode !== undefined)
|
|
747
779
|
this.unsupported("writeFile mode", path);
|
|
@@ -751,37 +783,39 @@ export class WebDavFileSystem {
|
|
|
751
783
|
fail("ECANCELED", "writeFile", path);
|
|
752
784
|
if (normalized === "/")
|
|
753
785
|
fail("EISDIR", "writeFile", path);
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
786
|
+
return this.withWalkDeadline(options, async (scopedOptions) => {
|
|
787
|
+
if (requiresCollection(path)) {
|
|
788
|
+
await this.stat(path, scopedOptions);
|
|
789
|
+
fail("EISDIR", "writeFile", path);
|
|
790
|
+
}
|
|
791
|
+
let parent = "";
|
|
792
|
+
for (const segment of normalized.slice(1).split("/").slice(0, -1)) {
|
|
793
|
+
parent += `/${segment}`;
|
|
794
|
+
await this.stat(`${parent}/`, scopedOptions);
|
|
795
|
+
}
|
|
796
|
+
const exclusive = options.flag === "wx" || options.flag === "ax";
|
|
797
|
+
const existing = exclusive ? undefined : await this.maybeStat(normalized, scopedOptions);
|
|
798
|
+
if (existing?.type === "directory")
|
|
799
|
+
fail("EISDIR", "writeFile", path);
|
|
800
|
+
let prefix = new Uint8Array();
|
|
801
|
+
const headers = { "Content-Type": "application/octet-stream" };
|
|
802
|
+
if (exclusive || (options.flag === "a" && !existing))
|
|
803
|
+
headers["If-None-Match"] = "*";
|
|
804
|
+
if (options.flag === "a" && existing) {
|
|
805
|
+
const snapshot = await this.request("GET", normalized, scopedOptions, { headers: { "Accept-Encoding": "identity" } }, async (response, signal) => {
|
|
806
|
+
if (response.status !== 200)
|
|
807
|
+
this.httpError(response.status, "GET", path);
|
|
808
|
+
const etag = strongEtag(response.headers.get("ETag"), path);
|
|
809
|
+
if (response.headers.get("Content-Encoding") && response.headers.get("Content-Encoding").toLowerCase() !== "identity") {
|
|
810
|
+
fail("ENOTSUP", "appendFile", path, "conditional append requires an identity representation");
|
|
811
|
+
}
|
|
812
|
+
return { etag, data: await this.bytes(response, this.maxResponseBytes, signal) };
|
|
813
|
+
});
|
|
814
|
+
prefix = snapshot.data;
|
|
815
|
+
headers["If-Match"] = snapshot.etag;
|
|
816
|
+
}
|
|
817
|
+
return { normalized, headers, prefix };
|
|
818
|
+
});
|
|
785
819
|
}
|
|
786
820
|
async writeFile(path, data, options = {}) {
|
|
787
821
|
if (!(data instanceof Uint8Array))
|
|
@@ -1156,7 +1190,7 @@ export class WebDavFileSystem {
|
|
|
1156
1190
|
if (mode & 2)
|
|
1157
1191
|
this.unsupported("access write/execute permission checks", path);
|
|
1158
1192
|
if (mode & 1)
|
|
1159
|
-
|
|
1193
|
+
validateDirectoryWalkPath(path);
|
|
1160
1194
|
const stat = await this.stat(path, options);
|
|
1161
1195
|
if (options.signal?.aborted)
|
|
1162
1196
|
fail("ECANCELED", "access", path);
|