@poe-platform/safe-fs 0.1.799 → 0.1.800
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 +12 -6
- package/dist/safe-fs/contracts/io.js +26 -36
- package/dist/safe-fs/contracts/path.d.ts +1 -7
- package/dist/safe-fs/contracts/path.js +1 -7
- package/dist/safe-fs/core.d.ts +1 -0
- package/dist/safe-fs/core.js +1 -0
- package/dist/safe-fs/fs/memory/index.js +3 -3
- package/dist/safe-fs/fs/overlay/index.d.ts +4 -1
- package/dist/safe-fs/fs/overlay/index.js +57 -11
- package/dist/safe-fs/fs/overlay/memory-publication.d.ts +11 -4
- package/dist/safe-fs/fs/overlay/memory-publication.js +240 -14
- package/dist/safe-fs/fs/s3/http/request-fetch.d.ts +4 -0
- package/dist/safe-fs/fs/s3/http/request-fetch.js +74 -0
- package/dist/safe-fs/fs/s3/http/request-node.d.ts +4 -0
- package/dist/safe-fs/fs/s3/http/request-node.js +111 -0
- package/dist/safe-fs/fs/s3/http/request.d.ts +5 -4
- package/dist/safe-fs/fs/s3/http/request.js +1 -109
- package/dist/safe-fs/fs/s3/http/signature.js +7 -5
- package/dist/safe-fs/fs/s3/http/transport.js +6 -3
- package/dist/xml-ast/index.d.ts +2 -0
- package/dist/xml-ast/index.js +33 -13
- package/package.json +9 -2
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# safe-fs
|
|
2
2
|
|
|
3
|
-
An asynchronous filesystem interface for memory, rooted host directories, S3, and WebDAV, with mounts, copy-on-write overlays, and Node-style bridges.
|
|
3
|
+
An asynchronous filesystem interface for memory, rooted host directories, S3, and WebDAV, with mounts, copy-on-write overlays, and Node-style bridges. Portable filesystem types and POSIX helpers compile without Node types.
|
|
4
|
+
|
|
5
|
+
Under `workerd` and `browser` conditions, the root export includes portable filesystems, `S3FileSystem`, and `MockS3Client`. Supply an S3 transport to use object storage without Node globals.
|
|
4
6
|
|
|
5
7
|
## Quickstart
|
|
6
8
|
|
|
@@ -44,7 +46,7 @@ To create new outputs with Safe Bash's `dos2unix`, `unix2dos`, or compression co
|
|
|
44
46
|
| `@poe-platform/safe-fs` | Full Node API; selects the portable core under the `browser` and `workerd` conditions. |
|
|
45
47
|
| `@poe-platform/safe-fs/core` | Memory, mounts, overlays, read-only, WebDAV, portable bridges, types, and errors. Bundle with the `browser` condition for browsers and Workers. |
|
|
46
48
|
| `@poe-platform/safe-fs/node` | Host filesystem, Node bridge, S3, and configuration helpers; unavailable in browsers. |
|
|
47
|
-
| `@poe-platform/safe-fs/fs/*` | Individual adapters: `memory`, `readonly`, `mount`, `overlay`, `webdav`, `real`, `s3`, and `s3/http`. S3
|
|
49
|
+
| `@poe-platform/safe-fs/fs/*` | Individual adapters: `memory`, `readonly`, `mount`, `overlay`, `webdav`, `real`, `s3`, and `s3/http`. S3, its mock, and the signed `s3/http` transport work in browsers and Workers. |
|
|
48
50
|
|
|
49
51
|
```ts
|
|
50
52
|
import { createMemoryFileSystem, FsError, type FileSystem } from "@poe-platform/safe-fs/core";
|
|
@@ -111,6 +113,10 @@ advertising `retainedStagingCleanup` accept `createStagedFile(..., { parent,
|
|
|
111
113
|
retainCleanup: true })`. Use the returned `staging.cleanup.remove()` and always
|
|
112
114
|
call `staging.cleanup.close()` in `finally`. Memory and its supported wrappers
|
|
113
115
|
retain only the owned staging entries; replacement entries remain protected.
|
|
116
|
+
Overlays backed by stock Memory filesystems support conditional file mutations,
|
|
117
|
+
directory preparation, and confined staging. Publication may omit `ancestors`
|
|
118
|
+
when the destination shares the staging parent; the overlay checks the ancestry
|
|
119
|
+
retained before staging began.
|
|
114
120
|
See the [staging contract](src/contracts/filesystem.md#atomic-owned-staging).
|
|
115
121
|
|
|
116
122
|
Capture `retainFileSystemCleanup(scoped, callback, { maxOperations })` before
|
|
@@ -273,7 +279,7 @@ Every raw filesystem operation accepts an optional `signal`. Additional fields a
|
|
|
273
279
|
|
|
274
280
|
`access` takes a separate mode bitmask from `ACCESS_MODES`. `chmod` takes a mode, `utimes` takes millisecond timestamps, and `truncate` takes a byte length (default 0). For conditional chmod, check `capabilitiesFor(path, { conditionalChmod: true })` (or `capabilities`) and require `conditionalChmod: true`; supply `parent`, `expected`, and complete root-to-parent `ancestors` together. An optional mutation-free `commitGuard` must return literal `true` synchronously. Memory validates at its metadata commit; mount and supported Memory overlays preserve wrapper ancestry. Real uses its existing externally isolated host-tree boundary and does not prevent races with other processes. Backend limits still apply. Node-shaped bridge methods translate their own options rather than accepting these raw option objects; see the [bridge signatures](src/bridge/filesystem.ts).
|
|
275
281
|
|
|
276
|
-
`collectBytes(source, { maxBytes, maxMemoryBytes, signal })` snapshots streamed chunks into one growing buffer. `maxBytes` and `maxMemoryBytes` accept explicit `Infinity`. `maxMemoryBytes` limits owned capacity, the current input's full backing buffer, and overlapping allocations during growth; exhaustion throws `EFBIG`. Collection limits are disabled by default on Node, browser, and Worker targets. The returned view may retain geometric spare capacity. Configured budgets cover collection, not caller-retained results, transport buffering, archive decoding, strings, or the rest of the runtime; use streaming APIs and limit concurrent workloads for larger inputs.
|
|
282
|
+
`collectBytes(source, { maxBytes, maxMemoryBytes, signal })` snapshots streamed chunks into one growing buffer. `maxBytes` and `maxMemoryBytes` accept explicit `Infinity`. `maxMemoryBytes` limits owned capacity, the current input's full backing buffer, and overlapping allocations during growth; exhaustion throws `EFBIG`. Collection limits are disabled by default on Node, browser, and Worker targets. Each call owns its budget; concurrent collections do not consume one another’s allowance. The returned view may retain geometric spare capacity. Configured budgets cover collection, not caller-retained results, transport buffering, archive decoding, strings, or the rest of the runtime; use streaming APIs and limit concurrent workloads for larger inputs.
|
|
277
283
|
|
|
278
284
|
<details>
|
|
279
285
|
<summary>S3 filesystem and HTTP transport options</summary>
|
|
@@ -336,7 +342,7 @@ Each batch lists from the beginning because previous keys have been deleted; it
|
|
|
336
342
|
| `enableCopy` | `true`; disabling uses a buffered GET/PUT fallback |
|
|
337
343
|
| `verifiedConditionalOperations` | Optional `put`, `copy`, `delete` booleans, each defaulting to false; enable only after verifying the server's semantics |
|
|
338
344
|
| `clock` | Current date/time function, used for signing |
|
|
339
|
-
| `request` | Optional Node HTTP request factory; defaults to
|
|
345
|
+
| `request` | Optional Node HTTP request factory; Node defaults to HTTP(S), browsers and Workers use global fetch |
|
|
340
346
|
|
|
341
347
|
</details>
|
|
342
348
|
|
|
@@ -369,10 +375,10 @@ See the [binding types](src/fs/webdav/webdav.ts) before implementing atomic dire
|
|
|
369
375
|
## Safety boundary and limitations
|
|
370
376
|
|
|
371
377
|
- **Not an OS sandbox.** Rooted storage and bridge confinement check paths and symlinks, but checks and host operations can race concurrent changes. Use a separate OS isolation boundary for hostile workloads. A custom adapter or transport is trusted code with its own host authority.
|
|
372
|
-
- **A read-only view is not an immutable store.** Mount views serialize guest symlink, rename, and removal operations against active path operations. Finish or close read streams before awaiting these namespace changes. Other references can still change the backing filesystem; this coordination applies only to operations through the same mount view. Overlay reads require retained handles and stable object and ancestor identities; unsupported backends or changed read admission fail with `ENOTSUP`. Overlays with two stock MemoryFileSystem layers support race-safe patch publication and confined directory creation/removal. Publication checks both layers, retains logical directory identity across copy-up, and leaves lower bytes intact; wrapped or customized layers remain unsupported. Overlays are not transactions; cancellation and cleanup do not guarantee rollback. Cross-mount rename can fail with `EXDEV`.
|
|
378
|
+
- **A read-only view is not an immutable store.** Mount views serialize guest symlink, rename, and removal operations against active path operations. Finish or close read streams before awaiting these namespace changes. Other references can still change the backing filesystem; this coordination applies only to operations through the same mount view. Overlay reads require retained handles and stable object and ancestor identities; unsupported backends or changed read admission fail with `ENOTSUP`. Overlays with two stock MemoryFileSystem layers support race-safe patch publication, conditional file writes/removal, no-replace rename, and confined directory creation/removal and file writes. Compression file output, `mv -n`, `sed -i`, and ssconvert work with these layers. Upper-file writes retain file identity; lower-file writes copy up without changing lower bytes. Publication checks both layers, retains logical directory identity across copy-up, and leaves lower bytes intact; wrapped or customized layers remain unsupported. Overlays are not transactions; cancellation and cleanup do not guarantee rollback. Cross-mount rename can fail with `EXDEV`.
|
|
373
379
|
- **Remote storage is not a POSIX disk.** S3 and WebDAV do not provide hardlinks or symlinks. S3 rename is non-atomic and may leave partial copies/deletions; `S3RenameError` reports the phase and affected keys. Strong empty-only removal and conditional writes depend on backend support, not a prior listing.
|
|
374
380
|
- **The bridges are partial Node compatibility layers.** No synchronous/callback API, file handles, watchers, bigint stats, flush/retry support, or `cp` dereference/timestamp-preservation options. Missing optional operations fail rather than being approximated with destructive alternatives.
|
|
375
|
-
- **Browser support is filesystem-only.** The `browser` and `workerd` export conditions select the portable surface: memory, mounts, overlays, read-only, WebDAV, and the codec-based bridge. S3 is available through `fs/s3` with an explicit transport, including its portable mock. Real storage, the Node bridge, and the configuration registry are not browser exports. WebDAV still needs server CORS support; no OPFS or directory-handle adapter is included. This does not make the SafeJS runtime browser-compatible.
|
|
381
|
+
- **Browser support is filesystem-only.** The `browser` and `workerd` export conditions select the portable surface: memory, mounts, overlays, read-only, WebDAV, and the codec-based bridge. S3 is available through `fs/s3` with an explicit transport, including its portable mock and `fs/s3/http`. The HTTP transport uses fetch in browsers and Workers and requires server CORS support in browsers. Fetch rejects keys containing dot segments that the URL parser would normalize, and never follows redirects. Node request factories are only supported in Node. Real storage, the Node bridge, and the configuration registry are not browser exports. WebDAV still needs server CORS support; no OPFS or directory-handle adapter is included. This does not make the SafeJS runtime browser-compatible.
|
|
376
382
|
- **Portable temporary names require secure randomness.** Browser overlay staging and portable bridge `mkdtemp` require `crypto.randomUUID` or `crypto.getRandomValues`; without either, they fail with `ENOTSUP` rather than use `Math.random`.
|
|
377
383
|
- **Allocation and identity may be unknown.** Optional `FileStat.allocatedBytes` is provider-reported allocation, not logical length or reclaimable space. Do not infer identity from size, timestamps, or inode numbers across unrelated backends.
|
|
378
384
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { FsError } from "./errors.js";
|
|
2
2
|
import { finishCleanup } from "./cleanup.js";
|
|
3
|
-
import { platform } from "#safe-fs-platform";
|
|
4
3
|
let sharedTextEncoder;
|
|
5
4
|
const EMPTY_BYTES = new Uint8Array(0);
|
|
6
5
|
const DONE_RESULT = Object.freeze({ done: true, value: undefined });
|
|
@@ -63,7 +62,6 @@ export function toByteSource(input) {
|
|
|
63
62
|
const bytes = typeof input === "string" ? (sharedTextEncoder ??= new TextEncoder()).encode(input) : new Uint8Array(input);
|
|
64
63
|
return new SingleChunkByteSource(bytes);
|
|
65
64
|
}
|
|
66
|
-
let activeCollectionBytes = 0;
|
|
67
65
|
export async function collectBytes(source, options) {
|
|
68
66
|
if (options.maxBytes !== undefined && options.maxBytes !== Infinity && (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 0)) {
|
|
69
67
|
throw new RangeError("maxBytes must be a nonnegative safe integer");
|
|
@@ -77,46 +75,38 @@ export async function collectBytes(source, options) {
|
|
|
77
75
|
let buffer = new Uint8Array(0);
|
|
78
76
|
let size = 0;
|
|
79
77
|
function reserve(bytes) {
|
|
80
|
-
if (bytes > memoryLimit - reserved
|
|
78
|
+
if (bytes > memoryLimit - reserved) {
|
|
81
79
|
throw new FsError("EFBIG", { syscall: "collectBytes", message: "collection exceeds memory budget" });
|
|
82
80
|
}
|
|
83
81
|
reserved += bytes;
|
|
84
|
-
activeCollectionBytes += bytes;
|
|
85
82
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (chunk.byteLength > (options.maxBytes ?? Infinity) - size) {
|
|
93
|
-
throw new FsError("EFBIG", { syscall: "collectBytes", message: "output exceeds maxBytes" });
|
|
94
|
-
}
|
|
95
|
-
// A small view can retain a large backing buffer. Keep this reservation
|
|
96
|
-
// while awaiting the next chunk, including while its source is suspended.
|
|
97
|
-
reserve(chunk.buffer.byteLength);
|
|
98
|
-
inputBytes = chunk.buffer.byteLength;
|
|
99
|
-
const nextSize = size + chunk.byteLength;
|
|
100
|
-
if (nextSize > buffer.byteLength) {
|
|
101
|
-
const capacity = Math.min(options.maxBytes ?? Infinity, Math.max(nextSize, buffer.byteLength * 2));
|
|
102
|
-
// Admit both generations before allocation. Do not fall back to repeated
|
|
103
|
-
// exact-size growth when geometric growth exceeds the budget.
|
|
104
|
-
reserve(capacity);
|
|
105
|
-
const replacement = new Uint8Array(capacity);
|
|
106
|
-
replacement.set(buffer.subarray(0, size));
|
|
107
|
-
activeCollectionBytes -= buffer.byteLength;
|
|
108
|
-
reserved -= buffer.byteLength;
|
|
109
|
-
buffer = replacement;
|
|
110
|
-
}
|
|
111
|
-
buffer.set(chunk, size);
|
|
112
|
-
size = nextSize;
|
|
83
|
+
options.signal?.throwIfAborted();
|
|
84
|
+
for await (const chunk of readBytes(source, options.signal)) {
|
|
85
|
+
reserved -= inputBytes;
|
|
86
|
+
inputBytes = 0;
|
|
87
|
+
if (chunk.byteLength > (options.maxBytes ?? Infinity) - size) {
|
|
88
|
+
throw new FsError("EFBIG", { syscall: "collectBytes", message: "output exceeds maxBytes" });
|
|
113
89
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
90
|
+
// A small view can retain a large backing buffer. Keep this reservation
|
|
91
|
+
// while awaiting the next chunk, including while its source is suspended.
|
|
92
|
+
reserve(chunk.buffer.byteLength);
|
|
93
|
+
inputBytes = chunk.buffer.byteLength;
|
|
94
|
+
const nextSize = size + chunk.byteLength;
|
|
95
|
+
if (nextSize > buffer.byteLength) {
|
|
96
|
+
const capacity = Math.min(options.maxBytes ?? Infinity, Math.max(nextSize, buffer.byteLength * 2));
|
|
97
|
+
// Admit both generations before allocation. Do not fall back to repeated
|
|
98
|
+
// exact-size growth when geometric growth exceeds the budget.
|
|
99
|
+
reserve(capacity);
|
|
100
|
+
const replacement = new Uint8Array(capacity);
|
|
101
|
+
replacement.set(buffer.subarray(0, size));
|
|
102
|
+
reserved -= buffer.byteLength;
|
|
103
|
+
buffer = replacement;
|
|
104
|
+
}
|
|
105
|
+
buffer.set(chunk, size);
|
|
106
|
+
size = nextSize;
|
|
119
107
|
}
|
|
108
|
+
options.signal?.throwIfAborted();
|
|
109
|
+
return buffer.subarray(0, size);
|
|
120
110
|
}
|
|
121
111
|
async function abortable(operation, signal) {
|
|
122
112
|
signal?.throwIfAborted();
|
|
@@ -1,8 +1,2 @@
|
|
|
1
|
-
import { posix } from "#safe-fs-platform-path";
|
|
2
1
|
export { assertPathWithin, isPathWithin, normalizePath, relativePath, resolvePath, validatePath } from "./virtual-path.js";
|
|
3
|
-
export
|
|
4
|
-
export declare const basename: typeof posix.basename;
|
|
5
|
-
export declare const dirname: typeof posix.dirname;
|
|
6
|
-
export declare const extname: typeof posix.extname;
|
|
7
|
-
export declare const joinPath: typeof posix.join;
|
|
8
|
-
export declare const isAbsolutePath: typeof posix.isAbsolute;
|
|
2
|
+
export { posixPath, basename, dirname, extname, joinPath, isAbsolutePath } from "./portable-path.js";
|
|
@@ -1,8 +1,2 @@
|
|
|
1
|
-
import { posix } from "#safe-fs-platform-path";
|
|
2
1
|
export { assertPathWithin, isPathWithin, normalizePath, relativePath, resolvePath, validatePath } from "./virtual-path.js";
|
|
3
|
-
export
|
|
4
|
-
export const basename = posix.basename;
|
|
5
|
-
export const dirname = posix.dirname;
|
|
6
|
-
export const extname = posix.extname;
|
|
7
|
-
export const joinPath = posix.join;
|
|
8
|
-
export const isAbsolutePath = posix.isAbsolute;
|
|
2
|
+
export { posixPath, basename, dirname, extname, joinPath, isAbsolutePath } from "./portable-path.js";
|
package/dist/safe-fs/core.d.ts
CHANGED
|
@@ -24,3 +24,4 @@ export type { XmlName, XmlAttribute, XmlContent, XmlElement, XmlLimits } from ".
|
|
|
24
24
|
export * from "./contracts/object.js";
|
|
25
25
|
export { ObjectAuthority } from "./fs/object-authority.js";
|
|
26
26
|
export { bindConditionalMutation, type ConditionalMutationBinding } from "./fs/memory/index.js";
|
|
27
|
+
export * from "./fs/s3/index.js";
|
package/dist/safe-fs/core.js
CHANGED
|
@@ -20,3 +20,4 @@ export { parseXml, parseXmlSteps, XmlLimitError } from "./xml.js";
|
|
|
20
20
|
export * from "./contracts/object.js";
|
|
21
21
|
export { ObjectAuthority } from "./fs/object-authority.js";
|
|
22
22
|
export { bindConditionalMutation } from "./fs/memory/index.js";
|
|
23
|
+
export * from "./fs/s3/index.js";
|
|
@@ -3440,7 +3440,7 @@ function isStockMemoryMethods(mem, names, checkRootAccessor = true) {
|
|
|
3440
3440
|
let lastReadViewData;
|
|
3441
3441
|
let lastReadViewSourceRef;
|
|
3442
3442
|
export function getLastReadMemoryFileSourceRef(view) {
|
|
3443
|
-
const ref = view === lastReadViewData ? lastReadViewSourceRef : undefined;
|
|
3443
|
+
const ref = view === lastReadViewData?.deref() ? lastReadViewSourceRef?.deref() : undefined;
|
|
3444
3444
|
lastReadViewData = undefined;
|
|
3445
3445
|
lastReadViewSourceRef = undefined;
|
|
3446
3446
|
return ref;
|
|
@@ -3462,8 +3462,8 @@ export function tryReadMemoryFileViewSync(filesystem, path, maxBytes, signal, ca
|
|
|
3462
3462
|
else if ((++fastWriteNowTick & 63) === 0)
|
|
3463
3463
|
node.atimeMs = fastWriteCachedNow = Date.now();
|
|
3464
3464
|
if (captureSourceRef && node.revision === 0) {
|
|
3465
|
-
lastReadViewData = data;
|
|
3466
|
-
lastReadViewSourceRef = node.sourceRef;
|
|
3465
|
+
lastReadViewData = new WeakRef(data);
|
|
3466
|
+
lastReadViewSourceRef = node.sourceRef === undefined ? undefined : new WeakRef(node.sourceRef);
|
|
3467
3467
|
}
|
|
3468
3468
|
return data;
|
|
3469
3469
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ByteSource } from "../../contracts/io.js";
|
|
2
2
|
import type { FileDescriptor, OpenFileOptions } from "../../contracts/descriptor.js";
|
|
3
|
-
import type { CreateStagedFileOptions, FileStaging, PublishStagedFileOptions, StagedFileContent, AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, OpenReadFileOptions, FileReadHandle, FileStat, FileSystem, FileSystemCapabilities, FsOptions, ChmodOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
|
|
3
|
+
import type { ConditionalWriteFileOptions, ConditionalRemoveFileOptions, PrepareDirectoryOptions, CreateStagedFileOptions, FileStaging, PublishStagedFileOptions, StagedFileContent, AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, OpenReadFileOptions, FileReadHandle, FileStat, FileSystem, FileSystemCapabilities, FsOptions, ChmodOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
|
|
4
4
|
export interface OverlayFileSystemOptions {
|
|
5
5
|
readonly upper: FileSystem;
|
|
6
6
|
readonly lower: FileSystem;
|
|
@@ -56,6 +56,9 @@ export declare class OverlayFileSystem implements FileSystem {
|
|
|
56
56
|
private replace;
|
|
57
57
|
readFile(path: string, options?: ReadFileOptions): Promise<Uint8Array>;
|
|
58
58
|
writeFile(path: string, data: Uint8Array, options?: WriteFileOptions): Promise<void>;
|
|
59
|
+
prepareDirectory(path: string, options: PrepareDirectoryOptions): Promise<FileStat>;
|
|
60
|
+
writeFileConditional(path: string, data: Uint8Array, options: ConditionalWriteFileOptions): Promise<FileStat>;
|
|
61
|
+
removeFileConditional(path: string, options: ConditionalRemoveFileOptions): Promise<void>;
|
|
59
62
|
appendFile(path: string, data: Uint8Array, options?: AppendFileOptions): Promise<void>;
|
|
60
63
|
stat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
61
64
|
lstat(path: string, options?: FsOptions): Promise<FileStat>;
|
|
@@ -126,13 +126,13 @@ export class OverlayFileSystem {
|
|
|
126
126
|
...semantics,
|
|
127
127
|
open: false,
|
|
128
128
|
conditionalChmod: this.publication.supported(),
|
|
129
|
-
atomicFilePublication: false, atomicFileMutation:
|
|
129
|
+
atomicFilePublication: false, atomicFileMutation: this.publication.supported(), atomicEntryRemoval: false, atomicEntryRemovalReceipt: false, atomicFileStaging: this.publication.supported(), atomicStagingAncestry: this.publication.supported(), retainedStagingCleanup: this.publication.supported(), atomicDirectoryMetadata: this.publication.supported(), trustedOwnedStaging: false,
|
|
130
130
|
implicitDirectories: false,
|
|
131
131
|
readlink: upper.readlink === true && this.#lower.capabilities.readlink === true ? true
|
|
132
132
|
: upper.readlink === false && this.#lower.capabilities.readlink === false ? false : undefined,
|
|
133
133
|
...(upper.readOnly === undefined ? {} : { readOnly: upper.readOnly }),
|
|
134
134
|
...(effectiveAppend === undefined ? {} : { append: effectiveAppend }),
|
|
135
|
-
atomicRename:
|
|
135
|
+
atomicRename: this.publication.supported(), atomicRenameNoReplace: this.publication.supported(),
|
|
136
136
|
descriptorWriteStream: false,
|
|
137
137
|
retainedResize: false, atomicResize: false,
|
|
138
138
|
hardlinks: false,
|
|
@@ -148,7 +148,7 @@ export class OverlayFileSystem {
|
|
|
148
148
|
Object.defineProperty(this, "capabilities", { writable: false, configurable: false });
|
|
149
149
|
}
|
|
150
150
|
confineExtraction(roots, options = {}) {
|
|
151
|
-
return this.run(options, async () => this.publication.confine(roots, options, (options, operation) => this.run(options, operation, false), this), false);
|
|
151
|
+
return this.run(options, async () => this.publication.confine(roots, options, (options, operation) => this.run(options, operation, false), this, this.maxBufferBytes), false);
|
|
152
152
|
}
|
|
153
153
|
createStagedFile(directory, name, content, options) {
|
|
154
154
|
return this.run(options, async () => {
|
|
@@ -715,11 +715,38 @@ export class OverlayFileSystem {
|
|
|
715
715
|
const append = options.flag === "a" || options.flag === "ax";
|
|
716
716
|
if (append && (location.entry?.stat.size ?? 0) + bytes.byteLength > this.maxBufferBytes)
|
|
717
717
|
fail("EFBIG", path);
|
|
718
|
-
|
|
719
|
-
await this
|
|
720
|
-
|
|
718
|
+
if (this.publication.supported() && location.entry?.backend === this.#upper) {
|
|
719
|
+
const parent = await this.required(dirname(location.path), options);
|
|
720
|
+
await this.publication.writeFile(location.path, bytes, {
|
|
721
|
+
...options, parent: this.publication.stat(parent.path, parent.stat),
|
|
722
|
+
expected: this.publication.stat(location.path, location.entry.stat), append,
|
|
723
|
+
}, this.maxBufferBytes);
|
|
724
|
+
}
|
|
725
|
+
else {
|
|
726
|
+
await this.replace(location, options, async (temporary) => {
|
|
727
|
+
await this.#upper.writeFile(temporary, bytes, { ...options, flag: append ? "a" : "w" });
|
|
728
|
+
});
|
|
729
|
+
}
|
|
721
730
|
});
|
|
722
731
|
}
|
|
732
|
+
prepareDirectory(path, options) {
|
|
733
|
+
const captured = { ...options };
|
|
734
|
+
return this.run(captured, () => this.publication.prepareDirectory(path, captured), false);
|
|
735
|
+
}
|
|
736
|
+
async writeFileConditional(path, data, options) {
|
|
737
|
+
options.signal?.throwIfAborted();
|
|
738
|
+
if (!(data instanceof Uint8Array))
|
|
739
|
+
throw new TypeError("Overlay files require Uint8Array data");
|
|
740
|
+
const bytes = new Uint8Array(data);
|
|
741
|
+
if (bytes.byteLength > this.maxBufferBytes)
|
|
742
|
+
fail("EFBIG", path);
|
|
743
|
+
const captured = { ...options };
|
|
744
|
+
return this.run(captured, () => this.publication.writeFile(path, bytes, captured, this.maxBufferBytes), false);
|
|
745
|
+
}
|
|
746
|
+
removeFileConditional(path, options) {
|
|
747
|
+
const captured = { ...options };
|
|
748
|
+
return this.run(captured, () => this.publication.removeFile(path, captured), false);
|
|
749
|
+
}
|
|
723
750
|
async appendFile(path, data, options = {}) {
|
|
724
751
|
return this.writeFile(path, data, { ...options, flag: "a" });
|
|
725
752
|
}
|
|
@@ -866,11 +893,13 @@ export class OverlayFileSystem {
|
|
|
866
893
|
}
|
|
867
894
|
async rename(source, destination, options = {}) {
|
|
868
895
|
return this.run(options, async () => {
|
|
869
|
-
if (options.noReplace)
|
|
896
|
+
if (options.noReplace && !this.publication.supported())
|
|
870
897
|
fail("ENOTSUP", source, "atomic no-replace rename is unsupported");
|
|
871
898
|
this.writable(source);
|
|
872
899
|
const original = await this.required(source, options, false);
|
|
873
900
|
const target = await this.resolve(destination, options, false, true);
|
|
901
|
+
if (options.noReplace && target.entry)
|
|
902
|
+
fail("EEXIST", destination);
|
|
874
903
|
if (original.path === target.path)
|
|
875
904
|
return;
|
|
876
905
|
if (original.path === "/" || target.path === "/")
|
|
@@ -885,6 +914,7 @@ export class OverlayFileSystem {
|
|
|
885
914
|
if (target.entry.stat.type === "directory" && (await this.listing(target.entry, options)).length)
|
|
886
915
|
fail("ENOTEMPTY", destination);
|
|
887
916
|
}
|
|
917
|
+
const guard = this.capabilities.atomicRename ? this.publication.prepareRename(original.path, target.path) : undefined;
|
|
888
918
|
await this.parent(original.path, options);
|
|
889
919
|
await this.parent(target.path, options);
|
|
890
920
|
await this.materialize(original, options);
|
|
@@ -902,7 +932,10 @@ export class OverlayFileSystem {
|
|
|
902
932
|
movedOrigins.push([destination, { ...origin, moves }]);
|
|
903
933
|
}
|
|
904
934
|
options.signal?.throwIfAborted();
|
|
905
|
-
|
|
935
|
+
if (this.capabilities.atomicRename)
|
|
936
|
+
await this.publication.rename(original.path, target.path, options, guard);
|
|
937
|
+
else
|
|
938
|
+
await this.#upper.rename(original.path, target.path, options);
|
|
906
939
|
this.whiteouts.add(original.path);
|
|
907
940
|
if (original.stat.type === "directory")
|
|
908
941
|
this.opaque.add(target.path);
|
|
@@ -998,6 +1031,9 @@ export class OverlayFileSystem {
|
|
|
998
1031
|
await this.copyUp(entry, options);
|
|
999
1032
|
await operation(entry.path);
|
|
1000
1033
|
}
|
|
1034
|
+
else if (this.publication.supported() && entry.backend === this.#upper) {
|
|
1035
|
+
await operation(entry.path);
|
|
1036
|
+
}
|
|
1001
1037
|
else
|
|
1002
1038
|
await this.replace({ path: entry.path, entry }, options, operation);
|
|
1003
1039
|
});
|
|
@@ -1123,9 +1159,19 @@ export class OverlayFileSystem {
|
|
|
1123
1159
|
const append = options.flag === "a" || options.flag === "ax";
|
|
1124
1160
|
if (append && (location.entry?.stat.size ?? 0) + size > this.maxBufferBytes)
|
|
1125
1161
|
fail("EFBIG", path);
|
|
1126
|
-
|
|
1127
|
-
await this
|
|
1128
|
-
|
|
1162
|
+
if (this.publication.supported() && location.entry?.backend === this.#upper) {
|
|
1163
|
+
const bytes = await this.#upper.readFile(incoming, options);
|
|
1164
|
+
const parent = await this.required(dirname(location.path), options);
|
|
1165
|
+
await this.publication.writeFile(location.path, bytes, {
|
|
1166
|
+
...options, parent: this.publication.stat(parent.path, parent.stat),
|
|
1167
|
+
expected: this.publication.stat(location.path, location.entry.stat), append,
|
|
1168
|
+
}, this.maxBufferBytes);
|
|
1169
|
+
}
|
|
1170
|
+
else {
|
|
1171
|
+
await this.replace(location, options, async (temporary) => {
|
|
1172
|
+
await this.streamToUpper(temporary, this.#upper.readStream(incoming, options), { ...options, flag: append ? "a" : "w" });
|
|
1173
|
+
}, true);
|
|
1174
|
+
}
|
|
1129
1175
|
});
|
|
1130
1176
|
});
|
|
1131
1177
|
return;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CreateStagedFileOptions, FileStaging, FileStat, FileSystem, FsOptions, ChmodOptions, PublishStagedFileOptions, StagedFileContent } from "../../contracts/filesystem.js";
|
|
1
|
+
import type { ConditionalWriteFileOptions, ConditionalRemoveFileOptions, RenameOptions, PrepareDirectoryOptions, 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 {
|
|
@@ -11,6 +11,7 @@ export declare class OverlayMemoryPublication {
|
|
|
11
11
|
private readonly directories;
|
|
12
12
|
private readonly origins;
|
|
13
13
|
private readonly stages;
|
|
14
|
+
private readonly stagingAncestors;
|
|
14
15
|
constructor(upper: FileSystem, lower: FileSystem, whiteouts: Set<string>, opaque: Set<string>, hidden: Set<string>);
|
|
15
16
|
supported(): boolean;
|
|
16
17
|
private stores;
|
|
@@ -27,9 +28,15 @@ export declare class OverlayMemoryPublication {
|
|
|
27
28
|
private capture;
|
|
28
29
|
private check;
|
|
29
30
|
prepareChmod(path: string, options: ChmodOptions): (() => true) | undefined;
|
|
31
|
+
prepareWrite(path: string, options: ConditionalWriteFileOptions): () => true;
|
|
32
|
+
writeFile(path: string, data: Uint8Array, options: ConditionalWriteFileOptions, maxBytes: number, guard?: () => void): Promise<FileStat>;
|
|
33
|
+
removeFile(path: string, options: ConditionalRemoveFileOptions): Promise<void>;
|
|
34
|
+
prepareDirectory(path: string, options: PrepareDirectoryOptions, guard?: () => void): Promise<FileStat>;
|
|
35
|
+
prepareRename(source: string, destination: string): () => void;
|
|
36
|
+
rename(source: string, destination: string, options: RenameOptions, guard: () => void): Promise<void>;
|
|
30
37
|
private copyParents;
|
|
31
|
-
create(directory: string, name: string, content: StagedFileContent, options: CreateStagedFileOptions): Promise<FileStaging>;
|
|
38
|
+
create(directory: string, name: string, content: StagedFileContent, options: CreateStagedFileOptions, guard?: () => void): Promise<FileStaging>;
|
|
32
39
|
publish(staging: FileStaging, path: string, options: PublishStagedFileOptions): Promise<void>;
|
|
33
|
-
cleanup(staging: FileStaging, options: FsOptions): Promise<void>;
|
|
34
|
-
confine(roots: readonly string[], options: FsOptions, run: <T>(options: FsOptions, operation: () => Promise<T>) => Promise<T>, fs: FileSystem): FileSystem;
|
|
40
|
+
cleanup(staging: FileStaging, options: FsOptions, retained?: boolean): Promise<void>;
|
|
41
|
+
confine(roots: readonly string[], options: FsOptions, run: <T>(options: FsOptions, operation: () => Promise<T>) => Promise<T>, fs: FileSystem, maxBytes: number): FileSystem;
|
|
35
42
|
}
|