@poe-platform/safe-fs 0.1.728 → 0.1.730
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 +16 -8
- package/dist/safe-fs/contracts/filesystem.d.ts +8 -0
- package/dist/safe-fs/contracts/io.js +140 -114
- package/dist/safe-fs/core.d.ts +1 -1
- package/dist/safe-fs/core.js +1 -1
- package/dist/safe-fs/fs/capabilities.js +4 -2
- package/dist/safe-fs/fs/memory/index.d.ts +36 -0
- package/dist/safe-fs/fs/memory/index.js +704 -156
- package/dist/safe-fs/fs/memory/ledger.d.ts +9 -3
- package/dist/safe-fs/fs/memory/ledger.js +37 -4
- package/dist/safe-fs/fs/mount/index.js +1 -1
- package/dist/safe-fs/fs/overlay/index.js +5 -3
- package/dist/safe-fs/fs/quota/index.js +4 -4
- package/dist/safe-fs/fs/real/index.js +28 -9
- package/dist/safe-fs/fs/s3/filesystem.js +10 -10
- package/dist/safe-fs/fs/scoped.d.ts +1 -0
- package/dist/safe-fs/fs/scoped.js +34 -3
- package/dist/safe-fs/fs/webdav/webdav.js +7 -5
- package/dist/safe-fs/node/native-seek-unavailable.d.ts +1 -0
- package/dist/safe-fs/node/native-seek-unavailable.js +5 -0
- package/dist/safe-fs/platform/browser.d.ts +8 -0
- package/dist/safe-fs/platform/browser.js +1 -0
- package/dist/safe-fs/platform/node.d.ts +8 -0
- package/dist/safe-fs/platform/node.js +1 -0
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -25,6 +25,14 @@ console.log(await fs.readFile("note.txt", "utf8"));
|
|
|
25
25
|
|
|
26
26
|
Output: `hello`, then `hello world`. Nothing touches the host filesystem. Raw adapters exchange `Uint8Array` values; the Node bridge adds strings, encodings, `Buffer` results, and stat predicates. Its `cwd` is the relative-path base and, by default, the confinement boundary. Set an explicit `root` to use a different boundary, for example `{ cwd: "/work", root: "/" }` to address a complete provider namespace. The bridge supports `unlink` when the provider offers atomic file removal; unsupported methods fail without host fallback. `createHostFileSystem()` provides trusted, unrestricted native host access when a rooted adapter is not desired.
|
|
27
27
|
|
|
28
|
+
Under the `workerd` export condition, `@poe-platform/safe-fs/fs/real` reads the
|
|
29
|
+
native request-local filesystem, including retained `/tmp` file bytes.
|
|
30
|
+
The restricted profile refuses generic descriptors, permission/timestamp
|
|
31
|
+
changes, exact creation modes, and owned staging; it promises neither atomic
|
|
32
|
+
rename nor inode/version identity from placeholder native metadata. Retained
|
|
33
|
+
reads remain available, with native seek reporting `ENOTSUP`. Ordinary browser
|
|
34
|
+
bundles cannot import this backend. Node keeps its POSIX host profile.
|
|
35
|
+
|
|
28
36
|
For host storage, use `await createRealFileSystem({ root: "/absolute/existing/directory" })` instead. The root must already exist; virtual `/` maps to that directory. ZIP creation and updates can use this adapter's private owned staging in an isolated host tree. Unzip extraction requires atomic ancestry verification, currently supported by MemoryFileSystem; the real adapter and mount views refuse it. Its `trustedOwnedStaging` capability checks original entries before publication and cleanup, and preserves foreign staging children. It does not advertise atomic conditional mutations: keep external writers and other in-flight writes away from the tree during these operations. Read the safety boundary below before exposing it to untrusted code.
|
|
29
37
|
|
|
30
38
|
To create new outputs with Safe Bash's `dos2unix`, `unix2dos`, or compression commands, host adapters need atomic no-replace publication. Supply `createRealFileSystem({ root, renameNoReplace })` only when that callback binds a qualified native primitive such as Linux `renameat2` with `RENAME_NOREPLACE`. It receives resolved absolute host paths and an optional signal in its third argument. Existing destinations must reject `EEXIST` atomically; existence checks followed by rename and copy/delete are insufficient. Without this binding the adapter explicitly refuses those operations. See the [no-replace contract](src/contracts/filesystem.md#atomic-no-replace-rename).
|
|
@@ -108,7 +116,7 @@ See the [staging contract](src/contracts/filesystem.md#atomic-owned-staging).
|
|
|
108
116
|
| Backend or wrapper | Use it for |
|
|
109
117
|
| --- | --- |
|
|
110
118
|
| `createMemoryFileSystem()` | Isolated, nonpersistent storage with links, permissions, timestamps, and streams; each path resolution admits at most 65,536 cumulative UTF-16 code units across the input and followed symlink targets, rejecting excess with `ENAMETOOLONG` before component allocation |
|
|
111
|
-
| `createRealFileSystem({ root })` | An existing host directory, with virtual paths rooted inside it; Node
|
|
119
|
+
| `createRealFileSystem({ root })` | An existing host directory, with virtual paths rooted inside it; Node or qualified Workerd operations |
|
|
112
120
|
| `new S3FileSystem({ transport, bucket, … })` | Bucket/prefix storage through an explicitly supplied transport; Node only |
|
|
113
121
|
| `new WebDavFileSystem({ baseUrl, fetch, … })` | A WebDAV namespace through an explicitly supplied Fetch implementation |
|
|
114
122
|
| `createReadOnlyFileSystem(filesystem)` | Rejecting writes through one view of an existing filesystem |
|
|
@@ -238,7 +246,7 @@ There are no package environment variables, implicit credentials, or automatic `
|
|
|
238
246
|
| Memory / read-only | Memory accepts independent optional `maxFileBytes`, `maxRetainedBytes`, `maxMetadataUnits` and `maxBytes` quotas, all unlimited by default. Read-only takes the backing filesystem, without an options object. |
|
|
239
247
|
| Real | Required `root`: existing absolute host directory; the constructor/factory also accepts the root string directly. |
|
|
240
248
|
| Mount | Required `root`: fallback filesystem. `mounts` defaults to `{}` and maps absolute virtual paths to filesystems. |
|
|
241
|
-
| Overlay | Required `upper` and `lower`; `maxBufferBytes` is unlimited unless configured
|
|
249
|
+
| Overlay | Required `upper` and `lower`; `maxBufferBytes` is unlimited unless configured; it and per-read `maxBytes` accept explicit `Infinity`. |
|
|
242
250
|
| Quota | `withFileSystemQuota` requires a nonnegative safe-integer `maxBytes`. It serializes mutations and counts files, symlinks, copies, hard links, truncation, and streaming writes. |
|
|
243
251
|
| Node bridge | `cwd` defaults to `/`, must be an absolute virtual path; optional lifetime `signal` and trusted `readFileMaxBytes` cap forwarded to backend reads before copy/decode. |
|
|
244
252
|
| Portable bridge | Same `cwd`, `signal` and `readFileMaxBytes`, plus required `codec` with `isEncoding`, `encode`, and `decode` functions. |
|
|
@@ -260,7 +268,7 @@ Every raw filesystem operation accepts an optional `signal`. Additional fields a
|
|
|
260
268
|
|
|
261
269
|
`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).
|
|
262
270
|
|
|
263
|
-
`collectBytes(source, { maxBytes, maxMemoryBytes, signal })` snapshots streamed chunks into one growing buffer. `maxMemoryBytes` limits owned capacity, the current input's full backing buffer, and overlapping allocations during growth; exhaustion throws `EFBIG`. Browser and Worker bundles additionally share a fixed 32 MiB budget across active collectors, even when byte limits are omitted. The returned view may retain geometric spare capacity. This budget covers 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.
|
|
271
|
+
`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`. Browser and Worker bundles additionally share a fixed 32 MiB budget across active collectors, even when byte limits are omitted. The returned view may retain geometric spare capacity. This budget covers 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.
|
|
264
272
|
|
|
265
273
|
<details>
|
|
266
274
|
<summary>S3 filesystem and HTTP transport options</summary>
|
|
@@ -280,12 +288,12 @@ For in-memory S3 simulations, `new MockS3Client({ buckets, pageSize?, now?, auth
|
|
|
280
288
|
| `maxReadBytes` | Unlimited unless configured |
|
|
281
289
|
| `maxStreamBytes` | Unlimited unless configured |
|
|
282
290
|
| `maxListEntries` | Unlimited unless configured |
|
|
283
|
-
| `removalLimits.maxRequests` |
|
|
284
|
-
| `removalLimits.maxListEntries` |
|
|
285
|
-
| `removalLimits.maxDeleteObjects` |
|
|
291
|
+
| `removalLimits.maxRequests` | Unlimited unless configured; transport calls per `rm`, including lookup, listing, and deletes |
|
|
292
|
+
| `removalLimits.maxListEntries` | Unlimited unless configured; returned listing entries in aggregate per `rm`, including lookup |
|
|
293
|
+
| `removalLimits.maxDeleteObjects` | Unlimited unless configured; objects per `rm` |
|
|
286
294
|
| `compareEntry` | Optional trusted backing-identity callback |
|
|
287
295
|
|
|
288
|
-
Removal limits apply even when shell filesystem-call limits admit a recursive `rm` as one operation.
|
|
296
|
+
Removal limits apply even when shell filesystem-call limits admit a recursive `rm` as one operation. Removal limits default to unlimited and accept a positive safe integer or `Infinity`. Read, stream, listing, and request limits also accept explicit `Infinity`. Traversal stops at the listing/request cap and rejects with `EFBIG`; all delete requests must fit the remaining request budget before the first mutation. Nonrecursive removal checks for children using pages of at most two entries. Configure finite `removalLimits` to bound the work in deployments that need them. These limits count adapter transport calls; retries inside a supplied transport need their own limit. Remote failures or concurrent writers can still cause partial deletion after preflight.
|
|
289
297
|
|
|
290
298
|
For larger trees, a trusted integration can process one bounded batch per request/job using its explicitly supplied transport. This example uses at most 17 transport calls and retains at most 16 summaries; repeat in a later job until `done`. The prefix must come from trusted deployment configuration, include the filesystem's configured prefix, and end in `/`. This deliberately bypasses filesystem collision checks and deletes directory markers as well as files; serialize it with writers when complete removal is required.
|
|
291
299
|
|
|
@@ -346,7 +354,7 @@ Each batch lists from the beginning because previous keys have been deleted; it
|
|
|
346
354
|
|
|
347
355
|
Known identity Content-Length responses use one result buffer; other responses grow storage up to the configured ceiling and return a view without a final copy. Growth can temporarily retain the old and new buffers (up to three times the response size), plus transport chunks. These per-response defaults leave headroom in Workers; hosts must still budget for metadata parsing, text decoding, transport buffers, and concurrent reads, especially when raising the limits. Use streaming reads for large files.
|
|
348
356
|
|
|
349
|
-
WebDAV metadata parsing limits the document to 100,000 elements, 100,000 content nodes, 100,000 attributes, and 256 levels of nesting, independently of `maxEntries`. Text is bounded by `maxXmlBytes`. Exceeding a structural budget reports `EFBIG`; parsing yields cooperatively so caller cancellation and `timeoutMs` remain active.
|
|
357
|
+
WebDAV `maxEntries`, `maxResponseBytes`, `maxXmlBytes`, and per-read `maxBytes` accept explicit `Infinity`. WebDAV metadata parsing limits the document to 100,000 elements, 100,000 content nodes, 100,000 attributes, and 256 levels of nesting, independently of `maxEntries`. Text is bounded by `maxXmlBytes`. Exceeding a structural budget reports `EFBIG`; parsing yields cooperatively so caller cancellation and `timeoutMs` remain active.
|
|
350
358
|
|
|
351
359
|
See the [binding types](src/fs/webdav/webdav.ts) before implementing atomic directory removal. A recursive WebDAV DELETE does not satisfy that contract.
|
|
352
360
|
|
|
@@ -78,6 +78,7 @@ export interface FileSystemCapabilities {
|
|
|
78
78
|
readonly atomicFileStaging?: boolean;
|
|
79
79
|
/** Creates a cleanup handle bound to owned staging entries, independent of ancestor paths. */
|
|
80
80
|
readonly retainedStagingCleanup?: boolean;
|
|
81
|
+
readonly retainedStagingWrite?: boolean;
|
|
81
82
|
/** Atomically verifies every supplied root-to-parent directory identity at publication. */
|
|
82
83
|
readonly atomicStagingAncestry?: boolean;
|
|
83
84
|
/** Prepares directory guards that validate without yielding or mutating state. */
|
|
@@ -249,6 +250,13 @@ export interface FileStaging {
|
|
|
249
250
|
readonly directory: FileStagingEntry;
|
|
250
251
|
readonly file: FileStagingEntry;
|
|
251
252
|
readonly cleanup?: FileStagingCleanup;
|
|
253
|
+
/** Available for retained regular-file staging on qualified backends. Writes
|
|
254
|
+
* append to the owned entry; finish seals it and returns its publication stat.
|
|
255
|
+
* Cleanup owns the lifetime and checks the last successfully written revision. */
|
|
256
|
+
readonly writer?: {
|
|
257
|
+
write(bytes: Uint8Array, options?: FsOptions): Promise<void>;
|
|
258
|
+
finish(options?: FsOptions): Promise<FileStat>;
|
|
259
|
+
};
|
|
252
260
|
}
|
|
253
261
|
export type StagedFileContent = {
|
|
254
262
|
readonly type: "file";
|
|
@@ -15,6 +15,40 @@ const EMPTY_BYTE_ITERATOR = Object.freeze({
|
|
|
15
15
|
const EMPTY_BYTE_SOURCE = Object.freeze({
|
|
16
16
|
[Symbol.asyncIterator]() { return EMPTY_BYTE_ITERATOR; },
|
|
17
17
|
});
|
|
18
|
+
const noop = () => { };
|
|
19
|
+
class SingleChunkByteIterator {
|
|
20
|
+
constructor(bytes) {
|
|
21
|
+
this.bytes = bytes;
|
|
22
|
+
this.consumed = false;
|
|
23
|
+
}
|
|
24
|
+
[Symbol.asyncIterator]() {
|
|
25
|
+
return this;
|
|
26
|
+
}
|
|
27
|
+
tryNextSync() {
|
|
28
|
+
if (this.consumed)
|
|
29
|
+
return DONE_RESULT;
|
|
30
|
+
this.consumed = true;
|
|
31
|
+
return { done: false, value: this.bytes };
|
|
32
|
+
}
|
|
33
|
+
next() {
|
|
34
|
+
if (this.consumed)
|
|
35
|
+
return RESOLVED_DONE;
|
|
36
|
+
this.consumed = true;
|
|
37
|
+
return Promise.resolve({ done: false, value: this.bytes });
|
|
38
|
+
}
|
|
39
|
+
return() {
|
|
40
|
+
this.consumed = true;
|
|
41
|
+
return RESOLVED_DONE;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
class SingleChunkByteSource {
|
|
45
|
+
constructor(bytes) {
|
|
46
|
+
this.bytes = bytes;
|
|
47
|
+
}
|
|
48
|
+
[Symbol.asyncIterator]() {
|
|
49
|
+
return new SingleChunkByteIterator(this.bytes);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
18
52
|
export function toByteSource(input) {
|
|
19
53
|
if (typeof input !== "string" && !(input instanceof Uint8Array)) {
|
|
20
54
|
throw new TypeError("Byte source input must be a string or Uint8Array");
|
|
@@ -27,38 +61,15 @@ export function toByteSource(input) {
|
|
|
27
61
|
return EMPTY_BYTE_SOURCE;
|
|
28
62
|
}
|
|
29
63
|
const bytes = typeof input === "string" ? (sharedTextEncoder ??= new TextEncoder()).encode(input) : new Uint8Array(input);
|
|
30
|
-
return
|
|
31
|
-
[Symbol.asyncIterator]() {
|
|
32
|
-
let consumed = false;
|
|
33
|
-
return {
|
|
34
|
-
[Symbol.asyncIterator]() { return this; },
|
|
35
|
-
tryNextSync() {
|
|
36
|
-
if (consumed)
|
|
37
|
-
return DONE_RESULT;
|
|
38
|
-
consumed = true;
|
|
39
|
-
return { done: false, value: bytes };
|
|
40
|
-
},
|
|
41
|
-
next() {
|
|
42
|
-
if (consumed)
|
|
43
|
-
return RESOLVED_DONE;
|
|
44
|
-
consumed = true;
|
|
45
|
-
return Promise.resolve({ done: false, value: bytes });
|
|
46
|
-
},
|
|
47
|
-
return() {
|
|
48
|
-
consumed = true;
|
|
49
|
-
return RESOLVED_DONE;
|
|
50
|
-
},
|
|
51
|
-
};
|
|
52
|
-
},
|
|
53
|
-
};
|
|
64
|
+
return new SingleChunkByteSource(bytes);
|
|
54
65
|
}
|
|
55
66
|
let activeCollectionBytes = 0;
|
|
56
67
|
export async function collectBytes(source, options) {
|
|
57
68
|
if (options.maxBytes !== undefined && options.maxBytes !== Infinity && (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 0)) {
|
|
58
69
|
throw new RangeError("maxBytes must be a nonnegative safe integer");
|
|
59
70
|
}
|
|
60
|
-
if (options.maxMemoryBytes !== undefined && (!Number.isSafeInteger(options.maxMemoryBytes) || options.maxMemoryBytes < 0)) {
|
|
61
|
-
throw new RangeError("maxMemoryBytes must be a nonnegative safe integer");
|
|
71
|
+
if (options.maxMemoryBytes !== undefined && options.maxMemoryBytes !== Infinity && (!Number.isSafeInteger(options.maxMemoryBytes) || options.maxMemoryBytes < 0)) {
|
|
72
|
+
throw new RangeError("maxMemoryBytes must be a nonnegative safe integer or Infinity");
|
|
62
73
|
}
|
|
63
74
|
const memoryLimit = options.maxMemoryBytes ?? Infinity;
|
|
64
75
|
let reserved = 0;
|
|
@@ -132,76 +143,97 @@ async function abortable(operation, signal) {
|
|
|
132
143
|
}
|
|
133
144
|
});
|
|
134
145
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
146
|
+
class ReadBytesGenerator {
|
|
147
|
+
constructor(source, signal) {
|
|
148
|
+
this.source = source;
|
|
149
|
+
this.abortSignal = signal;
|
|
150
|
+
this.iterator = undefined;
|
|
151
|
+
this.nativeAbort = false;
|
|
152
|
+
this.finished = false;
|
|
153
|
+
this.closing = undefined;
|
|
154
|
+
this.turn = undefined;
|
|
155
|
+
this.readingSync = false;
|
|
156
|
+
this.syncFailure = undefined;
|
|
157
|
+
}
|
|
158
|
+
get [readBytesSignal]() {
|
|
159
|
+
return this.abortSignal;
|
|
138
160
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
let readingSync = false;
|
|
145
|
-
let syncFailure;
|
|
146
|
-
const schedule = (action) => {
|
|
147
|
-
const previous = turn;
|
|
161
|
+
[Symbol.asyncIterator]() {
|
|
162
|
+
return this;
|
|
163
|
+
}
|
|
164
|
+
_schedule(action) {
|
|
165
|
+
const previous = this.turn;
|
|
148
166
|
let release;
|
|
149
167
|
const reserved = new Promise(resolve => { release = resolve; });
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
|
|
168
|
+
// Reserve before calling a producer, which may synchronously reenter us.
|
|
169
|
+
this.turn = reserved;
|
|
170
|
+
const result = previous
|
|
171
|
+
? previous.then(action)
|
|
172
|
+
: this.readingSync
|
|
173
|
+
? Promise.resolve().then(action)
|
|
174
|
+
: action();
|
|
175
|
+
const finish = () => {
|
|
176
|
+
if (this.turn === reserved)
|
|
177
|
+
this.turn = undefined;
|
|
178
|
+
release();
|
|
179
|
+
};
|
|
154
180
|
void result.then(finish, finish);
|
|
155
181
|
return result;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
|
|
182
|
+
}
|
|
183
|
+
_ensureIterator() {
|
|
184
|
+
let it = this.iterator;
|
|
185
|
+
if (!it) {
|
|
186
|
+
const signal = this.abortSignal;
|
|
159
187
|
signal?.throwIfAborted();
|
|
160
|
-
|
|
161
|
-
|
|
188
|
+
it = this.source[Symbol.asyncIterator]();
|
|
189
|
+
this.iterator = it;
|
|
190
|
+
this.nativeAbort = signal !== undefined && it.abortSignal === signal;
|
|
162
191
|
}
|
|
163
|
-
return
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
192
|
+
return it;
|
|
193
|
+
}
|
|
194
|
+
async _cleanupIterator(failed) {
|
|
195
|
+
const signal = this.abortSignal;
|
|
196
|
+
if (!this.finished && this.iterator?.return) {
|
|
197
|
+
this.finished = true;
|
|
198
|
+
const it = this.iterator;
|
|
199
|
+
const cleanup = Promise.resolve().then(() => it.return());
|
|
169
200
|
if (signal?.aborted)
|
|
170
|
-
void cleanup.catch(
|
|
201
|
+
void cleanup.catch(noop);
|
|
171
202
|
else
|
|
172
|
-
closing = abortable(() => cleanup, signal).then(
|
|
203
|
+
this.closing = abortable(() => cleanup, signal).then(noop);
|
|
173
204
|
}
|
|
174
205
|
else {
|
|
175
|
-
finished = true;
|
|
206
|
+
this.finished = true;
|
|
176
207
|
}
|
|
177
|
-
if (closing) {
|
|
178
|
-
const pending = closing;
|
|
208
|
+
if (this.closing) {
|
|
209
|
+
const pending = this.closing;
|
|
179
210
|
try {
|
|
180
211
|
await finishCleanup(() => pending, failed);
|
|
181
212
|
}
|
|
182
213
|
finally {
|
|
183
|
-
if (closing === pending)
|
|
184
|
-
closing = undefined;
|
|
214
|
+
if (this.closing === pending)
|
|
215
|
+
this.closing = undefined;
|
|
185
216
|
}
|
|
186
217
|
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
if (turn || readingSync || syncFailure)
|
|
218
|
+
}
|
|
219
|
+
tryNextSync() {
|
|
220
|
+
if (this.turn || this.readingSync || this.syncFailure)
|
|
190
221
|
return undefined;
|
|
191
|
-
if (finished)
|
|
222
|
+
if (this.finished)
|
|
192
223
|
return DONE_RESULT;
|
|
193
|
-
readingSync = true;
|
|
224
|
+
this.readingSync = true;
|
|
194
225
|
try {
|
|
195
|
-
const it =
|
|
226
|
+
const it = this._ensureIterator();
|
|
196
227
|
if (typeof it.tryNextSync !== "function")
|
|
197
228
|
return undefined;
|
|
229
|
+
const signal = this.abortSignal;
|
|
198
230
|
signal?.throwIfAborted();
|
|
199
231
|
const syncResult = it.tryNextSync();
|
|
200
232
|
signal?.throwIfAborted();
|
|
201
233
|
if (syncResult === undefined)
|
|
202
234
|
return undefined;
|
|
203
235
|
if (syncResult.done) {
|
|
204
|
-
finished = true;
|
|
236
|
+
this.finished = true;
|
|
205
237
|
return DONE_RESULT;
|
|
206
238
|
}
|
|
207
239
|
if (!(syncResult.value instanceof Uint8Array))
|
|
@@ -209,29 +241,30 @@ export function readBytes(source, signal) {
|
|
|
209
241
|
return syncResult;
|
|
210
242
|
}
|
|
211
243
|
catch (error) {
|
|
212
|
-
syncFailure = { reason: error };
|
|
244
|
+
this.syncFailure = { reason: error };
|
|
213
245
|
return undefined;
|
|
214
246
|
}
|
|
215
247
|
finally {
|
|
216
|
-
readingSync = false;
|
|
248
|
+
this.readingSync = false;
|
|
217
249
|
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
if (finished)
|
|
250
|
+
}
|
|
251
|
+
async _runNext() {
|
|
252
|
+
if (this.finished)
|
|
221
253
|
return DONE_RESULT;
|
|
222
254
|
try {
|
|
223
|
-
if (syncFailure) {
|
|
224
|
-
const { reason } = syncFailure;
|
|
225
|
-
syncFailure = undefined;
|
|
255
|
+
if (this.syncFailure) {
|
|
256
|
+
const { reason } = this.syncFailure;
|
|
257
|
+
this.syncFailure = undefined;
|
|
226
258
|
throw reason;
|
|
227
259
|
}
|
|
228
|
-
const it =
|
|
260
|
+
const it = this._ensureIterator();
|
|
261
|
+
const signal = this.abortSignal;
|
|
229
262
|
signal?.throwIfAborted();
|
|
230
263
|
const syncResult = typeof it.tryNextSync === "function" ? it.tryNextSync() : undefined;
|
|
231
|
-
const result = syncResult ?? (nativeAbort ? await it.next() : await abortable(() => it.next(), signal));
|
|
264
|
+
const result = syncResult ?? (this.nativeAbort ? await it.next() : await abortable(() => it.next(), signal));
|
|
232
265
|
signal?.throwIfAborted();
|
|
233
266
|
if (result.done) {
|
|
234
|
-
finished = true;
|
|
267
|
+
this.finished = true;
|
|
235
268
|
return DONE_RESULT;
|
|
236
269
|
}
|
|
237
270
|
if (!(result.value instanceof Uint8Array))
|
|
@@ -239,42 +272,35 @@ export function readBytes(source, signal) {
|
|
|
239
272
|
return result;
|
|
240
273
|
}
|
|
241
274
|
catch (error) {
|
|
242
|
-
await
|
|
275
|
+
await this._cleanupIterator(true);
|
|
243
276
|
throw error;
|
|
244
277
|
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
return(
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
return schedule(async () => {
|
|
274
|
-
await cleanupIterator(true);
|
|
275
|
-
throw error;
|
|
276
|
-
});
|
|
277
|
-
},
|
|
278
|
-
};
|
|
279
|
-
return gen;
|
|
278
|
+
}
|
|
279
|
+
next() {
|
|
280
|
+
if (this.finished && !this.turn && !this.readingSync && !this.syncFailure && !this.closing)
|
|
281
|
+
return RESOLVED_DONE;
|
|
282
|
+
return this._schedule(() => this._runNext());
|
|
283
|
+
}
|
|
284
|
+
return(value) {
|
|
285
|
+
if ((this.finished || !this.iterator?.return) && !this.turn && !this.readingSync && !this.closing && value === undefined) {
|
|
286
|
+
this.finished = true;
|
|
287
|
+
return RESOLVED_DONE;
|
|
288
|
+
}
|
|
289
|
+
return this._schedule(async () => {
|
|
290
|
+
await this._cleanupIterator(false);
|
|
291
|
+
return { done: true, value: await value };
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
throw(error) {
|
|
295
|
+
return this._schedule(async () => {
|
|
296
|
+
await this._cleanupIterator(true);
|
|
297
|
+
throw error;
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
export function readBytes(source, signal) {
|
|
302
|
+
if (signal !== undefined && source[readBytesSignal] === signal) {
|
|
303
|
+
return source;
|
|
304
|
+
}
|
|
305
|
+
return new ReadBytesGenerator(source, signal);
|
|
280
306
|
}
|
package/dist/safe-fs/core.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export * from "./fs/mount/index.js";
|
|
|
12
12
|
export * from "./fs/overlay/index.js";
|
|
13
13
|
export * from "./fs/quota/index.js";
|
|
14
14
|
export * from "./fs/object-publication/index.js";
|
|
15
|
-
export { scopeFileSystem, retainFileSystemCleanup } from "./fs/scoped.js";
|
|
15
|
+
export { retargetScopedFileSystem, scopeFileSystem, retainFileSystemCleanup } from "./fs/scoped.js";
|
|
16
16
|
export type { RetainedFileSystemCleanupView, RetainedFileSystemCleanupOptions } from "./fs/scoped.js";
|
|
17
17
|
export * from "./fs/webdav/index.js";
|
|
18
18
|
export * from "./bridge/index.js";
|
package/dist/safe-fs/core.js
CHANGED
|
@@ -11,7 +11,7 @@ export * from "./fs/mount/index.js";
|
|
|
11
11
|
export * from "./fs/overlay/index.js";
|
|
12
12
|
export * from "./fs/quota/index.js";
|
|
13
13
|
export * from "./fs/object-publication/index.js";
|
|
14
|
-
export { scopeFileSystem, retainFileSystemCleanup } from "./fs/scoped.js";
|
|
14
|
+
export { retargetScopedFileSystem, scopeFileSystem, retainFileSystemCleanup } from "./fs/scoped.js";
|
|
15
15
|
export * from "./fs/webdav/index.js";
|
|
16
16
|
export * from "./bridge/index.js";
|
|
17
17
|
export * from "./python/index.js";
|
|
@@ -105,7 +105,7 @@ export function readOnlyCapabilities(capabilities) {
|
|
|
105
105
|
"symlinks", "streamingRead", "open", "retainedRead", "versionedDescriptors",
|
|
106
106
|
].filter(name => capabilities[name] !== undefined).map(name => [name, capabilities[name]]));
|
|
107
107
|
return Object.freeze({
|
|
108
|
-
...inspection, retainedStagingCleanup: false, readOnly: true, write: false, append: false, exclusiveCreate: false,
|
|
108
|
+
...inspection, retainedStagingCleanup: false, retainedStagingWrite: false, readOnly: true, write: false, append: false, exclusiveCreate: false,
|
|
109
109
|
synchronousStagingResolution: false,
|
|
110
110
|
mkdir: false, recursiveMkdir: false, remove: false, removeDirectory: false, recursiveRemove: false,
|
|
111
111
|
rename: false, copy: false, exclusiveCopy: false, truncate: false, streamingAppend: false,
|
|
@@ -118,7 +118,7 @@ export function quotaCapabilities(capabilities) {
|
|
|
118
118
|
const streamingWrite = requireCapabilities(capabilities.write, capabilities.append, !capabilities.readOnly);
|
|
119
119
|
const streamingAppend = requireCapabilities(capabilities.append, !capabilities.readOnly);
|
|
120
120
|
const { streamingWrite: ignoredWrite, streamingAppend: ignoredAppend, ...rest } = capabilities;
|
|
121
|
-
return Object.freeze({ ...rest, synchronousStagingResolution: false, retainedStagingCleanup: false, atomicStagingAncestry: false, synchronousDirectoryValidation: false, guardedStagingPublication: false, atomicFilePublication: false, descriptorWriteStream: false, atomicResize: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicEntryRemovalReceipt: false, atomicFileStaging: false, atomicDirectoryMetadata: false, trustedOwnedStaging: false,
|
|
121
|
+
return Object.freeze({ ...rest, synchronousStagingResolution: false, retainedStagingCleanup: false, retainedStagingWrite: false, atomicStagingAncestry: false, synchronousDirectoryValidation: false, guardedStagingPublication: false, atomicFilePublication: false, descriptorWriteStream: false, atomicResize: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicEntryRemovalReceipt: false, atomicFileStaging: false, atomicDirectoryMetadata: false, trustedOwnedStaging: false,
|
|
122
122
|
...(streamingWrite === undefined ? {} : { streamingWrite }),
|
|
123
123
|
...(streamingAppend === undefined ? {} : { streamingAppend }),
|
|
124
124
|
});
|
|
@@ -143,6 +143,8 @@ export function ownedMutationCapabilities(filesystem, capabilities = filesystem.
|
|
|
143
143
|
(unavailable ??= {}).atomicFileStaging = false;
|
|
144
144
|
if (capabilities.retainedStagingCleanup === true && (capabilities.atomicFileStaging !== true || unavailable?.atomicFileStaging === false))
|
|
145
145
|
(unavailable ??= {}).retainedStagingCleanup = false;
|
|
146
|
+
if (capabilities.retainedStagingWrite === true && (capabilities.retainedStagingCleanup !== true || unavailable?.retainedStagingCleanup === false))
|
|
147
|
+
(unavailable ??= {}).retainedStagingWrite = false;
|
|
146
148
|
if (capabilities.atomicStagingAncestry === true && (capabilities.atomicFileStaging !== true || unavailable?.atomicFileStaging === false))
|
|
147
149
|
(unavailable ??= {}).atomicStagingAncestry = false;
|
|
148
150
|
if (capabilities.synchronousDirectoryValidation === true && typeof filesystem.prepareDirectoryAncestry !== "function")
|
|
@@ -1,7 +1,26 @@
|
|
|
1
1
|
import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, EntryComparison, FileReadHandle, FileResizeHandle, FileStat, FileSystem, FileSystemCapabilities, FsOptions, ChmodOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, FileDescriptor, OpenFileOptions, OpenReadFileOptions, OpenResizeFileOptions, WriteFileOptions, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, ConditionalRemoveEntryOptions, ConditionalRemoveEntryReceiptOptions, CreateStagedFileOptions, FileStaging, FileStagingEntry, FileStagingResolution, PublishStagedFileOptions, PrepareDirectoryOptions, StagedFileContent } from "../../contracts/filesystem.js";
|
|
2
2
|
import type { ByteSource } from "../../contracts/io.js";
|
|
3
|
+
import { MemoryAllocation } from "./ledger.js";
|
|
3
4
|
import { type MemoryFileSystemOptions } from "./limits.js";
|
|
4
5
|
export { defaultMemoryFileSystemLimits, type MemoryFileSystemLimits, type MemoryFileSystemOptions } from "./limits.js";
|
|
6
|
+
interface Metadata {
|
|
7
|
+
revision: number;
|
|
8
|
+
mode: number;
|
|
9
|
+
ino: number;
|
|
10
|
+
nlink: number;
|
|
11
|
+
atimeMs: number;
|
|
12
|
+
mtimeMs: number;
|
|
13
|
+
ctimeMs: number;
|
|
14
|
+
birthtimeMs: number;
|
|
15
|
+
references: number;
|
|
16
|
+
}
|
|
17
|
+
interface FileNode extends Metadata {
|
|
18
|
+
type: "file";
|
|
19
|
+
byteLength: number;
|
|
20
|
+
view: Uint8Array | undefined;
|
|
21
|
+
data: Uint8Array;
|
|
22
|
+
allocation: MemoryAllocation;
|
|
23
|
+
}
|
|
5
24
|
export interface ConditionalMutationBinding {
|
|
6
25
|
readonly path: string;
|
|
7
26
|
readonly parent: FileStat;
|
|
@@ -24,6 +43,7 @@ export declare class MemoryFileSystem implements FileSystem {
|
|
|
24
43
|
confineExtraction(roots: readonly string[], options?: FsOptions): Promise<FileSystem>;
|
|
25
44
|
private metadata;
|
|
26
45
|
private directory;
|
|
46
|
+
private addDirectoryNode;
|
|
27
47
|
private addNode;
|
|
28
48
|
private releaseNode;
|
|
29
49
|
private releaseReference;
|
|
@@ -50,6 +70,7 @@ export declare class MemoryFileSystem implements FileSystem {
|
|
|
50
70
|
private writeData;
|
|
51
71
|
private writeAt;
|
|
52
72
|
writeMemoryFileFast(path: string, data: Uint8Array, append: boolean, mode: number): void;
|
|
73
|
+
writeMemoryFileInDirFast(dirPrefix: string, name: string, data: Uint8Array, append: boolean, mode: number): void;
|
|
53
74
|
readFile(path: string, options?: ReadFileOptions): Promise<Uint8Array>;
|
|
54
75
|
private expectEntry;
|
|
55
76
|
private stagingLocations;
|
|
@@ -96,6 +117,18 @@ export declare class MemoryFileSystem implements FileSystem {
|
|
|
96
117
|
writeStream(path: string, source: ByteSource, options?: WriteFileOptions): Promise<void>;
|
|
97
118
|
}
|
|
98
119
|
export declare function createMemoryFileSystem(options?: MemoryFileSystemOptions | Readonly<Record<string, unknown>>): MemoryFileSystem;
|
|
120
|
+
export declare class MemoryRedirectHandle {
|
|
121
|
+
readonly fs: MemoryFileSystem;
|
|
122
|
+
readonly path: string;
|
|
123
|
+
readonly node: FileNode;
|
|
124
|
+
readonly append: boolean;
|
|
125
|
+
position: number;
|
|
126
|
+
closed: boolean;
|
|
127
|
+
constructor(fs: MemoryFileSystem, path: string, node: FileNode, append: boolean);
|
|
128
|
+
writeSync(chunk: Uint8Array, signal?: AbortSignal): void;
|
|
129
|
+
close(): void;
|
|
130
|
+
}
|
|
131
|
+
export declare function tryOpenMemoryRedirectHandleSync(filesystem: FileSystem, path: string, append: boolean, mode: number, signal?: AbortSignal): MemoryRedirectHandle | undefined;
|
|
99
132
|
export declare function utf8ByteLength(value: string): number;
|
|
100
133
|
export declare function isCleanAbsolutePath(path: string): boolean;
|
|
101
134
|
export declare function tryResolveMemoryDevicePath(filesystem: FileSystem, path: string, resizeCreate?: boolean): string | undefined;
|
|
@@ -104,3 +137,6 @@ export declare function tryGetMemoryDirectoryEntryNamesSync(filesystem: FileSyst
|
|
|
104
137
|
readonly type: "file" | "directory" | "symlink";
|
|
105
138
|
}> | undefined;
|
|
106
139
|
export declare function tryWriteMemoryFileSync(filesystem: FileSystem, path: string, data: Uint8Array, append: boolean, mode: number, signal?: AbortSignal): boolean;
|
|
140
|
+
export declare function tryMkdirMemorySync(filesystem: FileSystem, path: string, recursive: boolean, mode: number, signal?: AbortSignal): boolean;
|
|
141
|
+
export declare function tryRmRfMemorySync(filesystem: FileSystem, path: string, signal?: AbortSignal): boolean;
|
|
142
|
+
export declare function tryWriteMemoryFileInDirSync(filesystem: FileSystem, dirPrefix: string, name: string, data: Uint8Array, append: boolean, mode: number, signal?: AbortSignal): boolean;
|