@poe-platform/safe-fs 0.1.41 → 0.1.43
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
CHANGED
|
@@ -77,6 +77,7 @@ All required methods must exist, but a backend may reject an operation with `FsE
|
|
|
77
77
|
| `createReadOnlyFileSystem(filesystem)` | Rejecting writes through one view of an existing filesystem |
|
|
78
78
|
| `createMountFileSystem({ root, mounts })` | Routing absolute virtual mount paths to different filesystems |
|
|
79
79
|
| `createOverlayFileSystem({ upper, lower })` | Reading through to a lower layer and writing changes to an upper layer |
|
|
80
|
+
| `withFileSystemQuota(filesystem, { maxBytes })` | Enforcing a cumulative logical-byte ceiling across every write path |
|
|
80
81
|
|
|
81
82
|
`createNodeFsBridge` offers a promises-shaped subset, including recursive `cp` and `mkdtemp`. The portable `createFsBridge` from `@poe-platform/safe-fs/core` instead requires a caller-supplied text codec and returns `Uint8Array` values.
|
|
82
83
|
|
|
@@ -201,6 +202,7 @@ There are no package environment variables, implicit credentials, or automatic `
|
|
|
201
202
|
| Real | Required `root`: existing absolute host directory; the constructor/factory also accepts the root string directly. |
|
|
202
203
|
| Mount | Required `root`: fallback filesystem. `mounts` defaults to `{}` and maps absolute virtual paths to filesystems. |
|
|
203
204
|
| Overlay | Required `upper` and `lower`; `maxBufferBytes` defaults to 64 MiB. |
|
|
205
|
+
| Quota | `withFileSystemQuota` requires a nonnegative safe-integer `maxBytes`. It serializes mutations and counts files, symlinks, copies, hard links, truncation, and streaming writes. |
|
|
204
206
|
| Node bridge | `cwd` defaults to `/`, must be an absolute virtual path; optional lifetime `signal`. |
|
|
205
207
|
| Portable bridge | Same `cwd` and `signal`, plus required `codec` with `isEncoding`, `encode`, and `decode` functions. |
|
|
206
208
|
| Catalog example | Required `files`: a plain record of single-component filenames to text strings; no other options. |
|
package/dist/safe-fs/core.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export * from "./fs/memory/index.js";
|
|
|
7
7
|
export * from "./fs/readonly/index.js";
|
|
8
8
|
export * from "./fs/mount/index.js";
|
|
9
9
|
export * from "./fs/overlay/index.js";
|
|
10
|
+
export * from "./fs/quota/index.js";
|
|
10
11
|
export * from "./fs/webdav/index.js";
|
|
11
12
|
export * from "./bridge/index.js";
|
|
12
13
|
export { compareEntries } from "./fs/mount/comparison.js";
|
package/dist/safe-fs/core.js
CHANGED
|
@@ -7,6 +7,7 @@ export * from "./fs/memory/index.js";
|
|
|
7
7
|
export * from "./fs/readonly/index.js";
|
|
8
8
|
export * from "./fs/mount/index.js";
|
|
9
9
|
export * from "./fs/overlay/index.js";
|
|
10
|
+
export * from "./fs/quota/index.js";
|
|
10
11
|
export * from "./fs/webdav/index.js";
|
|
11
12
|
export * from "./bridge/index.js";
|
|
12
13
|
export { compareEntries } from "./fs/mount/comparison.js";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { FileSystem } from "../../contracts/filesystem.js";
|
|
2
|
+
export interface FileSystemQuotaOptions {
|
|
3
|
+
readonly maxBytes: number;
|
|
4
|
+
}
|
|
5
|
+
export declare class FileSystemQuotaError extends Error {
|
|
6
|
+
readonly maxBytes: number;
|
|
7
|
+
constructor(maxBytes: number);
|
|
8
|
+
}
|
|
9
|
+
export declare function withFileSystemQuota(fs: FileSystem, options: FileSystemQuotaOptions): FileSystem;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
export class FileSystemQuotaError extends Error {
|
|
2
|
+
maxBytes;
|
|
3
|
+
constructor(maxBytes) {
|
|
4
|
+
super(`Filesystem quota exceeded (${maxBytes} bytes)`);
|
|
5
|
+
this.maxBytes = maxBytes;
|
|
6
|
+
this.name = "FileSystemQuotaError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
async function usedBytes(fs, options) {
|
|
10
|
+
let total = 0;
|
|
11
|
+
const pending = ["/"];
|
|
12
|
+
while (pending.length) {
|
|
13
|
+
const directory = pending.pop();
|
|
14
|
+
for (const entry of await fs.readdir(directory, options)) {
|
|
15
|
+
const path = `${directory === "/" ? "" : directory}/${entry.name}`;
|
|
16
|
+
if (entry.type === "directory")
|
|
17
|
+
pending.push(path);
|
|
18
|
+
else
|
|
19
|
+
total += (await fs.lstat(path, options)).size;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return total;
|
|
23
|
+
}
|
|
24
|
+
async function existingBytes(fs, path, options) {
|
|
25
|
+
try {
|
|
26
|
+
const stat = await fs.lstat(path, options);
|
|
27
|
+
return stat.type === "directory" ? 0 : stat.size;
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
31
|
+
return 0;
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function withFileSystemQuota(fs, options) {
|
|
36
|
+
if (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 0)
|
|
37
|
+
throw new RangeError("maxBytes must be a nonnegative safe integer");
|
|
38
|
+
let queue = Promise.resolve();
|
|
39
|
+
const mutate = (operation) => {
|
|
40
|
+
const result = queue.then(operation);
|
|
41
|
+
queue = result.catch(() => undefined);
|
|
42
|
+
return result;
|
|
43
|
+
};
|
|
44
|
+
const assertDelta = async (path, nextBytes, fsOptions) => {
|
|
45
|
+
const current = await existingBytes(fs, path, fsOptions);
|
|
46
|
+
if (await usedBytes(fs, fsOptions) - current + nextBytes > options.maxBytes)
|
|
47
|
+
throw new FileSystemQuotaError(options.maxBytes);
|
|
48
|
+
};
|
|
49
|
+
const mutations = {
|
|
50
|
+
writeFile(path, data, writeOptions) {
|
|
51
|
+
return mutate(async () => {
|
|
52
|
+
const append = writeOptions?.flag === "a" || writeOptions?.flag === "ax";
|
|
53
|
+
const current = append ? await existingBytes(fs, path, writeOptions) : 0;
|
|
54
|
+
await assertDelta(path, current + data.length, writeOptions);
|
|
55
|
+
await fs.writeFile(path, data, writeOptions);
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
appendFile(path, data, appendOptions) {
|
|
59
|
+
return mutate(async () => {
|
|
60
|
+
await assertDelta(path, await existingBytes(fs, path, appendOptions) + data.length, appendOptions);
|
|
61
|
+
await fs.appendFile(path, data, appendOptions);
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
copyFile(source, destination, copyOptions) {
|
|
65
|
+
return mutate(async () => {
|
|
66
|
+
await assertDelta(destination, (await fs.stat(source, copyOptions)).size, copyOptions);
|
|
67
|
+
await fs.copyFile(source, destination, copyOptions);
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
truncate(path, length = 0, truncateOptions) {
|
|
71
|
+
return mutate(async () => {
|
|
72
|
+
await assertDelta(path, length, truncateOptions);
|
|
73
|
+
await fs.truncate(path, length, truncateOptions);
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
link(source, destination, linkOptions) {
|
|
77
|
+
return mutate(async () => {
|
|
78
|
+
await assertDelta(destination, (await fs.stat(source, linkOptions)).size, linkOptions);
|
|
79
|
+
await fs.link(source, destination, linkOptions);
|
|
80
|
+
});
|
|
81
|
+
},
|
|
82
|
+
symlink(target, path, linkOptions) {
|
|
83
|
+
return mutate(async () => {
|
|
84
|
+
await assertDelta(path, new TextEncoder().encode(target).length, linkOptions);
|
|
85
|
+
await fs.symlink(target, path, linkOptions);
|
|
86
|
+
});
|
|
87
|
+
},
|
|
88
|
+
writeStream(path, source, writeOptions) {
|
|
89
|
+
return mutate(async () => {
|
|
90
|
+
const append = writeOptions?.flag === "a" || writeOptions?.flag === "ax";
|
|
91
|
+
if (!append)
|
|
92
|
+
await fs.writeFile(path, new Uint8Array(), writeOptions);
|
|
93
|
+
for await (const chunk of source) {
|
|
94
|
+
await assertDelta(path, await existingBytes(fs, path, writeOptions) + chunk.length, writeOptions);
|
|
95
|
+
await fs.appendFile(path, chunk, writeOptions);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
return new Proxy(fs, {
|
|
101
|
+
get(target, property) {
|
|
102
|
+
const replacement = Reflect.get(mutations, property);
|
|
103
|
+
if (typeof replacement === "function")
|
|
104
|
+
return replacement;
|
|
105
|
+
const original = Reflect.get(target, property);
|
|
106
|
+
return typeof original === "function" ? original.bind(target) : original;
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
}
|
package/dist/safe-fs/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export * from "./fs/webdav/index.js";
|
|
|
8
8
|
export * from "./fs/readonly/index.js";
|
|
9
9
|
export * from "./fs/mount/index.js";
|
|
10
10
|
export * from "./fs/overlay/index.js";
|
|
11
|
+
export * from "./fs/quota/index.js";
|
|
11
12
|
export * from "./node/index.js";
|
|
12
13
|
export { createFileSystem, readConfigRecord, validateFileSystemConfig } from "./config.js";
|
|
13
14
|
export type { FileSystemConfig, FileSystemAdapterDescriptor, FileSystemAdapterRegistry } from "./config.js";
|
package/dist/safe-fs/index.js
CHANGED
|
@@ -7,6 +7,7 @@ export * from "./fs/webdav/index.js";
|
|
|
7
7
|
export * from "./fs/readonly/index.js";
|
|
8
8
|
export * from "./fs/mount/index.js";
|
|
9
9
|
export * from "./fs/overlay/index.js";
|
|
10
|
+
export * from "./fs/quota/index.js";
|
|
10
11
|
export * from "./node/index.js";
|
|
11
12
|
export { createFileSystem, readConfigRecord, validateFileSystemConfig } from "./config.js";
|
|
12
13
|
export { createNodeFileSystemAdapterRegistry } from "./config.node.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poe-platform/safe-fs",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.43",
|
|
4
4
|
"description": "Composable filesystem with a portable core and explicit Node adapters",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -82,6 +82,10 @@
|
|
|
82
82
|
"./fs/overlay": {
|
|
83
83
|
"types": "./dist/safe-fs/fs/overlay/index.d.ts",
|
|
84
84
|
"import": "./dist/safe-fs/fs/overlay/index.js"
|
|
85
|
+
},
|
|
86
|
+
"./fs/quota": {
|
|
87
|
+
"types": "./dist/safe-fs/fs/quota/index.d.ts",
|
|
88
|
+
"import": "./dist/safe-fs/fs/quota/index.js"
|
|
85
89
|
}
|
|
86
90
|
},
|
|
87
91
|
"repository": {
|