@openclaw/fs-safe 0.1.0
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/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +390 -0
- package/SECURITY.md +12 -0
- package/dist/absolute-path.d.ts +19 -0
- package/dist/absolute-path.d.ts.map +1 -0
- package/dist/absolute-path.js +98 -0
- package/dist/advanced.d.ts +22 -0
- package/dist/advanced.d.ts.map +1 -0
- package/dist/advanced.js +24 -0
- package/dist/archive-entry.d.ts +13 -0
- package/dist/archive-entry.d.ts.map +1 -0
- package/dist/archive-entry.js +47 -0
- package/dist/archive-kind.d.ts +8 -0
- package/dist/archive-kind.d.ts.map +1 -0
- package/dist/archive-kind.js +58 -0
- package/dist/archive-limits.d.ts +40 -0
- package/dist/archive-limits.d.ts.map +1 -0
- package/dist/archive-limits.js +92 -0
- package/dist/archive-staging.d.ts +26 -0
- package/dist/archive-staging.d.ts.map +1 -0
- package/dist/archive-staging.js +171 -0
- package/dist/archive-tar.d.ts +14 -0
- package/dist/archive-tar.d.ts.map +1 -0
- package/dist/archive-tar.js +52 -0
- package/dist/archive-utils.d.ts +3 -0
- package/dist/archive-utils.d.ts.map +1 -0
- package/dist/archive-utils.js +26 -0
- package/dist/archive-zip-preflight.d.ts +7 -0
- package/dist/archive-zip-preflight.d.ts.map +1 -0
- package/dist/archive-zip-preflight.js +170 -0
- package/dist/archive.d.ts +24 -0
- package/dist/archive.d.ts.map +1 -0
- package/dist/archive.js +251 -0
- package/dist/async-lock.d.ts +2 -0
- package/dist/async-lock.d.ts.map +1 -0
- package/dist/async-lock.js +17 -0
- package/dist/atomic.d.ts +5 -0
- package/dist/atomic.d.ts.map +1 -0
- package/dist/atomic.js +4 -0
- package/dist/boundary-file-read.d.ts +44 -0
- package/dist/boundary-file-read.d.ts.map +1 -0
- package/dist/boundary-file-read.js +129 -0
- package/dist/boundary-path.d.ts +39 -0
- package/dist/boundary-path.d.ts.map +1 -0
- package/dist/boundary-path.js +598 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +1 -0
- package/dist/errors.d.ts +11 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +20 -0
- package/dist/file-identity.d.ts +6 -0
- package/dist/file-identity.d.ts.map +1 -0
- package/dist/file-identity.js +14 -0
- package/dist/file-lock.d.ts +20 -0
- package/dist/file-lock.d.ts.map +1 -0
- package/dist/file-lock.js +34 -0
- package/dist/file-store.d.ts +66 -0
- package/dist/file-store.d.ts.map +1 -0
- package/dist/file-store.js +436 -0
- package/dist/file-url.d.ts +2 -0
- package/dist/file-url.d.ts.map +1 -0
- package/dist/file-url.js +1 -0
- package/dist/filename.d.ts +2 -0
- package/dist/filename.d.ts.map +1 -0
- package/dist/filename.js +25 -0
- package/dist/fs-pinned-path-helper.d.ts +7 -0
- package/dist/fs-pinned-path-helper.d.ts.map +1 -0
- package/dist/fs-pinned-path-helper.js +182 -0
- package/dist/fs-pinned-write-helper.d.ts +21 -0
- package/dist/fs-pinned-write-helper.d.ts.map +1 -0
- package/dist/fs-pinned-write-helper.js +263 -0
- package/dist/fs.d.ts +13 -0
- package/dist/fs.d.ts.map +1 -0
- package/dist/fs.js +30 -0
- package/dist/hardlink-guards.d.ts +7 -0
- package/dist/hardlink-guards.d.ts.map +1 -0
- package/dist/hardlink-guards.js +30 -0
- package/dist/home-dir.d.ts +22 -0
- package/dist/home-dir.d.ts.map +1 -0
- package/dist/home-dir.js +107 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/install-path.d.ts +20 -0
- package/dist/install-path.d.ts.map +1 -0
- package/dist/install-path.js +94 -0
- package/dist/install-safe-path.d.ts +20 -0
- package/dist/install-safe-path.d.ts.map +1 -0
- package/dist/install-safe-path.js +94 -0
- package/dist/json-document-store.d.ts +30 -0
- package/dist/json-document-store.d.ts.map +1 -0
- package/dist/json-document-store.js +68 -0
- package/dist/json-file.d.ts +3 -0
- package/dist/json-file.d.ts.map +1 -0
- package/dist/json-file.js +123 -0
- package/dist/json-files.d.ts +20 -0
- package/dist/json-files.d.ts.map +1 -0
- package/dist/json-files.js +153 -0
- package/dist/json-store.d.ts +9 -0
- package/dist/json-store.d.ts.map +1 -0
- package/dist/json-store.js +12 -0
- package/dist/json.d.ts +17 -0
- package/dist/json.d.ts.map +1 -0
- package/dist/json.js +198 -0
- package/dist/local-file-access.d.ts +8 -0
- package/dist/local-file-access.d.ts.map +1 -0
- package/dist/local-file-access.js +73 -0
- package/dist/local-roots.d.ts +26 -0
- package/dist/local-roots.d.ts.map +1 -0
- package/dist/local-roots.js +150 -0
- package/dist/mode.d.ts +2 -0
- package/dist/mode.d.ts.map +1 -0
- package/dist/mode.js +3 -0
- package/dist/move-path.d.ts +6 -0
- package/dist/move-path.d.ts.map +1 -0
- package/dist/move-path.js +18 -0
- package/dist/path-alias-guards.d.ts +19 -0
- package/dist/path-alias-guards.d.ts.map +1 -0
- package/dist/path-alias-guards.js +21 -0
- package/dist/path-guards.d.ts +7 -0
- package/dist/path-guards.d.ts.map +1 -0
- package/dist/path-guards.js +49 -0
- package/dist/path-policy.d.ts +25 -0
- package/dist/path-policy.d.ts.map +1 -0
- package/dist/path-policy.js +50 -0
- package/dist/path-safety.d.ts +12 -0
- package/dist/path-safety.d.ts.map +1 -0
- package/dist/path-safety.js +50 -0
- package/dist/path-scope.d.ts +2 -0
- package/dist/path-scope.d.ts.map +1 -0
- package/dist/path-scope.js +1 -0
- package/dist/path.d.ts +18 -0
- package/dist/path.d.ts.map +1 -0
- package/dist/path.js +123 -0
- package/dist/permissions-public.d.ts +2 -0
- package/dist/permissions-public.d.ts.map +1 -0
- package/dist/permissions-public.js +1 -0
- package/dist/permissions.d.ts +86 -0
- package/dist/permissions.d.ts.map +1 -0
- package/dist/permissions.js +435 -0
- package/dist/pinned-helper.d.ts +9 -0
- package/dist/pinned-helper.d.ts.map +1 -0
- package/dist/pinned-helper.js +19 -0
- package/dist/pinned-open.d.ts +24 -0
- package/dist/pinned-open.d.ts.map +1 -0
- package/dist/pinned-open.js +71 -0
- package/dist/pinned-path.d.ts +7 -0
- package/dist/pinned-path.d.ts.map +1 -0
- package/dist/pinned-path.js +21 -0
- package/dist/pinned-python-config.d.ts +9 -0
- package/dist/pinned-python-config.d.ts.map +1 -0
- package/dist/pinned-python-config.js +39 -0
- package/dist/pinned-python.d.ts +12 -0
- package/dist/pinned-python.d.ts.map +1 -0
- package/dist/pinned-python.js +599 -0
- package/dist/pinned-write.d.ts +33 -0
- package/dist/pinned-write.d.ts.map +1 -0
- package/dist/pinned-write.js +198 -0
- package/dist/private-file-store.d.ts +60 -0
- package/dist/private-file-store.d.ts.map +1 -0
- package/dist/private-file-store.js +233 -0
- package/dist/private-temp-workspace.d.ts +39 -0
- package/dist/private-temp-workspace.d.ts.map +1 -0
- package/dist/private-temp-workspace.js +185 -0
- package/dist/regular-file.d.ts +38 -0
- package/dist/regular-file.d.ts.map +1 -0
- package/dist/regular-file.js +239 -0
- package/dist/replace-directory.d.ts +7 -0
- package/dist/replace-directory.d.ts.map +1 -0
- package/dist/replace-directory.js +32 -0
- package/dist/replace-file.d.ts +41 -0
- package/dist/replace-file.d.ts.map +1 -0
- package/dist/replace-file.js +325 -0
- package/dist/root-file.d.ts +44 -0
- package/dist/root-file.d.ts.map +1 -0
- package/dist/root-file.js +129 -0
- package/dist/root-path.d.ts +39 -0
- package/dist/root-path.d.ts.map +1 -0
- package/dist/root-path.js +598 -0
- package/dist/root-paths.d.ts +92 -0
- package/dist/root-paths.d.ts.map +1 -0
- package/dist/root-paths.js +338 -0
- package/dist/root.d.ts +108 -0
- package/dist/root.d.ts.map +1 -0
- package/dist/root.js +1332 -0
- package/dist/safe-open-sync.d.ts +24 -0
- package/dist/safe-open-sync.d.ts.map +1 -0
- package/dist/safe-open-sync.js +71 -0
- package/dist/safe-root.d.ts +123 -0
- package/dist/safe-root.d.ts.map +1 -0
- package/dist/safe-root.js +1060 -0
- package/dist/secret-file.d.ts +17 -0
- package/dist/secret-file.d.ts.map +1 -0
- package/dist/secret-file.js +232 -0
- package/dist/secret.d.ts +2 -0
- package/dist/secret.d.ts.map +1 -0
- package/dist/secret.js +1 -0
- package/dist/secure-file.d.ts +32 -0
- package/dist/secure-file.d.ts.map +1 -0
- package/dist/secure-file.js +163 -0
- package/dist/secure-temp-dir.d.ts +27 -0
- package/dist/secure-temp-dir.d.ts.map +1 -0
- package/dist/secure-temp-dir.js +155 -0
- package/dist/secure-temp-workspace.d.ts +25 -0
- package/dist/secure-temp-workspace.d.ts.map +1 -0
- package/dist/secure-temp-workspace.js +136 -0
- package/dist/sibling-temp-file.d.ts +16 -0
- package/dist/sibling-temp-file.d.ts.map +1 -0
- package/dist/sibling-temp-file.js +73 -0
- package/dist/sibling-temp-write.d.ts +8 -0
- package/dist/sibling-temp-write.d.ts.map +1 -0
- package/dist/sibling-temp-write.js +40 -0
- package/dist/sibling-temp.d.ts +23 -0
- package/dist/sibling-temp.d.ts.map +1 -0
- package/dist/sibling-temp.js +119 -0
- package/dist/sidecar-lock.d.ts +50 -0
- package/dist/sidecar-lock.d.ts.map +1 -0
- package/dist/sidecar-lock.js +235 -0
- package/dist/store.d.ts +3 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +2 -0
- package/dist/string-coerce.d.ts +14 -0
- package/dist/string-coerce.d.ts.map +1 -0
- package/dist/string-coerce.js +72 -0
- package/dist/symlink-parents.d.ts +12 -0
- package/dist/symlink-parents.d.ts.map +1 -0
- package/dist/symlink-parents.js +78 -0
- package/dist/temp-cleanup.d.ts +6 -0
- package/dist/temp-cleanup.d.ts.map +1 -0
- package/dist/temp-cleanup.js +42 -0
- package/dist/temp-target.d.ts +28 -0
- package/dist/temp-target.d.ts.map +1 -0
- package/dist/temp-target.js +84 -0
- package/dist/temp.d.ts +3 -0
- package/dist/temp.d.ts.map +1 -0
- package/dist/temp.js +2 -0
- package/dist/test-hooks.d.ts +9 -0
- package/dist/test-hooks.d.ts.map +1 -0
- package/dist/test-hooks.js +13 -0
- package/dist/text-atomic.d.ts +7 -0
- package/dist/text-atomic.d.ts.map +1 -0
- package/dist/text-atomic.js +13 -0
- package/dist/timing.d.ts +6 -0
- package/dist/timing.d.ts.map +1 -0
- package/dist/timing.js +24 -0
- package/dist/trash.d.ts +5 -0
- package/dist/trash.d.ts.map +1 -0
- package/dist/trash.js +127 -0
- package/dist/types.d.ts +23 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/walk.d.ts +26 -0
- package/dist/walk.d.ts.map +1 -0
- package/dist/walk.js +169 -0
- package/dist/write-queue.d.ts +2 -0
- package/dist/write-queue.d.ts.map +1 -0
- package/dist/write-queue.js +18 -0
- package/package.json +117 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-identity.d.ts","sourceRoot":"","sources":["../src/file-identity.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;IACrB,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;CACtB,CAAC;AAMF,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,gBAAgB,EACtB,KAAK,EAAE,gBAAgB,EACvB,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAC3C,OAAO,CAWT"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
function isZero(value) {
|
|
2
|
+
return value === 0 || value === 0n;
|
|
3
|
+
}
|
|
4
|
+
export function sameFileIdentity(left, right, platform = process.platform) {
|
|
5
|
+
if (left.ino !== right.ino) {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
// On Windows, path-based stat calls can report dev=0 while fd-based stat
|
|
9
|
+
// reports a real volume serial; treat either-side dev=0 as "unknown device".
|
|
10
|
+
if (left.dev === right.dev) {
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
return platform === "win32" && (isZero(left.dev) || isZero(right.dev));
|
|
14
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type SidecarLockAcquireOptions, type SidecarLockHandle, type SidecarLockHeldEntry, type SidecarLockRetryOptions } from "./sidecar-lock.js";
|
|
2
|
+
export type FileLockRetryOptions = SidecarLockRetryOptions;
|
|
3
|
+
export type FileLockAcquireOptions<TPayload extends Record<string, unknown>> = Omit<SidecarLockAcquireOptions<TPayload>, "targetPath"> & {
|
|
4
|
+
managerKey?: string;
|
|
5
|
+
};
|
|
6
|
+
export type FileLockHandle = SidecarLockHandle;
|
|
7
|
+
export type FileLockHeldEntry = SidecarLockHeldEntry;
|
|
8
|
+
export type FileLockManager = {
|
|
9
|
+
acquire<TPayload extends Record<string, unknown>>(targetPath: string, options: FileLockAcquireOptions<TPayload>): Promise<FileLockHandle>;
|
|
10
|
+
withLock<T, TPayload extends Record<string, unknown>>(targetPath: string, options: FileLockAcquireOptions<TPayload>, fn: () => Promise<T>): Promise<T>;
|
|
11
|
+
drain(): Promise<void>;
|
|
12
|
+
reset(): void;
|
|
13
|
+
heldEntries(): FileLockHeldEntry[];
|
|
14
|
+
};
|
|
15
|
+
export declare function acquireFileLock<TPayload extends Record<string, unknown>>(targetPath: string, options: FileLockAcquireOptions<TPayload>): Promise<FileLockHandle>;
|
|
16
|
+
export declare function withFileLock<T, TPayload extends Record<string, unknown>>(targetPath: string, options: FileLockAcquireOptions<TPayload>, fn: () => Promise<T>): Promise<T>;
|
|
17
|
+
export declare function createFileLockManager(key: string): FileLockManager;
|
|
18
|
+
export declare function drainFileLockManagerForTest(targetPath: string, managerKey?: string): Promise<void>;
|
|
19
|
+
export declare function resetFileLockManagerForTest(targetPath: string, managerKey?: string): void;
|
|
20
|
+
//# sourceMappingURL=file-lock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-lock.d.ts","sourceRoot":"","sources":["../src/file-lock.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,yBAAyB,EAC9B,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EACzB,KAAK,uBAAuB,EAC7B,MAAM,mBAAmB,CAAC;AAE3B,MAAM,MAAM,oBAAoB,GAAG,uBAAuB,CAAC;AAE3D,MAAM,MAAM,sBAAsB,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,IAAI,CACjF,yBAAyB,CAAC,QAAQ,CAAC,EACnC,YAAY,CACb,GAAG;IACF,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG,iBAAiB,CAAC;AAC/C,MAAM,MAAM,iBAAiB,GAAG,oBAAoB,CAAC;AAErD,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9C,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,sBAAsB,CAAC,QAAQ,CAAC,GACxC,OAAO,CAAC,cAAc,CAAC,CAAC;IAC3B,QAAQ,CAAC,CAAC,EAAE,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAClD,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,sBAAsB,CAAC,QAAQ,CAAC,EACzC,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACnB,OAAO,CAAC,CAAC,CAAC,CAAC;IACd,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,KAAK,IAAI,IAAI,CAAC;IACd,WAAW,IAAI,iBAAiB,EAAE,CAAC;CACpC,CAAC;AAMF,wBAAsB,eAAe,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5E,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,sBAAsB,CAAC,QAAQ,CAAC,GACxC,OAAO,CAAC,cAAc,CAAC,CAGzB;AAED,wBAAsB,YAAY,CAAC,CAAC,EAAE,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5E,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,sBAAsB,CAAC,QAAQ,CAAC,EACzC,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACnB,OAAO,CAAC,CAAC,CAAC,CAGZ;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,CAelE;AAED,wBAAsB,2BAA2B,CAC/C,UAAU,EAAE,MAAM,EAClB,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,IAAI,CAAC,CAEf;AAED,wBAAgB,2BAA2B,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAEzF"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createSidecarLockManager, } from "./sidecar-lock.js";
|
|
2
|
+
function resolveFileLockManagerKey(targetPath, managerKey) {
|
|
3
|
+
return managerKey ?? `fs-safe.file-lock:${targetPath}`;
|
|
4
|
+
}
|
|
5
|
+
export async function acquireFileLock(targetPath, options) {
|
|
6
|
+
return await createFileLockManager(resolveFileLockManagerKey(targetPath, options.managerKey))
|
|
7
|
+
.acquire(targetPath, options);
|
|
8
|
+
}
|
|
9
|
+
export async function withFileLock(targetPath, options, fn) {
|
|
10
|
+
return await createFileLockManager(resolveFileLockManagerKey(targetPath, options.managerKey))
|
|
11
|
+
.withLock(targetPath, options, fn);
|
|
12
|
+
}
|
|
13
|
+
export function createFileLockManager(key) {
|
|
14
|
+
const manager = createSidecarLockManager(key);
|
|
15
|
+
return {
|
|
16
|
+
acquire: async (targetPath, options) => {
|
|
17
|
+
const { managerKey: _managerKey, ...acquireOptions } = options;
|
|
18
|
+
return await manager.acquire({ ...acquireOptions, targetPath });
|
|
19
|
+
},
|
|
20
|
+
withLock: async (targetPath, options, fn) => {
|
|
21
|
+
const { managerKey: _managerKey, ...acquireOptions } = options;
|
|
22
|
+
return await manager.withLock({ ...acquireOptions, targetPath }, fn);
|
|
23
|
+
},
|
|
24
|
+
drain: manager.drain,
|
|
25
|
+
reset: manager.reset,
|
|
26
|
+
heldEntries: manager.heldEntries,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export async function drainFileLockManagerForTest(targetPath, managerKey) {
|
|
30
|
+
await createFileLockManager(resolveFileLockManagerKey(targetPath, managerKey)).drain();
|
|
31
|
+
}
|
|
32
|
+
export function resetFileLockManagerForTest(targetPath, managerKey) {
|
|
33
|
+
createFileLockManager(resolveFileLockManagerKey(targetPath, managerKey)).reset();
|
|
34
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { Readable } from "node:stream";
|
|
2
|
+
import { type JsonFileStoreOptions, type JsonStore } from "./json-document-store.js";
|
|
3
|
+
import { type OpenResult, type ReadResult, type Root, type RootReadOptions } from "./root.js";
|
|
4
|
+
export type FileStoreOptions = {
|
|
5
|
+
rootDir: string;
|
|
6
|
+
private?: boolean;
|
|
7
|
+
dirMode?: number;
|
|
8
|
+
mode?: number;
|
|
9
|
+
maxBytes?: number;
|
|
10
|
+
};
|
|
11
|
+
export type FileStoreWriteOptions = {
|
|
12
|
+
dirMode?: number;
|
|
13
|
+
mode?: number;
|
|
14
|
+
maxBytes?: number;
|
|
15
|
+
tempPrefix?: string;
|
|
16
|
+
};
|
|
17
|
+
export type FileStoreReadOptions = RootReadOptions & {
|
|
18
|
+
encoding?: BufferEncoding;
|
|
19
|
+
};
|
|
20
|
+
export type FileStorePruneOptions = {
|
|
21
|
+
ttlMs: number;
|
|
22
|
+
recursive?: boolean;
|
|
23
|
+
maxDepth?: number;
|
|
24
|
+
pruneEmptyDirs?: boolean;
|
|
25
|
+
};
|
|
26
|
+
export type FileStore = {
|
|
27
|
+
readonly rootDir: string;
|
|
28
|
+
path(relativePath: string): string;
|
|
29
|
+
root(): Promise<Root>;
|
|
30
|
+
write(relativePath: string, data: string | Uint8Array, options?: FileStoreWriteOptions): Promise<string>;
|
|
31
|
+
writeStream(relativePath: string, stream: Readable, options?: FileStoreWriteOptions): Promise<string>;
|
|
32
|
+
copyIn(relativePath: string, sourcePath: string, options?: FileStoreWriteOptions): Promise<string>;
|
|
33
|
+
open(relativePath: string, options?: RootReadOptions): Promise<OpenResult>;
|
|
34
|
+
read(relativePath: string, options?: RootReadOptions): Promise<ReadResult>;
|
|
35
|
+
readBytes(relativePath: string, options?: RootReadOptions): Promise<Buffer>;
|
|
36
|
+
readText(relativePath: string, options?: FileStoreReadOptions): Promise<string>;
|
|
37
|
+
readTextIfExists(relativePath: string, options?: FileStoreReadOptions): Promise<string | null>;
|
|
38
|
+
readJson<T = unknown>(relativePath: string, options?: FileStoreReadOptions): Promise<T>;
|
|
39
|
+
readJsonIfExists<T = unknown>(relativePath: string, options?: FileStoreReadOptions): Promise<T | null>;
|
|
40
|
+
remove(relativePath: string): Promise<void>;
|
|
41
|
+
exists(relativePath: string): Promise<boolean>;
|
|
42
|
+
writeText(relativePath: string, data: string | Uint8Array, options?: FileStoreWriteOptions): Promise<string>;
|
|
43
|
+
writeJson(relativePath: string, data: unknown, options?: FileStoreWriteOptions & {
|
|
44
|
+
trailingNewline?: boolean;
|
|
45
|
+
}): Promise<string>;
|
|
46
|
+
json<T = unknown>(relativePath: string, options?: JsonFileStoreOptions): JsonStore<T>;
|
|
47
|
+
pruneExpired(options: FileStorePruneOptions): Promise<void>;
|
|
48
|
+
};
|
|
49
|
+
export type FileStoreSync = {
|
|
50
|
+
readonly rootDir: string;
|
|
51
|
+
path(relativePath: string): string;
|
|
52
|
+
readTextIfExists(relativePath: string, options?: {
|
|
53
|
+
maxBytes?: number;
|
|
54
|
+
}): string | null;
|
|
55
|
+
readJsonIfExists<T = unknown>(relativePath: string, options?: {
|
|
56
|
+
maxBytes?: number;
|
|
57
|
+
}): T | null;
|
|
58
|
+
write(relativePath: string, data: string | Uint8Array, options?: FileStoreWriteOptions): string;
|
|
59
|
+
writeText(relativePath: string, data: string | Uint8Array, options?: FileStoreWriteOptions): string;
|
|
60
|
+
writeJson(relativePath: string, data: unknown, options?: FileStoreWriteOptions & {
|
|
61
|
+
trailingNewline?: boolean;
|
|
62
|
+
}): string;
|
|
63
|
+
};
|
|
64
|
+
export declare function fileStore(options: FileStoreOptions): FileStore;
|
|
65
|
+
export declare function fileStoreSync(options: FileStoreOptions): FileStoreSync;
|
|
66
|
+
//# sourceMappingURL=file-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-store.d.ts","sourceRoot":"","sources":["../src/file-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import syncFs from "node:fs";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { pipeline } from "node:stream/promises";
|
|
6
|
+
import { FsSafeError } from "./errors.js";
|
|
7
|
+
import { createJsonStore } from "./json-document-store.js";
|
|
8
|
+
import { isPathInside, resolveSafeRelativePath } from "./path.js";
|
|
9
|
+
import { root } from "./root.js";
|
|
10
|
+
import { writeSecretFileAtomic } from "./secret-file.js";
|
|
11
|
+
import { writeSiblingTempFile } from "./sibling-temp.js";
|
|
12
|
+
function assertRelativePath(relativePath) {
|
|
13
|
+
const raw = relativePath.trim();
|
|
14
|
+
if (!raw) {
|
|
15
|
+
throw new FsSafeError("invalid-path", "relative path must be non-empty");
|
|
16
|
+
}
|
|
17
|
+
return raw.replaceAll("\\", "/");
|
|
18
|
+
}
|
|
19
|
+
function resolveStorePath(rootDir, relativePath) {
|
|
20
|
+
return resolveSafeRelativePath(rootDir, assertRelativePath(relativePath));
|
|
21
|
+
}
|
|
22
|
+
function assertStoreFilePath(rootDir, filePath) {
|
|
23
|
+
if (!isPathInside(rootDir, filePath)) {
|
|
24
|
+
throw new FsSafeError("outside-workspace", "file path escapes store root");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function ensureParent(filePath, mode) {
|
|
28
|
+
const dir = path.dirname(filePath);
|
|
29
|
+
await fs.mkdir(dir, { recursive: true, mode });
|
|
30
|
+
await fs.chmod(dir, mode).catch(() => undefined);
|
|
31
|
+
}
|
|
32
|
+
function assertMaxBytes(size, maxBytes) {
|
|
33
|
+
if (maxBytes !== undefined && size > maxBytes) {
|
|
34
|
+
throw new FsSafeError("too-large", `file exceeds maximum size of ${maxBytes} bytes`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function isNotFound(error) {
|
|
38
|
+
return error instanceof FsSafeError
|
|
39
|
+
? error.code === "not-found"
|
|
40
|
+
: error.code === "ENOENT" ||
|
|
41
|
+
error.code === "ENOTDIR";
|
|
42
|
+
}
|
|
43
|
+
async function copyIntoRoot(params) {
|
|
44
|
+
const destination = resolveStorePath(params.rootDir, params.relativePath);
|
|
45
|
+
const sourceStat = await fs.lstat(params.sourcePath);
|
|
46
|
+
if (sourceStat.isSymbolicLink() || !sourceStat.isFile()) {
|
|
47
|
+
throw new FsSafeError("not-file", "source path is not a file");
|
|
48
|
+
}
|
|
49
|
+
assertMaxBytes(sourceStat.size, params.maxBytes);
|
|
50
|
+
await ensureParent(destination, params.dirMode ?? 0o700);
|
|
51
|
+
const result = await writeSiblingTempFile({
|
|
52
|
+
dir: path.dirname(destination),
|
|
53
|
+
dirMode: params.dirMode ?? 0o700,
|
|
54
|
+
mode: params.mode ?? 0o600,
|
|
55
|
+
tempPrefix: params.tempPrefix ?? `.${path.basename(destination)}`,
|
|
56
|
+
writeTemp: async (tempPath) => {
|
|
57
|
+
await fs.copyFile(params.sourcePath, tempPath);
|
|
58
|
+
},
|
|
59
|
+
resolveFinalPath: () => destination,
|
|
60
|
+
syncTempFile: true,
|
|
61
|
+
syncParentDir: true,
|
|
62
|
+
});
|
|
63
|
+
return result.filePath;
|
|
64
|
+
}
|
|
65
|
+
export function fileStore(options) {
|
|
66
|
+
const rootDir = path.resolve(options.rootDir);
|
|
67
|
+
const privateMode = options.private ?? false;
|
|
68
|
+
const dirMode = options.dirMode ?? 0o700;
|
|
69
|
+
const mode = options.mode ?? 0o600;
|
|
70
|
+
const maxBytes = options.maxBytes;
|
|
71
|
+
async function openRoot() {
|
|
72
|
+
return await root(rootDir, { hardlinks: "reject", maxBytes });
|
|
73
|
+
}
|
|
74
|
+
async function write(relativePath, data, writeOptions) {
|
|
75
|
+
const destination = resolveStorePath(rootDir, relativePath);
|
|
76
|
+
const content = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
77
|
+
assertMaxBytes(content.byteLength, writeOptions?.maxBytes ?? maxBytes);
|
|
78
|
+
if (privateMode) {
|
|
79
|
+
await writeSecretFileAtomic({
|
|
80
|
+
rootDir,
|
|
81
|
+
filePath: destination,
|
|
82
|
+
content,
|
|
83
|
+
dirMode: writeOptions?.dirMode ?? dirMode,
|
|
84
|
+
mode: writeOptions?.mode ?? mode,
|
|
85
|
+
});
|
|
86
|
+
return destination;
|
|
87
|
+
}
|
|
88
|
+
await ensureParent(destination, writeOptions?.dirMode ?? dirMode);
|
|
89
|
+
const result = await writeSiblingTempFile({
|
|
90
|
+
dir: path.dirname(destination),
|
|
91
|
+
dirMode: writeOptions?.dirMode ?? dirMode,
|
|
92
|
+
mode: writeOptions?.mode ?? mode,
|
|
93
|
+
tempPrefix: writeOptions?.tempPrefix ?? `.${path.basename(destination)}`,
|
|
94
|
+
writeTemp: async (tempPath) => {
|
|
95
|
+
await fs.writeFile(tempPath, content);
|
|
96
|
+
},
|
|
97
|
+
resolveFinalPath: () => destination,
|
|
98
|
+
syncTempFile: true,
|
|
99
|
+
syncParentDir: true,
|
|
100
|
+
});
|
|
101
|
+
return result.filePath;
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
rootDir,
|
|
105
|
+
path: (relativePath) => resolveStorePath(rootDir, relativePath),
|
|
106
|
+
root: openRoot,
|
|
107
|
+
write,
|
|
108
|
+
writeStream: async (relativePath, stream, writeOptions) => {
|
|
109
|
+
const destination = resolveStorePath(rootDir, relativePath);
|
|
110
|
+
const limit = writeOptions?.maxBytes ?? maxBytes;
|
|
111
|
+
if (privateMode) {
|
|
112
|
+
const chunks = [];
|
|
113
|
+
let total = 0;
|
|
114
|
+
for await (const chunk of stream) {
|
|
115
|
+
const buffer = typeof chunk === "string" ? Buffer.from(chunk) : Buffer.from(chunk);
|
|
116
|
+
total += buffer.byteLength;
|
|
117
|
+
assertMaxBytes(total, limit);
|
|
118
|
+
chunks.push(buffer);
|
|
119
|
+
}
|
|
120
|
+
await writeSecretFileAtomic({
|
|
121
|
+
rootDir,
|
|
122
|
+
filePath: destination,
|
|
123
|
+
content: Buffer.concat(chunks),
|
|
124
|
+
dirMode: writeOptions?.dirMode ?? dirMode,
|
|
125
|
+
mode: writeOptions?.mode ?? mode,
|
|
126
|
+
});
|
|
127
|
+
return destination;
|
|
128
|
+
}
|
|
129
|
+
await ensureParent(destination, writeOptions?.dirMode ?? dirMode);
|
|
130
|
+
let total = 0;
|
|
131
|
+
const result = await writeSiblingTempFile({
|
|
132
|
+
dir: path.dirname(destination),
|
|
133
|
+
dirMode: writeOptions?.dirMode ?? dirMode,
|
|
134
|
+
mode: writeOptions?.mode ?? mode,
|
|
135
|
+
tempPrefix: writeOptions?.tempPrefix ?? `.${path.basename(destination)}`,
|
|
136
|
+
writeTemp: async (tempPath) => {
|
|
137
|
+
const writable = await fs.open(tempPath, "w", writeOptions?.mode ?? mode);
|
|
138
|
+
try {
|
|
139
|
+
const out = writable.createWriteStream();
|
|
140
|
+
stream.on("data", (chunk) => {
|
|
141
|
+
total += Buffer.byteLength(chunk);
|
|
142
|
+
if (limit !== undefined && total > limit) {
|
|
143
|
+
stream.destroy(new FsSafeError("too-large", `file exceeds maximum size of ${limit} bytes`));
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
await pipeline(stream, out);
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
await writable.close().catch(() => undefined);
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
resolveFinalPath: () => destination,
|
|
153
|
+
syncTempFile: true,
|
|
154
|
+
syncParentDir: true,
|
|
155
|
+
});
|
|
156
|
+
return result.filePath;
|
|
157
|
+
},
|
|
158
|
+
copyIn: async (relativePath, sourcePath, writeOptions) => privateMode
|
|
159
|
+
? await (async () => {
|
|
160
|
+
const sourceStat = await fs.lstat(sourcePath);
|
|
161
|
+
if (sourceStat.isSymbolicLink() || !sourceStat.isFile()) {
|
|
162
|
+
throw new FsSafeError("not-file", "source path is not a file");
|
|
163
|
+
}
|
|
164
|
+
assertMaxBytes(sourceStat.size, writeOptions?.maxBytes ?? maxBytes);
|
|
165
|
+
return await write(relativePath, await fs.readFile(sourcePath), writeOptions);
|
|
166
|
+
})()
|
|
167
|
+
: await copyIntoRoot({
|
|
168
|
+
rootDir,
|
|
169
|
+
relativePath,
|
|
170
|
+
sourcePath,
|
|
171
|
+
dirMode: writeOptions?.dirMode ?? dirMode,
|
|
172
|
+
maxBytes: writeOptions?.maxBytes ?? maxBytes,
|
|
173
|
+
mode: writeOptions?.mode ?? mode,
|
|
174
|
+
tempPrefix: writeOptions?.tempPrefix,
|
|
175
|
+
}),
|
|
176
|
+
open: async (relativePath, readOptions) => await (await openRoot()).open(assertRelativePath(relativePath), readOptions),
|
|
177
|
+
read: async (relativePath, readOptions) => await (await openRoot()).read(assertRelativePath(relativePath), readOptions),
|
|
178
|
+
readBytes: async (relativePath, readOptions) => await (await openRoot()).readBytes(assertRelativePath(relativePath), readOptions),
|
|
179
|
+
readText: async (relativePath, readOptions) => {
|
|
180
|
+
const { encoding = "utf8", ...options } = readOptions ?? {};
|
|
181
|
+
return (await (await openRoot()).read(assertRelativePath(relativePath), options)).buffer
|
|
182
|
+
.toString(encoding);
|
|
183
|
+
},
|
|
184
|
+
readTextIfExists: async (relativePath, readOptions) => {
|
|
185
|
+
try {
|
|
186
|
+
return await (await openRoot()).readText(assertRelativePath(relativePath), readOptions);
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
if (isNotFound(error)) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
readJson: async (relativePath, readOptions) => {
|
|
196
|
+
const { encoding = "utf8", ...options } = readOptions ?? {};
|
|
197
|
+
return JSON.parse((await (await openRoot()).read(assertRelativePath(relativePath), options)).buffer
|
|
198
|
+
.toString(encoding));
|
|
199
|
+
},
|
|
200
|
+
readJsonIfExists: async (relativePath, readOptions) => {
|
|
201
|
+
try {
|
|
202
|
+
return await (await openRoot()).readJson(assertRelativePath(relativePath), readOptions);
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
if (isNotFound(error)) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
remove: async (relativePath) => {
|
|
212
|
+
await (await openRoot()).remove(assertRelativePath(relativePath));
|
|
213
|
+
},
|
|
214
|
+
exists: async (relativePath) => await (await openRoot()).exists(assertRelativePath(relativePath)),
|
|
215
|
+
writeText: async (relativePath, data, writeOptions) => await write(relativePath, data, writeOptions),
|
|
216
|
+
writeJson: async (relativePath, data, writeOptions) => {
|
|
217
|
+
const json = JSON.stringify(data, null, 2);
|
|
218
|
+
return await write(relativePath, writeOptions?.trailingNewline === false ? json : `${json}\n`, writeOptions);
|
|
219
|
+
},
|
|
220
|
+
json: (relativePath, jsonOptions) => {
|
|
221
|
+
const filePath = resolveStorePath(rootDir, relativePath);
|
|
222
|
+
return createJsonStore({
|
|
223
|
+
filePath,
|
|
224
|
+
readIfExists: async () => {
|
|
225
|
+
try {
|
|
226
|
+
return await (await openRoot()).readJson(assertRelativePath(relativePath));
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
if (isNotFound(error)) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
readRequired: async () => await (await openRoot()).readJson(assertRelativePath(relativePath)),
|
|
236
|
+
write: async (value, options) => {
|
|
237
|
+
const json = JSON.stringify(value, null, 2);
|
|
238
|
+
await write(relativePath, options?.trailingNewline === false ? json : `${json}\n`);
|
|
239
|
+
},
|
|
240
|
+
}, jsonOptions);
|
|
241
|
+
},
|
|
242
|
+
pruneExpired: async (pruneOptions) => {
|
|
243
|
+
const now = Date.now();
|
|
244
|
+
const recursive = pruneOptions.recursive ?? false;
|
|
245
|
+
const maxDepth = pruneOptions.maxDepth;
|
|
246
|
+
const pruneEmptyDirs = (recursive || maxDepth !== undefined) && (pruneOptions.pruneEmptyDirs ?? false);
|
|
247
|
+
async function pruneDir(dir, depth) {
|
|
248
|
+
const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
249
|
+
for (const entry of entries) {
|
|
250
|
+
const fullPath = path.join(dir, entry.name);
|
|
251
|
+
const stat = await fs.lstat(fullPath).catch(() => null);
|
|
252
|
+
if (!stat || stat.isSymbolicLink()) {
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (stat.isDirectory()) {
|
|
256
|
+
const shouldDescend = maxDepth !== undefined ? depth < maxDepth : recursive;
|
|
257
|
+
if (shouldDescend && (await pruneDir(fullPath, depth + 1))) {
|
|
258
|
+
await fs.rmdir(fullPath).catch(() => undefined);
|
|
259
|
+
}
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (stat.isFile() && now - stat.mtimeMs > pruneOptions.ttlMs) {
|
|
263
|
+
await fs.rm(fullPath, { force: true }).catch(() => undefined);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (!pruneEmptyDirs) {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
const remaining = await fs.readdir(dir).catch(() => null);
|
|
270
|
+
return remaining !== null && remaining.length === 0;
|
|
271
|
+
}
|
|
272
|
+
await fs.mkdir(rootDir, { recursive: true, mode: dirMode });
|
|
273
|
+
await pruneDir(rootDir, 0);
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
function ensureParentSync(filePath, mode) {
|
|
278
|
+
const dir = path.dirname(filePath);
|
|
279
|
+
syncFs.mkdirSync(dir, { recursive: true, mode });
|
|
280
|
+
try {
|
|
281
|
+
syncFs.chmodSync(dir, mode);
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
// Best-effort on platforms that do not enforce POSIX modes.
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
function ensurePrivateDirectorySync(rootDir, targetDir, mode) {
|
|
288
|
+
const root = path.resolve(rootDir);
|
|
289
|
+
const target = path.resolve(targetDir);
|
|
290
|
+
assertStoreFilePath(root, target);
|
|
291
|
+
let current = root;
|
|
292
|
+
syncFs.mkdirSync(current, { recursive: true, mode });
|
|
293
|
+
const rootStat = syncFs.lstatSync(current);
|
|
294
|
+
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
|
295
|
+
throw new FsSafeError("not-file", `private store root must be a directory: ${current}`);
|
|
296
|
+
}
|
|
297
|
+
try {
|
|
298
|
+
syncFs.chmodSync(current, mode);
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
// Best-effort on platforms that do not enforce POSIX modes.
|
|
302
|
+
}
|
|
303
|
+
for (const segment of path.relative(root, target).split(path.sep).filter(Boolean)) {
|
|
304
|
+
current = path.join(current, segment);
|
|
305
|
+
try {
|
|
306
|
+
const stat = syncFs.lstatSync(current);
|
|
307
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
308
|
+
throw new FsSafeError("not-file", `private store directory component must be a directory: ${current}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
if (error.code !== "ENOENT") {
|
|
313
|
+
throw error;
|
|
314
|
+
}
|
|
315
|
+
syncFs.mkdirSync(current, { mode });
|
|
316
|
+
}
|
|
317
|
+
const rootReal = syncFs.realpathSync(root);
|
|
318
|
+
const currentReal = syncFs.realpathSync(current);
|
|
319
|
+
if (!isPathInside(rootReal, currentReal)) {
|
|
320
|
+
throw new FsSafeError("outside-workspace", "private store directory escapes root");
|
|
321
|
+
}
|
|
322
|
+
try {
|
|
323
|
+
syncFs.chmodSync(current, mode);
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
// Best-effort on platforms that do not enforce POSIX modes.
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
function writeFileSyncAtomic(params) {
|
|
331
|
+
const filePath = path.resolve(params.filePath);
|
|
332
|
+
assertStoreFilePath(params.rootDir, filePath);
|
|
333
|
+
if (params.privateMode) {
|
|
334
|
+
ensurePrivateDirectorySync(params.rootDir, path.dirname(filePath), params.dirMode);
|
|
335
|
+
try {
|
|
336
|
+
const stat = syncFs.lstatSync(filePath);
|
|
337
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
338
|
+
throw new FsSafeError("not-file", `private store target must be a regular file: ${filePath}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
if (error.code !== "ENOENT") {
|
|
343
|
+
throw error;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
ensureParentSync(filePath, params.dirMode);
|
|
349
|
+
}
|
|
350
|
+
const tempPath = path.join(path.dirname(filePath), `.fs-safe-${process.pid}-${randomUUID()}.tmp`);
|
|
351
|
+
let tempExists = false;
|
|
352
|
+
try {
|
|
353
|
+
syncFs.writeFileSync(tempPath, params.content, { flag: "wx", mode: params.mode });
|
|
354
|
+
tempExists = true;
|
|
355
|
+
try {
|
|
356
|
+
syncFs.chmodSync(tempPath, params.mode);
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
// Best-effort on platforms that do not enforce POSIX modes.
|
|
360
|
+
}
|
|
361
|
+
syncFs.renameSync(tempPath, filePath);
|
|
362
|
+
tempExists = false;
|
|
363
|
+
try {
|
|
364
|
+
syncFs.chmodSync(filePath, params.mode);
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
// Best-effort on platforms that do not enforce POSIX modes.
|
|
368
|
+
}
|
|
369
|
+
return filePath;
|
|
370
|
+
}
|
|
371
|
+
finally {
|
|
372
|
+
if (tempExists) {
|
|
373
|
+
try {
|
|
374
|
+
syncFs.unlinkSync(tempPath);
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
// Best-effort cleanup after write failure.
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
export function fileStoreSync(options) {
|
|
383
|
+
const rootDir = path.resolve(options.rootDir);
|
|
384
|
+
const privateMode = options.private ?? false;
|
|
385
|
+
const dirMode = options.dirMode ?? 0o700;
|
|
386
|
+
const mode = options.mode ?? 0o600;
|
|
387
|
+
const maxBytes = options.maxBytes;
|
|
388
|
+
function write(relativePath, data, writeOptions) {
|
|
389
|
+
const destination = resolveStorePath(rootDir, relativePath);
|
|
390
|
+
const content = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
391
|
+
assertMaxBytes(content.byteLength, writeOptions?.maxBytes ?? maxBytes);
|
|
392
|
+
return writeFileSyncAtomic({
|
|
393
|
+
rootDir,
|
|
394
|
+
filePath: destination,
|
|
395
|
+
content,
|
|
396
|
+
privateMode,
|
|
397
|
+
dirMode: writeOptions?.dirMode ?? dirMode,
|
|
398
|
+
mode: writeOptions?.mode ?? mode,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
return {
|
|
402
|
+
rootDir,
|
|
403
|
+
path: (relativePath) => resolveStorePath(rootDir, relativePath),
|
|
404
|
+
readTextIfExists: (relativePath, readOptions) => {
|
|
405
|
+
const targetPath = resolveStorePath(rootDir, relativePath);
|
|
406
|
+
try {
|
|
407
|
+
const stat = syncFs.lstatSync(targetPath);
|
|
408
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
409
|
+
throw new FsSafeError("not-file", "store target is not a file");
|
|
410
|
+
}
|
|
411
|
+
assertMaxBytes(stat.size, readOptions?.maxBytes ?? maxBytes);
|
|
412
|
+
if (privateMode && stat.nlink > 1) {
|
|
413
|
+
throw new FsSafeError("hardlink", "private store target must not be hardlinked");
|
|
414
|
+
}
|
|
415
|
+
return syncFs.readFileSync(targetPath, "utf8");
|
|
416
|
+
}
|
|
417
|
+
catch (error) {
|
|
418
|
+
if (isNotFound(error)) {
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
throw error;
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
readJsonIfExists: (relativePath, readOptions) => {
|
|
425
|
+
const raw = fileStoreSync({ rootDir, private: privateMode, dirMode, mode, maxBytes })
|
|
426
|
+
.readTextIfExists(relativePath, readOptions);
|
|
427
|
+
return raw === null ? null : JSON.parse(raw);
|
|
428
|
+
},
|
|
429
|
+
write,
|
|
430
|
+
writeText: (relativePath, data, writeOptions) => write(relativePath, data, writeOptions),
|
|
431
|
+
writeJson: (relativePath, data, writeOptions) => {
|
|
432
|
+
const json = JSON.stringify(data, null, 2);
|
|
433
|
+
return write(relativePath, writeOptions?.trailingNewline === false ? json : `${json}\n`, writeOptions);
|
|
434
|
+
},
|
|
435
|
+
};
|
|
436
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-url.d.ts","sourceRoot":"","sources":["../src/file-url.ts"],"names":[],"mappings":"AAAA,cAAc,wBAAwB,CAAC"}
|
package/dist/file-url.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./local-file-access.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filename.d.ts","sourceRoot":"","sources":["../src/filename.ts"],"names":[],"mappings":"AAEA,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAuBxF"}
|
package/dist/filename.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
export function sanitizeUntrustedFileName(fileName, fallbackName) {
|
|
3
|
+
const trimmed = typeof fileName === "string" ? fileName.trim() : "";
|
|
4
|
+
if (!trimmed) {
|
|
5
|
+
return fallbackName;
|
|
6
|
+
}
|
|
7
|
+
let base = path.posix.basename(trimmed);
|
|
8
|
+
base = path.win32.basename(base);
|
|
9
|
+
let cleaned = "";
|
|
10
|
+
for (let i = 0; i < base.length; i++) {
|
|
11
|
+
const code = base.charCodeAt(i);
|
|
12
|
+
if (code < 0x20 || code === 0x7f) {
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
cleaned += base[i];
|
|
16
|
+
}
|
|
17
|
+
base = cleaned.trim();
|
|
18
|
+
if (!base || base === "." || base === "..") {
|
|
19
|
+
return fallbackName;
|
|
20
|
+
}
|
|
21
|
+
if (base.length > 200) {
|
|
22
|
+
base = base.slice(0, 200);
|
|
23
|
+
}
|
|
24
|
+
return base;
|
|
25
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function isPinnedPathHelperSpawnError(error: unknown): boolean;
|
|
2
|
+
export declare function runPinnedPathHelper(params: {
|
|
3
|
+
operation: "mkdirp" | "remove";
|
|
4
|
+
rootPath: string;
|
|
5
|
+
relativePath: string;
|
|
6
|
+
}): Promise<void>;
|
|
7
|
+
//# sourceMappingURL=fs-pinned-path-helper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fs-pinned-path-helper.d.ts","sourceRoot":"","sources":["../src/fs-pinned-path-helper.ts"],"names":[],"mappings":"AAmKA,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAWpE;AAED,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,SAAS,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;CACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAwBhB"}
|