@poe-platform/safe-fs 0.1.727 → 0.1.729
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/README.md +15 -8
- package/dist/safe-fs/contracts/errors.js +8 -1
- package/dist/safe-fs/contracts/filesystem.d.ts +68 -2
- package/dist/safe-fs/contracts/io.js +210 -28
- package/dist/safe-fs/contracts/virtual-path.d.ts +3 -1
- package/dist/safe-fs/contracts/virtual-path.js +20 -1
- package/dist/safe-fs/core.d.ts +1 -0
- package/dist/safe-fs/core.js +1 -0
- package/dist/safe-fs/fs/capabilities.d.ts +1 -1
- package/dist/safe-fs/fs/capabilities.js +59 -21
- package/dist/safe-fs/fs/conditional-chmod.d.ts +7 -0
- package/dist/safe-fs/fs/conditional-chmod.js +19 -0
- package/dist/safe-fs/fs/descriptor.js +51 -25
- package/dist/safe-fs/fs/devices/index.d.ts +7 -2
- package/dist/safe-fs/fs/devices/index.js +210 -29
- package/dist/safe-fs/fs/devices/path.d.ts +3 -1
- package/dist/safe-fs/fs/devices/path.js +70 -12
- package/dist/safe-fs/fs/memory/index.d.ts +70 -48
- package/dist/safe-fs/fs/memory/index.js +1595 -244
- package/dist/safe-fs/fs/memory/ledger.d.ts +8 -1
- package/dist/safe-fs/fs/memory/ledger.js +46 -6
- package/dist/safe-fs/fs/memory/missing-target.js +21 -1
- package/dist/safe-fs/fs/mount/index.d.ts +11 -3
- package/dist/safe-fs/fs/mount/index.js +326 -15
- package/dist/safe-fs/fs/object-publication/index.js +79 -3
- package/dist/safe-fs/fs/overlay/index.d.ts +2 -2
- package/dist/safe-fs/fs/overlay/index.js +27 -5
- package/dist/safe-fs/fs/overlay/memory-publication.d.ts +2 -1
- package/dist/safe-fs/fs/overlay/memory-publication.js +39 -2
- package/dist/safe-fs/fs/quota/index.js +5 -5
- package/dist/safe-fs/fs/readonly/index.d.ts +1 -0
- package/dist/safe-fs/fs/readonly/index.js +13 -0
- package/dist/safe-fs/fs/real/index.d.ts +6 -2
- package/dist/safe-fs/fs/real/index.js +78 -8
- package/dist/safe-fs/fs/s3/filesystem.d.ts +3 -0
- package/dist/safe-fs/fs/s3/filesystem.js +57 -25
- package/dist/safe-fs/fs/s3/http/transport.js +20 -3
- package/dist/safe-fs/fs/s3/mock.js +19 -2
- package/dist/safe-fs/fs/s3/namespace.js +8 -2
- package/dist/safe-fs/fs/s3/registry.js +2 -0
- package/dist/safe-fs/fs/scoped.d.ts +1 -0
- package/dist/safe-fs/fs/scoped.js +261 -74
- package/dist/safe-fs/fs/staging-ancestry.d.ts +8 -0
- package/dist/safe-fs/fs/staging-ancestry.js +115 -0
- package/dist/safe-fs/fs/staging-cleanup.d.ts +5 -0
- package/dist/safe-fs/fs/staging-cleanup.js +63 -0
- package/dist/safe-fs/fs/webdav/webdav.js +7 -5
- package/dist/safe-fs/platform/browser.d.ts +5 -0
- package/dist/safe-fs/platform/browser.js +17 -0
- package/dist/safe-fs/platform/node.d.ts +5 -0
- package/dist/safe-fs/platform/node.js +28 -0
- package/dist/safe-fs/platform/transport-budget.d.ts +7 -0
- package/dist/safe-fs/platform/transport-budget.js +4 -0
- package/dist/safe-fs/testing/object-io-metrics.d.ts +3 -0
- package/dist/safe-fs/testing/object-io-metrics.js +29 -0
- package/dist/safe-fs/testing/object-publication.d.ts +3 -1
- package/dist/safe-fs/testing/object-publication.js +2 -2
- package/package.json +1 -1
|
@@ -36,7 +36,9 @@ function hasCode(error, code) {
|
|
|
36
36
|
function fail(code, path, message) {
|
|
37
37
|
throw new FsError(code, { syscall: "overlay", path, ...(message === undefined ? {} : { message }) });
|
|
38
38
|
}
|
|
39
|
-
function integer(value, path) {
|
|
39
|
+
function integer(value, path, unlimited = false) {
|
|
40
|
+
if (unlimited && value === Infinity)
|
|
41
|
+
return;
|
|
40
42
|
if (!Number.isSafeInteger(value) || value < 0)
|
|
41
43
|
fail("EINVAL", path);
|
|
42
44
|
}
|
|
@@ -86,7 +88,7 @@ export class OverlayFileSystem {
|
|
|
86
88
|
}, false));
|
|
87
89
|
this.maxBufferBytes = options.maxBufferBytes ?? Infinity;
|
|
88
90
|
if (options.maxBufferBytes !== undefined)
|
|
89
|
-
integer(options.maxBufferBytes, "/");
|
|
91
|
+
integer(options.maxBufferBytes, "/", true);
|
|
90
92
|
const writable = !this.#upper.capabilities.readOnly && this.#upper.capabilities.atomicRename === true;
|
|
91
93
|
const readable = [this.#upper, this.#lower].map((backend) => retainedReadCapabilities(backend).retainedRead);
|
|
92
94
|
const streamingRead = readable.every((capability) => capability === true) ? true
|
|
@@ -123,7 +125,8 @@ export class OverlayFileSystem {
|
|
|
123
125
|
this.capabilities = Object.freeze({
|
|
124
126
|
...semantics,
|
|
125
127
|
open: false,
|
|
126
|
-
|
|
128
|
+
conditionalChmod: this.publication.supported(),
|
|
129
|
+
atomicFilePublication: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicEntryRemovalReceipt: false, atomicFileStaging: this.publication.supported(), atomicStagingAncestry: this.publication.supported(), atomicDirectoryMetadata: false, trustedOwnedStaging: false,
|
|
127
130
|
implicitDirectories: false,
|
|
128
131
|
readlink: upper.readlink === true && this.#lower.capabilities.readlink === true ? true
|
|
129
132
|
: upper.readlink === false && this.#lower.capabilities.readlink === false ? false : undefined,
|
|
@@ -519,7 +522,7 @@ export class OverlayFileSystem {
|
|
|
519
522
|
fail("EISDIR", entry.path);
|
|
520
523
|
this.permission(entry, 4);
|
|
521
524
|
if (options.maxBytes !== undefined)
|
|
522
|
-
integer(options.maxBytes, entry.path);
|
|
525
|
+
integer(options.maxBytes, entry.path, true);
|
|
523
526
|
const limit = Math.min(options.maxBytes ?? this.maxBufferBytes, this.maxBufferBytes);
|
|
524
527
|
if (entry.stat.size > limit)
|
|
525
528
|
fail("EFBIG", entry.path);
|
|
@@ -683,13 +686,14 @@ export class OverlayFileSystem {
|
|
|
683
686
|
}
|
|
684
687
|
return location;
|
|
685
688
|
}
|
|
686
|
-
async replace(location, options, operation, streaming = false) {
|
|
689
|
+
async replace(location, options, operation, streaming = false, commitGuard) {
|
|
687
690
|
await this.parent(location.path, options);
|
|
688
691
|
await this.staged(options, async (temporary) => {
|
|
689
692
|
if (location.entry)
|
|
690
693
|
await this.clone(location.entry, temporary, options, streaming);
|
|
691
694
|
await operation(temporary);
|
|
692
695
|
options.signal?.throwIfAborted();
|
|
696
|
+
commitGuard?.();
|
|
693
697
|
await this.#upper.rename(temporary, location.path, options);
|
|
694
698
|
this.linkMetadata.delete(location.path);
|
|
695
699
|
this.linkOrigins.delete(location.path);
|
|
@@ -1003,6 +1007,24 @@ export class OverlayFileSystem {
|
|
|
1003
1007
|
if (permissions === undefined)
|
|
1004
1008
|
fail("EINVAL", path);
|
|
1005
1009
|
mode(permissions, path);
|
|
1010
|
+
const guard = this.publication.prepareChmod(path, options);
|
|
1011
|
+
if (guard) {
|
|
1012
|
+
const controls = options.signal === undefined ? {} : { signal: options.signal };
|
|
1013
|
+
return this.run(controls, async () => {
|
|
1014
|
+
this.writable(path);
|
|
1015
|
+
guard();
|
|
1016
|
+
const entry = await this.required(path, controls, false);
|
|
1017
|
+
guard();
|
|
1018
|
+
if (entry.stat.type === "directory") {
|
|
1019
|
+
await this.copyUp(entry, controls);
|
|
1020
|
+
guard();
|
|
1021
|
+
await this.#upper.chmod(entry.path, permissions, controls);
|
|
1022
|
+
}
|
|
1023
|
+
else {
|
|
1024
|
+
await this.replace({ path: entry.path, entry }, controls, temporary => this.#upper.chmod(temporary, permissions, controls), false, guard);
|
|
1025
|
+
}
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1006
1028
|
return this.metadata(path, "permissions", options, (target) => this.#upper.chmod(target, permissions, options));
|
|
1007
1029
|
}
|
|
1008
1030
|
async utimes(path, atimeMs, mtimeMs, options = {}) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CreateStagedFileOptions, FileStaging, FileStat, FileSystem, FsOptions, PublishStagedFileOptions, StagedFileContent } from "../../contracts/filesystem.js";
|
|
1
|
+
import type { CreateStagedFileOptions, FileStaging, FileStat, FileSystem, FsOptions, ChmodOptions, PublishStagedFileOptions, StagedFileContent } from "../../contracts/filesystem.js";
|
|
2
2
|
/** Stock Memory mutations commit before returning their promise. No user code or
|
|
3
3
|
* await is allowed between inspecting the two stores and invoking a mutation. */
|
|
4
4
|
export declare class OverlayMemoryPublication {
|
|
@@ -26,6 +26,7 @@ export declare class OverlayMemoryPublication {
|
|
|
26
26
|
private inspect;
|
|
27
27
|
private capture;
|
|
28
28
|
private check;
|
|
29
|
+
prepareChmod(path: string, options: ChmodOptions): (() => true) | undefined;
|
|
29
30
|
private copyParents;
|
|
30
31
|
create(directory: string, name: string, content: StagedFileContent, options: CreateStagedFileOptions): Promise<FileStaging>;
|
|
31
32
|
publish(staging: FileStaging, path: string, options: PublishStagedFileOptions): Promise<void>;
|
|
@@ -2,6 +2,8 @@ import { FsError, isFsError } from "../../contracts/errors.js";
|
|
|
2
2
|
import { dirname, isPathWithin, validatePath } from "../../contracts/virtual-path.js";
|
|
3
3
|
import { memoryAtomicView } from "../memory/atomic-view.js";
|
|
4
4
|
import { compareIdentity } from "../mount/identity.js";
|
|
5
|
+
import { snapshotConditionalChmod } from "../conditional-chmod.js";
|
|
6
|
+
import { runStagingGuard } from "../staging-ancestry.js";
|
|
5
7
|
/** Stock Memory mutations commit before returning their promise. No user code or
|
|
6
8
|
* await is allowed between inspecting the two stores and invoking a mutation. */
|
|
7
9
|
export class OverlayMemoryPublication {
|
|
@@ -21,10 +23,10 @@ export class OverlayMemoryPublication {
|
|
|
21
23
|
this.opaque = opaque;
|
|
22
24
|
this.hidden = hidden;
|
|
23
25
|
}
|
|
24
|
-
supported() { return !!memoryAtomicView(this.upper) && !!memoryAtomicView(this.lower); }
|
|
26
|
+
supported() { return this.upper.capabilities.readOnly !== true && !!memoryAtomicView(this.upper) && !!memoryAtomicView(this.lower); }
|
|
25
27
|
stores() {
|
|
26
28
|
const upper = memoryAtomicView(this.upper), lower = memoryAtomicView(this.lower);
|
|
27
|
-
if (!upper || !lower)
|
|
29
|
+
if (!upper || !lower || this.upper.capabilities.readOnly === true)
|
|
28
30
|
throw new FsError("ENOTSUP", { syscall: "overlayPublication" });
|
|
29
31
|
return { upper, lower };
|
|
30
32
|
}
|
|
@@ -138,6 +140,41 @@ export class OverlayMemoryPublication {
|
|
|
138
140
|
throw new FsError("ENOTDIR", { path: entry.path });
|
|
139
141
|
}
|
|
140
142
|
}
|
|
143
|
+
prepareChmod(path, options) {
|
|
144
|
+
const { signal, parent, expected, ancestors, commitGuard } = options;
|
|
145
|
+
const sources = ancestors?.map(entry => ({ path: entry.path, stat: entry.stat }));
|
|
146
|
+
const captured = snapshotConditionalChmod(path, {
|
|
147
|
+
...(signal === undefined ? {} : { signal }), ...(parent === undefined ? {} : { parent }),
|
|
148
|
+
...(expected === undefined ? {} : { expected }), ...(sources === undefined ? {} : { ancestors: sources }),
|
|
149
|
+
...(commitGuard === undefined ? {} : { commitGuard }),
|
|
150
|
+
});
|
|
151
|
+
if (!captured)
|
|
152
|
+
return undefined;
|
|
153
|
+
this.stores();
|
|
154
|
+
this.path(path);
|
|
155
|
+
const retain = (source, snapshot) => {
|
|
156
|
+
const receipt = this.receipts.get(source);
|
|
157
|
+
if (!receipt)
|
|
158
|
+
throw new FsError("ENOTSUP", { path, message: "unowned overlay chmod receipt" });
|
|
159
|
+
this.receipts.set(snapshot, receipt);
|
|
160
|
+
};
|
|
161
|
+
retain(parent, captured.parent);
|
|
162
|
+
retain(expected, captured.expected);
|
|
163
|
+
for (const [index, entry] of captured.ancestors.entries())
|
|
164
|
+
retain(sources[index].stat, entry.stat);
|
|
165
|
+
return () => {
|
|
166
|
+
signal?.throwIfAborted();
|
|
167
|
+
if (captured.commitGuard)
|
|
168
|
+
runStagingGuard(captured.commitGuard);
|
|
169
|
+
this.stores();
|
|
170
|
+
this.check(captured.ancestors);
|
|
171
|
+
this.expect(dirname(path), captured.parent);
|
|
172
|
+
this.expect(path, captured.expected, captured.expected.type !== "directory");
|
|
173
|
+
if (this.inspect(path)?.type !== captured.expected.type)
|
|
174
|
+
throw new FsError("EAGAIN", { path });
|
|
175
|
+
return true;
|
|
176
|
+
};
|
|
177
|
+
}
|
|
141
178
|
async copyParents(entries, options) {
|
|
142
179
|
for (const entry of entries) {
|
|
143
180
|
options.signal?.throwIfAborted();
|
|
@@ -153,15 +153,15 @@ async function existingBytes(fs, path, options) {
|
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
155
|
export function withFileSystemQuota(fs, options) {
|
|
156
|
-
if (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 0)
|
|
157
|
-
throw new RangeError("maxBytes must be a nonnegative safe integer");
|
|
156
|
+
if (options.maxBytes !== Infinity && (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 0))
|
|
157
|
+
throw new RangeError("maxBytes must be a nonnegative safe integer or Infinity");
|
|
158
158
|
const scanLimits = {
|
|
159
159
|
maxScanEntries: options.maxScanEntries ?? Infinity,
|
|
160
160
|
maxScanDepth: options.maxScanDepth ?? Infinity,
|
|
161
161
|
};
|
|
162
162
|
for (const [name, value] of Object.entries({ maxScanEntries: options.maxScanEntries, maxScanDepth: options.maxScanDepth })) {
|
|
163
|
-
if (value !== undefined && (!Number.isSafeInteger(value) || value < 0))
|
|
164
|
-
throw new RangeError(`${name} must be a nonnegative safe integer`);
|
|
163
|
+
if (value !== undefined && value !== Infinity && (!Number.isSafeInteger(value) || value < 0))
|
|
164
|
+
throw new RangeError(`${name} must be a nonnegative safe integer or Infinity`);
|
|
165
165
|
}
|
|
166
166
|
let queue = Promise.resolve();
|
|
167
167
|
let reservedBytes = 0;
|
|
@@ -499,7 +499,7 @@ export function withFileSystemQuota(fs, options) {
|
|
|
499
499
|
// capabilities and methods without violating invariants on own properties.
|
|
500
500
|
return new Proxy(Object.create(fs), {
|
|
501
501
|
get(_target, property) {
|
|
502
|
-
if (property === "publishFileConditional" || property === "removeEntryConditional" || property === "writeFileConditional" || property === "removeFileConditional" || property === "resizeFile" || property === "canonicalizeMissingTarget" || property === "createStagedFile" || property === "publishStagedFile" || property === "removeStagedFile" || property === "prepareDirectory")
|
|
502
|
+
if (property === "prepareStagingResolution" || property === "prepareDirectoryAncestry" || property === "publishFileConditional" || property === "removeEntryConditional" || property === "writeFileConditional" || property === "removeFileConditional" || property === "resizeFile" || property === "canonicalizeMissingTarget" || property === "createStagedFile" || property === "publishStagedFile" || property === "removeStagedFile" || property === "prepareDirectory")
|
|
503
503
|
return undefined;
|
|
504
504
|
if (property === "capabilities")
|
|
505
505
|
return quotaCapabilities({ ...retainedResizeCapabilities(fs, retainedReadCapabilities(fs)), ...(typeof fs.open === "function" ? {} : { open: false }) });
|
|
@@ -3,6 +3,7 @@ import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, Direct
|
|
|
3
3
|
import type { FileDescriptor, OpenFileOptions } from "../../contracts/descriptor.js";
|
|
4
4
|
export declare class ReadOnlyFileSystem implements FileSystem {
|
|
5
5
|
#private;
|
|
6
|
+
readonly removeEntryConditional?: NonNullable<FileSystem["removeEntryConditional"]>;
|
|
6
7
|
constructor(filesystem: FileSystem);
|
|
7
8
|
get capabilities(): FileSystemCapabilities;
|
|
8
9
|
capabilitiesFor(path: string, options?: CapabilityQueryOptions): Promise<FileSystemCapabilities>;
|
|
@@ -7,6 +7,7 @@ import { openRetainedReadFile, readOnlyCapabilities, retainedReadCapabilities }
|
|
|
7
7
|
import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
|
|
8
8
|
import { forwardFileDescriptor, openFileDescriptor } from "../descriptor.js";
|
|
9
9
|
import { pathNamespace, readOnlyPathNamespace } from "../path-namespace.js";
|
|
10
|
+
import { memoryAtomicView, registerMemoryAtomicView } from "../memory/atomic-view.js";
|
|
10
11
|
function readOnly(syscall, path, dest) {
|
|
11
12
|
throw new FsError("EROFS", { syscall, path, ...(dest === undefined ? {} : { dest }) });
|
|
12
13
|
}
|
|
@@ -37,6 +38,17 @@ export class ReadOnlyFileSystem {
|
|
|
37
38
|
#capabilities;
|
|
38
39
|
constructor(filesystem) {
|
|
39
40
|
this.#filesystem = filesystem;
|
|
41
|
+
const atomic = memoryAtomicView(filesystem);
|
|
42
|
+
if (atomic)
|
|
43
|
+
registerMemoryAtomicView(this, atomic, () => {
|
|
44
|
+
if (Object.getPrototypeOf(this) !== ReadOnlyFileSystem.prototype || memoryAtomicView(filesystem) !== atomic)
|
|
45
|
+
return false;
|
|
46
|
+
return Object.entries(readOnlyImplementation).every(([name, expected]) => {
|
|
47
|
+
const actual = Object.getOwnPropertyDescriptor(this, name)
|
|
48
|
+
?? Object.getOwnPropertyDescriptor(ReadOnlyFileSystem.prototype, name);
|
|
49
|
+
return actual?.value === expected.value && actual?.get === expected.get && actual?.set === expected.set;
|
|
50
|
+
});
|
|
51
|
+
});
|
|
40
52
|
if (Reflect.has(filesystem, pathNamespace))
|
|
41
53
|
Object.defineProperty(this, pathNamespace, {
|
|
42
54
|
get: () => readOnlyPathNamespace(Reflect.get(filesystem, pathNamespace)),
|
|
@@ -190,6 +202,7 @@ export class ReadOnlyFileSystem {
|
|
|
190
202
|
readOnly("openResizeFile", path);
|
|
191
203
|
}
|
|
192
204
|
}
|
|
205
|
+
const readOnlyImplementation = Object.getOwnPropertyDescriptors(ReadOnlyFileSystem.prototype);
|
|
193
206
|
export function createReadOnlyFileSystem(filesystem) {
|
|
194
207
|
return new ReadOnlyFileSystem(filesystem);
|
|
195
208
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { FileDescriptor, OpenFileOptions } from "../../contracts/descriptor.js";
|
|
2
|
-
import type { AppendFileOptions, ByteSource, CopyFileOptions, DirectoryEntry, FileReadHandle, FileResizeHandle, FileStat, FileSystem, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, CreateStagedFileOptions, FileStaging, PublishStagedFileOptions, PrepareDirectoryOptions, StagedFileContent, OpenReadFileOptions, OpenResizeFileOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/index.js";
|
|
2
|
+
import type { AppendFileOptions, ByteSource, CopyFileOptions, DirectoryEntry, FileReadHandle, FileResizeHandle, FileStat, FileSystem, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ChmodOptions, FileStagingEntry, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, CreateStagedFileOptions, FileStaging, PublishStagedFileOptions, PrepareDirectoryOptions, StagedFileContent, OpenReadFileOptions, OpenResizeFileOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/index.js";
|
|
3
3
|
export interface RealFileSystemOptions {
|
|
4
4
|
/** An existing, absolute host directory. It is never created implicitly. */
|
|
5
5
|
readonly root: string;
|
|
@@ -36,6 +36,9 @@ export interface RealFileSystemOptions {
|
|
|
36
36
|
* concurrent renames, mount changes, and preexisting hardlinks cannot be made
|
|
37
37
|
* safe by these APIs. This is NOT a race-proof sandbox or an isolation boundary
|
|
38
38
|
* against another process modifying the tree. Use an OS sandbox for that.
|
|
39
|
+
* Conditional chmod and synchronous ancestry validation use the same trusted,
|
|
40
|
+
* externally isolated tree boundary as owned staging. Their final checks and
|
|
41
|
+
* metadata commit do not yield to JavaScript; they do not prevent OS races.
|
|
39
42
|
*
|
|
40
43
|
* Only regular files, directories, and symlinks are represented. Permissions,
|
|
41
44
|
* ownership, timestamp precision, case sensitivity, umask, and rename behavior
|
|
@@ -62,6 +65,7 @@ export declare class RealFileSystem implements FileSystem {
|
|
|
62
65
|
private stagingSnapshot;
|
|
63
66
|
private expectStaging;
|
|
64
67
|
private stagingMetadata;
|
|
68
|
+
prepareDirectoryAncestry(ancestors: readonly FileStagingEntry[], options?: FsOptions): Promise<() => true>;
|
|
65
69
|
createStagedFile(directoryPath: string, name: string, content: StagedFileContent, options: CreateStagedFileOptions): Promise<FileStaging>;
|
|
66
70
|
private stagingPaths;
|
|
67
71
|
private checkStaging;
|
|
@@ -89,7 +93,7 @@ export declare class RealFileSystem implements FileSystem {
|
|
|
89
93
|
readlink(path: string, options?: FsOptions): Promise<string>;
|
|
90
94
|
symlink(target: string, path: string, options?: FsOptions): Promise<void>;
|
|
91
95
|
link(existingPath: string, newPath: string, options?: FsOptions): Promise<void>;
|
|
92
|
-
chmod(path: string, mode: number, options?:
|
|
96
|
+
chmod(path: string, mode: number, options?: ChmodOptions): Promise<void>;
|
|
93
97
|
utimes(path: string, atimeMs: number, mtimeMs: number, options?: FsOptions): Promise<void>;
|
|
94
98
|
truncate(path: string, length?: number, options?: FsOptions): Promise<void>;
|
|
95
99
|
openResizeFile(path: string, options?: OpenResizeFileOptions): Promise<FileResizeHandle>;
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { snapshotStagingCreation } from "../staging-cleanup.js";
|
|
2
|
+
import { snapshotConditionalChmod } from "../conditional-chmod.js";
|
|
3
|
+
import { runStagingGuard, snapshotDirectoryAncestry } from "../staging-ancestry.js";
|
|
1
4
|
import { constants } from "node:fs";
|
|
2
5
|
import * as immediate from "node:fs";
|
|
3
6
|
import * as native from "node:fs/promises";
|
|
@@ -79,6 +82,9 @@ function nativeError(error) {
|
|
|
79
82
|
* concurrent renames, mount changes, and preexisting hardlinks cannot be made
|
|
80
83
|
* safe by these APIs. This is NOT a race-proof sandbox or an isolation boundary
|
|
81
84
|
* against another process modifying the tree. Use an OS sandbox for that.
|
|
85
|
+
* Conditional chmod and synchronous ancestry validation use the same trusted,
|
|
86
|
+
* externally isolated tree boundary as owned staging. Their final checks and
|
|
87
|
+
* metadata commit do not yield to JavaScript; they do not prevent OS races.
|
|
82
88
|
*
|
|
83
89
|
* Only regular files, directories, and symlinks are represented. Permissions,
|
|
84
90
|
* ownership, timestamp precision, case sensitivity, umask, and rename behavior
|
|
@@ -98,6 +104,7 @@ export class RealFileSystem {
|
|
|
98
104
|
rename: true, atomicRenameNoReplace: false, copy: true, exclusiveCopy: true, readlink: true, truncate: true,
|
|
99
105
|
streamingAppend: true, randomAccessWrite: true,
|
|
100
106
|
readOnly: false, symlinks: true, hardlinks: true, permissions: true,
|
|
107
|
+
conditionalChmod: true,
|
|
101
108
|
timestamps: true, atomicRename: true, streamingRead: true, streamingWrite: true, retainedRead: true, retainedResize: true, trustedOwnedStaging: true,
|
|
102
109
|
});
|
|
103
110
|
configuredRoot;
|
|
@@ -244,18 +251,22 @@ export class RealFileSystem {
|
|
|
244
251
|
throw new FsError("ENOENT");
|
|
245
252
|
return this.walk(await this.root(options), path.split("/").map((name) => ({ name, fromLink: false })), options);
|
|
246
253
|
}
|
|
247
|
-
async operation(syscall, path, options, action, dest) {
|
|
254
|
+
async operation(syscall, path, options, action, dest, preserveReceipt = false) {
|
|
248
255
|
options.signal?.throwIfAborted();
|
|
249
256
|
try {
|
|
250
257
|
validatePath(path);
|
|
251
258
|
if (dest !== undefined)
|
|
252
259
|
validatePath(dest);
|
|
253
260
|
const result = await action();
|
|
254
|
-
|
|
261
|
+
if (!preserveReceipt)
|
|
262
|
+
options.signal?.throwIfAborted();
|
|
255
263
|
return result;
|
|
256
264
|
}
|
|
257
265
|
catch (error) {
|
|
258
|
-
|
|
266
|
+
if (!preserveReceipt)
|
|
267
|
+
options.signal?.throwIfAborted();
|
|
268
|
+
else if (options.signal?.aborted && Object.is(error, options.signal.reason))
|
|
269
|
+
throw error;
|
|
259
270
|
const converted = nativeError(error);
|
|
260
271
|
throw new FsError(converted.code, {
|
|
261
272
|
syscall, path, ...(dest === undefined ? {} : { dest }),
|
|
@@ -298,11 +309,37 @@ export class RealFileSystem {
|
|
|
298
309
|
if (time !== undefined && (!Number.isFinite(time) || Math.abs(time) > 8.64e15))
|
|
299
310
|
throw new FsError("EINVAL");
|
|
300
311
|
}
|
|
312
|
+
async prepareDirectoryAncestry(ancestors, options = {}) {
|
|
313
|
+
const controls = options.signal === undefined ? {} : { signal: options.signal };
|
|
314
|
+
controls.signal?.throwIfAborted();
|
|
315
|
+
const entries = snapshotDirectoryAncestry(ancestors);
|
|
316
|
+
const path = entries.at(-1).path;
|
|
317
|
+
return this.operation("prepareDirectoryAncestry", path, controls, async () => {
|
|
318
|
+
const root = await this.root(controls);
|
|
319
|
+
const validate = () => {
|
|
320
|
+
controls.signal?.throwIfAborted();
|
|
321
|
+
try {
|
|
322
|
+
for (const entry of entries)
|
|
323
|
+
this.expectStaging(join(root, entry.path.slice(1)), entry.stat, true);
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
controls.signal?.throwIfAborted();
|
|
328
|
+
throw new FsError(nativeError(error).code, { syscall: "prepareDirectoryAncestry", path });
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
validate();
|
|
332
|
+
return validate;
|
|
333
|
+
});
|
|
334
|
+
}
|
|
301
335
|
async createStagedFile(directoryPath, name, content, options) {
|
|
336
|
+
options = snapshotStagingCreation(options, directoryPath);
|
|
302
337
|
// Unlike operation(), this must return its receipt after commit even if the
|
|
303
338
|
// signal is aborted before the promise settles.
|
|
304
339
|
options.signal?.throwIfAborted();
|
|
305
340
|
try {
|
|
341
|
+
if (options.retainCleanup === true)
|
|
342
|
+
throw new FsError("ENOTSUP");
|
|
306
343
|
if (!name || name === "." || name === ".." || name.includes("/") || name.includes("\0"))
|
|
307
344
|
throw new FsError("EINVAL");
|
|
308
345
|
this.stagingMetadata(options);
|
|
@@ -369,6 +406,9 @@ export class RealFileSystem {
|
|
|
369
406
|
}
|
|
370
407
|
async publishStagedFile(staging, destination, options) {
|
|
371
408
|
return this.operation("publishStagedFile", staging.file.path, options, async () => {
|
|
409
|
+
options.signal?.throwIfAborted();
|
|
410
|
+
if (options.ancestors !== undefined || options.commitGuard !== undefined)
|
|
411
|
+
throw new FsError("ENOTSUP");
|
|
372
412
|
const paths = await this.stagingPaths(staging, options);
|
|
373
413
|
const target = await this.path(destination, { ...options, followFinal: false, missing: "final" });
|
|
374
414
|
options.signal?.throwIfAborted();
|
|
@@ -377,12 +417,13 @@ export class RealFileSystem {
|
|
|
377
417
|
this.expectStaging(paths.file, staging.file.stat);
|
|
378
418
|
this.expectStaging(dirname(target), options.parent, true);
|
|
379
419
|
const existing = this.expectStaging(target, options.destination);
|
|
380
|
-
if (existing &&
|
|
381
|
-
|
|
382
|
-
if (target === paths.file || target === paths.directory || dirname(target) === paths.directory)
|
|
420
|
+
if (existing && existing.dev === staging.file.stat.dev && existing.ino === staging.file.stat.ino
|
|
421
|
+
|| target === paths.file || target === paths.directory || dirname(target) === paths.directory)
|
|
383
422
|
throw new FsError("EINVAL");
|
|
423
|
+
if (existing && existing.type !== "file")
|
|
424
|
+
throw new FsError("EAGAIN");
|
|
384
425
|
immediate.renameSync(paths.file, target);
|
|
385
|
-
}, destination);
|
|
426
|
+
}, destination, true);
|
|
386
427
|
}
|
|
387
428
|
async removeStagedFile(staging, options = {}) {
|
|
388
429
|
return this.operation("removeStagedFile", staging.directory.path, options, async () => {
|
|
@@ -410,7 +451,7 @@ export class RealFileSystem {
|
|
|
410
451
|
options.signal?.throwIfAborted();
|
|
411
452
|
this.expectStaging(dirname(target), options.parent, true);
|
|
412
453
|
const existing = this.expectStaging(target, options.expected);
|
|
413
|
-
if (existing &&
|
|
454
|
+
if (existing && existing.type !== "file")
|
|
414
455
|
throw new FsError("EAGAIN");
|
|
415
456
|
const fd = immediate.openSync(target, constants.O_WRONLY | constants.O_NOFOLLOW | (existing ? 0 : constants.O_CREAT | constants.O_EXCL), options.mode ?? 0o666);
|
|
416
457
|
try {
|
|
@@ -427,6 +468,10 @@ export class RealFileSystem {
|
|
|
427
468
|
}
|
|
428
469
|
if (options.mode !== undefined)
|
|
429
470
|
immediate.fchmodSync(fd, options.mode);
|
|
471
|
+
if (options.atimeMs !== undefined || options.mtimeMs !== undefined) {
|
|
472
|
+
const stat = immediate.fstatSync(fd);
|
|
473
|
+
immediate.futimesSync(fd, (options.atimeMs ?? stat.atimeMs) / 1000, (options.mtimeMs ?? stat.mtimeMs) / 1000);
|
|
474
|
+
}
|
|
430
475
|
return fileStat(immediate.fstatSync(fd, { bigint: true }));
|
|
431
476
|
}
|
|
432
477
|
finally {
|
|
@@ -769,6 +814,31 @@ export class RealFileSystem {
|
|
|
769
814
|
async chmod(path, mode, options = {}) {
|
|
770
815
|
return this.operation("chmod", path, options, async () => {
|
|
771
816
|
integer(mode);
|
|
817
|
+
const conditional = snapshotConditionalChmod(path, options);
|
|
818
|
+
if (conditional) {
|
|
819
|
+
const target = await this.path(path, { ...conditional, followFinal: false });
|
|
820
|
+
const root = await this.root(conditional);
|
|
821
|
+
if ((target === root ? "/" : `/${relative(root, target)}`) !== path)
|
|
822
|
+
throw new FsError("EAGAIN");
|
|
823
|
+
if (conditional.expected.type !== "file" && conditional.expected.type !== "directory")
|
|
824
|
+
throw new FsError("ENOTSUP");
|
|
825
|
+
const parent = target === root ? root : dirname(target);
|
|
826
|
+
const validate = () => {
|
|
827
|
+
conditional.signal?.throwIfAborted();
|
|
828
|
+
for (const entry of conditional.ancestors)
|
|
829
|
+
this.expectStaging(join(root, entry.path.slice(1)), entry.stat, true);
|
|
830
|
+
this.expectStaging(parent, conditional.parent, true);
|
|
831
|
+
const current = this.expectStaging(target, conditional.expected, conditional.expected.type === "directory");
|
|
832
|
+
if (current?.mode !== conditional.expected.mode)
|
|
833
|
+
throw new FsError("EAGAIN");
|
|
834
|
+
};
|
|
835
|
+
validate();
|
|
836
|
+
if (conditional.commitGuard)
|
|
837
|
+
runStagingGuard(conditional.commitGuard);
|
|
838
|
+
validate();
|
|
839
|
+
immediate.chmodSync(target, mode);
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
772
842
|
const target = await this.path(path, options);
|
|
773
843
|
options.signal?.throwIfAborted();
|
|
774
844
|
await native.chmod(target, mode);
|
|
@@ -14,6 +14,7 @@ export interface S3FileSystemOptions {
|
|
|
14
14
|
readonly maxReadBytes?: number;
|
|
15
15
|
readonly maxStreamBytes?: number;
|
|
16
16
|
readonly maxListEntries?: number;
|
|
17
|
+
readonly maxRequests?: number;
|
|
17
18
|
/** Aggregate budgets for each rm operation, including ancestor inspection. */
|
|
18
19
|
readonly removalLimits?: {
|
|
19
20
|
readonly maxRequests?: number;
|
|
@@ -74,6 +75,8 @@ export declare class S3FileSystem implements FileSystem {
|
|
|
74
75
|
private readonly maxReadBytes;
|
|
75
76
|
private readonly maxStreamBytes;
|
|
76
77
|
private readonly maxListEntries;
|
|
78
|
+
private readonly maxRequests;
|
|
79
|
+
private requests;
|
|
77
80
|
private readonly removalLimits;
|
|
78
81
|
private readonly removalBudgets;
|
|
79
82
|
constructor(options: S3FileSystemOptions);
|
|
@@ -3,6 +3,7 @@ import { FsError, isFsError } from "../../contracts/errors.js";
|
|
|
3
3
|
import { composeAbortSignals } from "../../contracts/abort.js";
|
|
4
4
|
import { collectBytes, readBytes } from "../../contracts/io.js";
|
|
5
5
|
import { compareEntries, registerEntryAuthority } from "../mount/comparison.js";
|
|
6
|
+
import { chargeScopedTransportCall } from "#safe-fs-platform";
|
|
6
7
|
import { compareOwnedS3Entries, queryS3Head, recordS3Stat, registerS3EntryOwner } from "./authority.js";
|
|
7
8
|
import { encodeCopySource } from "./transport.js";
|
|
8
9
|
import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
|
|
@@ -58,11 +59,31 @@ function fail(code, syscall, path, message) {
|
|
|
58
59
|
throw new FsError(code, { syscall, path, ...(message === undefined ? {} : { message }) });
|
|
59
60
|
}
|
|
60
61
|
function validateLimit(value, name, minimum, maximum = Number.MAX_SAFE_INTEGER) {
|
|
61
|
-
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
|
|
62
|
+
if ((value !== Infinity || maximum !== Infinity) && (!Number.isSafeInteger(value) || value < minimum || value > maximum)) {
|
|
62
63
|
throw new FsError("EINVAL", { message: `${name} must be an integer from ${minimum} to ${maximum}` });
|
|
63
64
|
}
|
|
64
65
|
return value;
|
|
65
66
|
}
|
|
67
|
+
function utf8ByteLength(value) {
|
|
68
|
+
let bytes = 0;
|
|
69
|
+
for (let i = 0; i < value.length; i++) {
|
|
70
|
+
const code = value.charCodeAt(i);
|
|
71
|
+
if (code < 0x80)
|
|
72
|
+
bytes += 1;
|
|
73
|
+
else if (code < 0x800)
|
|
74
|
+
bytes += 2;
|
|
75
|
+
else if (code >= 0xd800 && code <= 0xdbff && i + 1 < value.length && value.charCodeAt(i + 1) >= 0xdc00 && value.charCodeAt(i + 1) <= 0xdfff) {
|
|
76
|
+
bytes += 4;
|
|
77
|
+
i++;
|
|
78
|
+
}
|
|
79
|
+
else
|
|
80
|
+
bytes += 3;
|
|
81
|
+
}
|
|
82
|
+
return bytes;
|
|
83
|
+
}
|
|
84
|
+
function exceedsComponentByteLimit(value) {
|
|
85
|
+
return value.length > 85 && (value.length > 255 || utf8ByteLength(value) > 255);
|
|
86
|
+
}
|
|
66
87
|
function isWellFormed(value) {
|
|
67
88
|
return !/[\uD800-\uDFFF]/u.test(value);
|
|
68
89
|
}
|
|
@@ -82,6 +103,8 @@ export class S3FileSystem {
|
|
|
82
103
|
maxReadBytes;
|
|
83
104
|
maxStreamBytes;
|
|
84
105
|
maxListEntries;
|
|
106
|
+
maxRequests;
|
|
107
|
+
requests = 0;
|
|
85
108
|
removalLimits;
|
|
86
109
|
removalBudgets = new WeakMap();
|
|
87
110
|
constructor(options) {
|
|
@@ -101,19 +124,20 @@ export class S3FileSystem {
|
|
|
101
124
|
throw new FsError("EINVAL", { message: "prefix must be a canonical relative object-key prefix" });
|
|
102
125
|
}
|
|
103
126
|
this.prefix = prefix === "" || prefix.endsWith("/") ? prefix : `${prefix}/`;
|
|
104
|
-
if (
|
|
127
|
+
if (utf8ByteLength(this.prefix) > 1024)
|
|
105
128
|
throw new FsError("ENAMETOOLONG", { message: "S3 prefix exceeds 1024 UTF-8 bytes" });
|
|
106
129
|
this.transport = options.transport;
|
|
107
130
|
this.bucket = options.bucket;
|
|
108
131
|
this.allowRename = options.allowNonAtomicRename ?? true;
|
|
109
132
|
this.pageSize = validateLimit(options.pageSize ?? 1000, "pageSize", 1, 1000);
|
|
110
|
-
this.maxReadBytes = options.maxReadBytes === undefined ? Infinity : validateLimit(options.maxReadBytes, "maxReadBytes", 0);
|
|
111
|
-
this.maxStreamBytes = options.maxStreamBytes === undefined ? Infinity : validateLimit(options.maxStreamBytes, "maxStreamBytes", 0);
|
|
112
|
-
this.maxListEntries = options.maxListEntries === undefined ? Infinity : validateLimit(options.maxListEntries, "maxListEntries", 1);
|
|
133
|
+
this.maxReadBytes = options.maxReadBytes === undefined ? Infinity : validateLimit(options.maxReadBytes, "maxReadBytes", 0, Infinity);
|
|
134
|
+
this.maxStreamBytes = options.maxStreamBytes === undefined ? Infinity : validateLimit(options.maxStreamBytes, "maxStreamBytes", 0, Infinity);
|
|
135
|
+
this.maxListEntries = options.maxListEntries === undefined ? Infinity : validateLimit(options.maxListEntries, "maxListEntries", 1, Infinity);
|
|
136
|
+
this.maxRequests = options.maxRequests === undefined ? Infinity : validateLimit(options.maxRequests, "maxRequests", 1, Infinity);
|
|
113
137
|
this.removalLimits = {
|
|
114
|
-
maxRequests: validateLimit(options.removalLimits?.maxRequests ??
|
|
115
|
-
maxListEntries: validateLimit(options.removalLimits?.maxListEntries ??
|
|
116
|
-
maxDeleteObjects: validateLimit(options.removalLimits?.maxDeleteObjects ??
|
|
138
|
+
maxRequests: validateLimit(options.removalLimits?.maxRequests ?? Infinity, "removalLimits.maxRequests", 1, Infinity),
|
|
139
|
+
maxListEntries: validateLimit(options.removalLimits?.maxListEntries ?? Infinity, "removalLimits.maxListEntries", 1, Infinity),
|
|
140
|
+
maxDeleteObjects: validateLimit(options.removalLimits?.maxDeleteObjects ?? Infinity, "removalLimits.maxDeleteObjects", 1, Infinity),
|
|
117
141
|
};
|
|
118
142
|
this.capabilities = Object.freeze({
|
|
119
143
|
open: false,
|
|
@@ -167,7 +191,7 @@ export class S3FileSystem {
|
|
|
167
191
|
components.push(part);
|
|
168
192
|
}
|
|
169
193
|
const path = `/${components.join("/")}`;
|
|
170
|
-
if (
|
|
194
|
+
if (utf8ByteLength(this.key(path)) > 1024)
|
|
171
195
|
fail("ENAMETOOLONG", "resolve", input);
|
|
172
196
|
return path;
|
|
173
197
|
}
|
|
@@ -176,7 +200,7 @@ export class S3FileSystem {
|
|
|
176
200
|
}
|
|
177
201
|
directoryKey(path) {
|
|
178
202
|
const key = path === "/" ? this.prefix : `${this.key(path)}/`;
|
|
179
|
-
if (
|
|
203
|
+
if (utf8ByteLength(key) > 1024)
|
|
180
204
|
fail("ENAMETOOLONG", "resolve", path);
|
|
181
205
|
return key;
|
|
182
206
|
}
|
|
@@ -188,11 +212,17 @@ export class S3FileSystem {
|
|
|
188
212
|
return options.signal === undefined ? {} : { abortSignal: options.signal };
|
|
189
213
|
}
|
|
190
214
|
async call(syscall, path, options, action, precondition) {
|
|
215
|
+
this.checkAbort(options, syscall, path);
|
|
216
|
+
chargeScopedTransportCall(options);
|
|
191
217
|
this.checkAbort(options, syscall, path);
|
|
192
218
|
const budget = this.removalBudgets.get(options);
|
|
193
219
|
if (budget && ++budget.requests > this.removalLimits.maxRequests) {
|
|
194
220
|
fail("EFBIG", "rm", path, "S3 removal request budget exceeded");
|
|
195
221
|
}
|
|
222
|
+
if (this.requests >= this.maxRequests) {
|
|
223
|
+
fail("EFBIG", syscall, path, "S3 transport request budget exceeded");
|
|
224
|
+
}
|
|
225
|
+
this.requests++;
|
|
196
226
|
let onAbort;
|
|
197
227
|
try {
|
|
198
228
|
const pending = action();
|
|
@@ -297,7 +327,7 @@ export class S3FileSystem {
|
|
|
297
327
|
if (!key.startsWith(this.prefix))
|
|
298
328
|
fail("EIO", "listObjectsV2", path, "key escapes the configured prefix");
|
|
299
329
|
const relative = key.slice(this.prefix.length).replace(/\/$/, "");
|
|
300
|
-
if (!isWellFormed(key) || key.includes("\0") ||
|
|
330
|
+
if (!isWellFormed(key) || key.includes("\0") || utf8ByteLength(key) > 1024
|
|
301
331
|
|| (relative !== "" && relative.split("/").some((part) => !part || part === "." || part === ".."))) {
|
|
302
332
|
fail("ENOTSUP", "listObjectsV2", path, "object key cannot be represented as a canonical filesystem path");
|
|
303
333
|
}
|
|
@@ -338,22 +368,22 @@ export class S3FileSystem {
|
|
|
338
368
|
return { stat: this.makeStat("directory", marker), ...(marker ? { metadata: marker } : {}) };
|
|
339
369
|
}
|
|
340
370
|
const file = await this.head(this.key(path), path, options);
|
|
341
|
-
if (
|
|
371
|
+
if (utf8ByteLength(this.key(path)) === 1024) {
|
|
342
372
|
return file ? { stat: this.makeStat("file", file), metadata: file } : undefined;
|
|
343
373
|
}
|
|
344
374
|
const directoryKey = this.directoryKey(path);
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
directory = true;
|
|
353
|
-
break;
|
|
354
|
-
}
|
|
375
|
+
let directory = false;
|
|
376
|
+
let hasMarker = false;
|
|
377
|
+
for await (const children of this.pages(directoryKey, path, options, undefined, 1)) {
|
|
378
|
+
if ((children.Contents?.length ?? 0) > 0) {
|
|
379
|
+
directory = true;
|
|
380
|
+
hasMarker = children.Contents.some(object => object.Key === directoryKey);
|
|
381
|
+
break;
|
|
355
382
|
}
|
|
356
383
|
}
|
|
384
|
+
const marker = hasMarker ? await this.head(directoryKey, path, options) : undefined;
|
|
385
|
+
if (marker && marker.ContentLength !== 0)
|
|
386
|
+
this.unsupported("nonempty directory markers", path);
|
|
357
387
|
if (file && directory)
|
|
358
388
|
this.unsupported("file/prefix collisions", path);
|
|
359
389
|
if (file)
|
|
@@ -370,6 +400,8 @@ export class S3FileSystem {
|
|
|
370
400
|
break;
|
|
371
401
|
}
|
|
372
402
|
for (const parent of ancestors) {
|
|
403
|
+
if (parent === "/" && this.prefix === "")
|
|
404
|
+
continue;
|
|
373
405
|
const ancestor = await this.inspect(parent, options);
|
|
374
406
|
if (!ancestor)
|
|
375
407
|
fail("ENOENT", "stat", parent);
|
|
@@ -395,7 +427,7 @@ export class S3FileSystem {
|
|
|
395
427
|
return this.stat(path, options);
|
|
396
428
|
}
|
|
397
429
|
async body(output, path, options) {
|
|
398
|
-
const limit = Math.min(this.maxReadBytes, options.maxBytes === undefined ? this.maxReadBytes : validateLimit(options.maxBytes, "maxBytes", 0));
|
|
430
|
+
const limit = Math.min(this.maxReadBytes, options.maxBytes === undefined ? this.maxReadBytes : validateLimit(options.maxBytes, "maxBytes", 0, Infinity));
|
|
399
431
|
let iterator;
|
|
400
432
|
return this.call("readFile", path, options, async () => {
|
|
401
433
|
if (output.Body && Symbol.asyncIterator in output.Body)
|
|
@@ -441,7 +473,7 @@ export class S3FileSystem {
|
|
|
441
473
|
async readFile(input, options = {}) {
|
|
442
474
|
const path = this.path(input);
|
|
443
475
|
if (options.maxBytes !== undefined)
|
|
444
|
-
validateLimit(options.maxBytes, "maxBytes", 0);
|
|
476
|
+
validateLimit(options.maxBytes, "maxBytes", 0, Infinity);
|
|
445
477
|
const info = await this.stat(input, options);
|
|
446
478
|
if (info.type === "directory")
|
|
447
479
|
fail("EISDIR", "readFile", path);
|
|
@@ -776,7 +808,7 @@ export class S3FileSystem {
|
|
|
776
808
|
if (!conditionalCopy && object.Size > (streamFallback ? this.maxStreamBytes : this.maxReadBytes)) {
|
|
777
809
|
fail("EFBIG", "rename", source, "conditional PUT fallback exceeds its configured transfer or buffered read limit");
|
|
778
810
|
}
|
|
779
|
-
if (
|
|
811
|
+
if (utf8ByteLength(this.key(destination) + object.Key.slice(this.key(source).length)) > 1024)
|
|
780
812
|
fail("ENAMETOOLONG", "rename", destination);
|
|
781
813
|
}
|
|
782
814
|
const copied = [];
|