@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,13 @@
|
|
|
1
|
+
import { replaceFileAtomic } from "./replace-file.js";
|
|
2
|
+
export async function writeTextAtomic(filePath, content, options) {
|
|
3
|
+
const payload = options?.trailingNewline && !content.endsWith("\n") ? `${content}\n` : content;
|
|
4
|
+
await replaceFileAtomic({
|
|
5
|
+
filePath,
|
|
6
|
+
content: payload,
|
|
7
|
+
mode: options?.mode ?? 0o600,
|
|
8
|
+
dirMode: options?.dirMode ?? (0o777 & ~process.umask()),
|
|
9
|
+
copyFallbackOnPermissionError: true,
|
|
10
|
+
syncTempFile: true,
|
|
11
|
+
syncParentDir: true,
|
|
12
|
+
});
|
|
13
|
+
}
|
package/dist/timing.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timing.d.ts","sourceRoot":"","sources":["../src/timing.ts"],"names":[],"mappings":"AAAA,wBAAsB,WAAW,CAAC,CAAC,EACjC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EACnB,SAAS,EAAE,MAAM,EACjB,cAAc,GAAE,MAAM,GAAG;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,KAAK,CAAA;CAErF,GACA,OAAO,CAAC,CAAC,CAAC,CAuBZ"}
|
package/dist/timing.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export async function withTimeout(promise, timeoutMs, labelOrOptions = {
|
|
2
|
+
message: "timeout",
|
|
3
|
+
}) {
|
|
4
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
5
|
+
return await promise;
|
|
6
|
+
}
|
|
7
|
+
const options = typeof labelOrOptions === "string" ? { label: labelOrOptions } : labelOrOptions;
|
|
8
|
+
const createError = options.createError ??
|
|
9
|
+
(() => new Error(options.message ?? `${options.label ?? "operation"} timed out after ${timeoutMs}ms`));
|
|
10
|
+
let timeoutId;
|
|
11
|
+
try {
|
|
12
|
+
return await Promise.race([
|
|
13
|
+
promise,
|
|
14
|
+
new Promise((_, reject) => {
|
|
15
|
+
timeoutId = setTimeout(() => reject(createError()), timeoutMs);
|
|
16
|
+
}),
|
|
17
|
+
]);
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
if (timeoutId) {
|
|
21
|
+
clearTimeout(timeoutId);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
package/dist/trash.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trash.d.ts","sourceRoot":"","sources":["../src/trash.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,sBAAsB,GAAG;IACnC,YAAY,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;CACjC,CAAC;AA4HF,wBAAsB,eAAe,CACnC,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,MAAM,CAAC,CAcjB"}
|
package/dist/trash.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
const TRASH_DESTINATION_COLLISION_CODES = new Set(["EEXIST", "ENOTEMPTY", "ERR_FS_CP_EEXIST"]);
|
|
5
|
+
const TRASH_DESTINATION_RETRY_LIMIT = 4;
|
|
6
|
+
function getFsErrorCode(error) {
|
|
7
|
+
if (!error || typeof error !== "object" || !("code" in error)) {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
const code = error.code;
|
|
11
|
+
return typeof code === "string" ? code : undefined;
|
|
12
|
+
}
|
|
13
|
+
function isTrashDestinationCollision(error) {
|
|
14
|
+
const code = getFsErrorCode(error);
|
|
15
|
+
return Boolean(code && TRASH_DESTINATION_COLLISION_CODES.has(code));
|
|
16
|
+
}
|
|
17
|
+
function isSameOrChildPath(candidate, parent) {
|
|
18
|
+
return candidate === parent || candidate.startsWith(`${parent}${path.sep}`);
|
|
19
|
+
}
|
|
20
|
+
function resolveAllowedTrashRoots(allowedRoots) {
|
|
21
|
+
const roots = [...(allowedRoots ?? [os.homedir(), os.tmpdir()])].map((root) => {
|
|
22
|
+
try {
|
|
23
|
+
return path.resolve(fs.realpathSync.native(root));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return path.resolve(root);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
return [...new Set(roots)];
|
|
30
|
+
}
|
|
31
|
+
function assertAllowedTrashTarget(targetPath, allowedRoots) {
|
|
32
|
+
let resolvedTargetPath = path.resolve(targetPath);
|
|
33
|
+
try {
|
|
34
|
+
resolvedTargetPath = path.resolve(fs.realpathSync.native(targetPath));
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// The subsequent move will surface missing or inaccessible targets.
|
|
38
|
+
}
|
|
39
|
+
const isAllowed = resolveAllowedTrashRoots(allowedRoots).some((root) => resolvedTargetPath !== root && isSameOrChildPath(resolvedTargetPath, root));
|
|
40
|
+
if (!isAllowed) {
|
|
41
|
+
throw new Error(`Refusing to trash path outside allowed roots: ${targetPath}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function resolveTrashDir() {
|
|
45
|
+
const homeDir = os.homedir();
|
|
46
|
+
const trashDir = path.join(homeDir, ".Trash");
|
|
47
|
+
fs.mkdirSync(trashDir, { recursive: true, mode: 0o700 });
|
|
48
|
+
const trashDirStat = fs.lstatSync(trashDir);
|
|
49
|
+
if (!trashDirStat.isDirectory() || trashDirStat.isSymbolicLink()) {
|
|
50
|
+
throw new Error(`Refusing to use non-directory/symlink trash directory: ${trashDir}`);
|
|
51
|
+
}
|
|
52
|
+
const realHome = path.resolve(fs.realpathSync.native(homeDir));
|
|
53
|
+
const resolvedTrashDir = path.resolve(fs.realpathSync.native(trashDir));
|
|
54
|
+
if (resolvedTrashDir === realHome || !isSameOrChildPath(resolvedTrashDir, realHome)) {
|
|
55
|
+
throw new Error(`Trash directory escaped home directory: ${trashDir}`);
|
|
56
|
+
}
|
|
57
|
+
return resolvedTrashDir;
|
|
58
|
+
}
|
|
59
|
+
function trashBaseName(targetPath) {
|
|
60
|
+
const resolvedTargetPath = path.resolve(targetPath);
|
|
61
|
+
if (resolvedTargetPath === path.parse(resolvedTargetPath).root) {
|
|
62
|
+
throw new Error(`Refusing to trash root path: ${targetPath}`);
|
|
63
|
+
}
|
|
64
|
+
const base = path.basename(resolvedTargetPath).replace(/[\\/]+/g, "");
|
|
65
|
+
if (!base) {
|
|
66
|
+
throw new Error(`Unable to derive safe trash basename for: ${targetPath}`);
|
|
67
|
+
}
|
|
68
|
+
return base;
|
|
69
|
+
}
|
|
70
|
+
function resolveContainedPath(root, leaf) {
|
|
71
|
+
const resolvedRoot = path.resolve(root);
|
|
72
|
+
const resolvedPath = path.resolve(resolvedRoot, leaf);
|
|
73
|
+
if (!isSameOrChildPath(resolvedPath, resolvedRoot) || resolvedPath === resolvedRoot) {
|
|
74
|
+
throw new Error(`Trash destination escaped trash directory: ${resolvedPath}`);
|
|
75
|
+
}
|
|
76
|
+
return resolvedPath;
|
|
77
|
+
}
|
|
78
|
+
function reserveTrashDestination(trashDir, base, timestamp) {
|
|
79
|
+
const containerPrefix = resolveContainedPath(trashDir, `${base}-${timestamp}-`);
|
|
80
|
+
const container = fs.mkdtempSync(containerPrefix);
|
|
81
|
+
const resolvedContainer = path.resolve(container);
|
|
82
|
+
const resolvedTrashDir = path.resolve(trashDir);
|
|
83
|
+
if (resolvedContainer === resolvedTrashDir ||
|
|
84
|
+
!isSameOrChildPath(resolvedContainer, resolvedTrashDir)) {
|
|
85
|
+
throw new Error(`Trash destination escaped trash directory: ${container}`);
|
|
86
|
+
}
|
|
87
|
+
return resolveContainedPath(container, base);
|
|
88
|
+
}
|
|
89
|
+
function movePathToDestination(targetPath, dest) {
|
|
90
|
+
try {
|
|
91
|
+
fs.renameSync(targetPath, dest);
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (getFsErrorCode(error) !== "EXDEV") {
|
|
96
|
+
if (isTrashDestinationCollision(error)) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
fs.cpSync(targetPath, dest, { recursive: true, force: false, errorOnExist: true });
|
|
104
|
+
fs.rmSync(targetPath, { recursive: true, force: false });
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (isTrashDestinationCollision(error)) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
export async function movePathToTrash(targetPath, options = {}) {
|
|
115
|
+
// Avoid resolving external trash helpers through the service PATH during cleanup.
|
|
116
|
+
const base = trashBaseName(targetPath);
|
|
117
|
+
assertAllowedTrashTarget(targetPath, options.allowedRoots);
|
|
118
|
+
const trashDir = resolveTrashDir();
|
|
119
|
+
const timestamp = Date.now();
|
|
120
|
+
for (let attempt = 0; attempt < TRASH_DESTINATION_RETRY_LIMIT; attempt += 1) {
|
|
121
|
+
const dest = reserveTrashDestination(trashDir, base, timestamp);
|
|
122
|
+
if (movePathToDestination(targetPath, dest)) {
|
|
123
|
+
return dest;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
throw new Error(`Unable to choose a unique trash destination for ${targetPath}`);
|
|
127
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type FastPathMode = "auto" | "never" | "require";
|
|
2
|
+
export type SafeEncoding = BufferEncoding | null;
|
|
3
|
+
export type PathStat = {
|
|
4
|
+
dev: number;
|
|
5
|
+
gid: number;
|
|
6
|
+
ino: number;
|
|
7
|
+
isDirectory: boolean;
|
|
8
|
+
isFile: boolean;
|
|
9
|
+
isSymbolicLink: boolean;
|
|
10
|
+
mode: number;
|
|
11
|
+
mtimeMs: number;
|
|
12
|
+
nlink: number;
|
|
13
|
+
size: number;
|
|
14
|
+
uid: number;
|
|
15
|
+
};
|
|
16
|
+
export type DirEntry = PathStat & {
|
|
17
|
+
name: string;
|
|
18
|
+
};
|
|
19
|
+
export type BasePathOptions = {
|
|
20
|
+
rootDir: string;
|
|
21
|
+
relativePath: string;
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC;AAExD,MAAM,MAAM,YAAY,GAAG,cAAc,GAAG,IAAI,CAAC;AAEjD,MAAM,MAAM,QAAQ,GAAG;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,OAAO,CAAC;IACrB,MAAM,EAAE,OAAO,CAAC;IAChB,cAAc,EAAE,OAAO,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG;IAChC,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/walk.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import fsSync from "node:fs";
|
|
2
|
+
export type WalkEntryKind = "file" | "directory" | "symlink" | "other";
|
|
3
|
+
export type WalkSymlinkPolicy = "skip" | "follow" | "include";
|
|
4
|
+
export type WalkDirectoryEntry = {
|
|
5
|
+
name: string;
|
|
6
|
+
path: string;
|
|
7
|
+
relativePath: string;
|
|
8
|
+
depth: number;
|
|
9
|
+
kind: WalkEntryKind;
|
|
10
|
+
dirent: fsSync.Dirent;
|
|
11
|
+
};
|
|
12
|
+
export type WalkDirectoryOptions = {
|
|
13
|
+
maxDepth?: number;
|
|
14
|
+
maxEntries?: number;
|
|
15
|
+
symlinks?: WalkSymlinkPolicy;
|
|
16
|
+
include?: (entry: WalkDirectoryEntry) => boolean;
|
|
17
|
+
descend?: (entry: WalkDirectoryEntry) => boolean;
|
|
18
|
+
};
|
|
19
|
+
export type WalkDirectoryResult = {
|
|
20
|
+
entries: WalkDirectoryEntry[];
|
|
21
|
+
scannedEntryCount: number;
|
|
22
|
+
truncated: boolean;
|
|
23
|
+
};
|
|
24
|
+
export declare function walkDirectorySync(rootDir: string, options?: WalkDirectoryOptions): WalkDirectoryResult;
|
|
25
|
+
export declare function walkDirectory(rootDir: string, options?: WalkDirectoryOptions): Promise<WalkDirectoryResult>;
|
|
26
|
+
//# sourceMappingURL=walk.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"walk.d.ts","sourceRoot":"","sources":["../src/walk.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,SAAS,CAAC;AAI7B,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,WAAW,GAAG,SAAS,GAAG,OAAO,CAAC;AACvE,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE9D,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,aAAa,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC;IACjD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC;CAClD,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,SAAS,EAAE,OAAO,CAAC;CACpB,CAAC;AA8DF,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,oBAAyB,GACjC,mBAAmB,CAiDrB;AAED,wBAAsB,aAAa,CACjC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,mBAAmB,CAAC,CAiD9B"}
|
package/dist/walk.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import fsSync from "node:fs";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
function kindForDirent(dirent) {
|
|
5
|
+
if (dirent.isDirectory())
|
|
6
|
+
return "directory";
|
|
7
|
+
if (dirent.isFile())
|
|
8
|
+
return "file";
|
|
9
|
+
if (dirent.isSymbolicLink())
|
|
10
|
+
return "symlink";
|
|
11
|
+
return "other";
|
|
12
|
+
}
|
|
13
|
+
function shouldStop(result, options) {
|
|
14
|
+
return options.maxEntries !== undefined && result.scannedEntryCount >= Math.max(0, options.maxEntries);
|
|
15
|
+
}
|
|
16
|
+
function buildEntry(params) {
|
|
17
|
+
const fullPath = path.join(params.dir, params.dirent.name);
|
|
18
|
+
const relativePath = path.relative(params.rootDir, fullPath) || params.dirent.name;
|
|
19
|
+
return {
|
|
20
|
+
name: params.dirent.name,
|
|
21
|
+
path: fullPath,
|
|
22
|
+
relativePath,
|
|
23
|
+
depth: params.depth,
|
|
24
|
+
kind: params.kind ?? kindForDirent(params.dirent),
|
|
25
|
+
dirent: params.dirent,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function resolveSyncKind(fullPath, dirent, symlinks) {
|
|
29
|
+
const kind = kindForDirent(dirent);
|
|
30
|
+
if (kind !== "symlink")
|
|
31
|
+
return kind;
|
|
32
|
+
if (symlinks === "skip")
|
|
33
|
+
return null;
|
|
34
|
+
if (symlinks === "include")
|
|
35
|
+
return "symlink";
|
|
36
|
+
try {
|
|
37
|
+
const stat = fsSync.statSync(fullPath);
|
|
38
|
+
if (stat.isDirectory())
|
|
39
|
+
return "directory";
|
|
40
|
+
if (stat.isFile())
|
|
41
|
+
return "file";
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
return "other";
|
|
47
|
+
}
|
|
48
|
+
async function resolveAsyncKind(fullPath, dirent, symlinks) {
|
|
49
|
+
const kind = kindForDirent(dirent);
|
|
50
|
+
if (kind !== "symlink")
|
|
51
|
+
return kind;
|
|
52
|
+
if (symlinks === "skip")
|
|
53
|
+
return null;
|
|
54
|
+
if (symlinks === "include")
|
|
55
|
+
return "symlink";
|
|
56
|
+
try {
|
|
57
|
+
const stat = await fs.stat(fullPath);
|
|
58
|
+
if (stat.isDirectory())
|
|
59
|
+
return "directory";
|
|
60
|
+
if (stat.isFile())
|
|
61
|
+
return "file";
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
return "other";
|
|
67
|
+
}
|
|
68
|
+
export function walkDirectorySync(rootDir, options = {}) {
|
|
69
|
+
const root = path.resolve(rootDir);
|
|
70
|
+
const symlinks = options.symlinks ?? "skip";
|
|
71
|
+
const result = { entries: [], scannedEntryCount: 0, truncated: false };
|
|
72
|
+
const visitedDirs = new Set();
|
|
73
|
+
function visit(dir, depth) {
|
|
74
|
+
if (options.maxDepth !== undefined && depth > options.maxDepth)
|
|
75
|
+
return;
|
|
76
|
+
let realDir;
|
|
77
|
+
try {
|
|
78
|
+
realDir = fsSync.realpathSync(dir);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (visitedDirs.has(realDir))
|
|
84
|
+
return;
|
|
85
|
+
visitedDirs.add(realDir);
|
|
86
|
+
let entries;
|
|
87
|
+
try {
|
|
88
|
+
entries = fsSync.readdirSync(dir, { withFileTypes: true });
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
for (const dirent of entries) {
|
|
94
|
+
if (shouldStop(result, options)) {
|
|
95
|
+
result.truncated = true;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
result.scannedEntryCount += 1;
|
|
99
|
+
const fullPath = path.join(dir, dirent.name);
|
|
100
|
+
const kind = resolveSyncKind(fullPath, dirent, symlinks);
|
|
101
|
+
if (!kind)
|
|
102
|
+
continue;
|
|
103
|
+
const entry = buildEntry({ rootDir: root, dir, dirent, depth, kind });
|
|
104
|
+
if (options.include?.(entry) ?? true) {
|
|
105
|
+
result.entries.push(entry);
|
|
106
|
+
}
|
|
107
|
+
if (kind === "directory" &&
|
|
108
|
+
(options.maxDepth === undefined || depth < options.maxDepth) &&
|
|
109
|
+
(options.descend?.(entry) ?? true)) {
|
|
110
|
+
visit(fullPath, depth + 1);
|
|
111
|
+
if (result.truncated)
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
visit(root, 1);
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
export async function walkDirectory(rootDir, options = {}) {
|
|
120
|
+
const root = path.resolve(rootDir);
|
|
121
|
+
const symlinks = options.symlinks ?? "skip";
|
|
122
|
+
const result = { entries: [], scannedEntryCount: 0, truncated: false };
|
|
123
|
+
const visitedDirs = new Set();
|
|
124
|
+
async function visit(dir, depth) {
|
|
125
|
+
if (options.maxDepth !== undefined && depth > options.maxDepth)
|
|
126
|
+
return;
|
|
127
|
+
let realDir;
|
|
128
|
+
try {
|
|
129
|
+
realDir = await fs.realpath(dir);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (visitedDirs.has(realDir))
|
|
135
|
+
return;
|
|
136
|
+
visitedDirs.add(realDir);
|
|
137
|
+
let entries;
|
|
138
|
+
try {
|
|
139
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
for (const dirent of entries) {
|
|
145
|
+
if (shouldStop(result, options)) {
|
|
146
|
+
result.truncated = true;
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
result.scannedEntryCount += 1;
|
|
150
|
+
const fullPath = path.join(dir, dirent.name);
|
|
151
|
+
const kind = await resolveAsyncKind(fullPath, dirent, symlinks);
|
|
152
|
+
if (!kind)
|
|
153
|
+
continue;
|
|
154
|
+
const entry = buildEntry({ rootDir: root, dir, dirent, depth, kind });
|
|
155
|
+
if (options.include?.(entry) ?? true) {
|
|
156
|
+
result.entries.push(entry);
|
|
157
|
+
}
|
|
158
|
+
if (kind === "directory" &&
|
|
159
|
+
(options.maxDepth === undefined || depth < options.maxDepth) &&
|
|
160
|
+
(options.descend?.(entry) ?? true)) {
|
|
161
|
+
await visit(fullPath, depth + 1);
|
|
162
|
+
if (result.truncated)
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
await visit(root, 1);
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"write-queue.d.ts","sourceRoot":"","sources":["../src/write-queue.ts"],"names":[],"mappings":"AAEA,wBAAsB,kBAAkB,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAkB1F"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const writeQueues = new Map();
|
|
2
|
+
export async function serializePathWrite(key, run) {
|
|
3
|
+
const previous = writeQueues.get(key) ?? Promise.resolve();
|
|
4
|
+
const task = (async () => {
|
|
5
|
+
await previous.catch(() => undefined);
|
|
6
|
+
return await run();
|
|
7
|
+
})();
|
|
8
|
+
const done = task.then(() => undefined, () => undefined);
|
|
9
|
+
writeQueues.set(key, done);
|
|
10
|
+
try {
|
|
11
|
+
return await task;
|
|
12
|
+
}
|
|
13
|
+
finally {
|
|
14
|
+
if (writeQueues.get(key) === done) {
|
|
15
|
+
writeQueues.delete(key);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@openclaw/fs-safe",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Capability-style filesystem roots for Node.js apps that handle untrusted relative paths.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/openclaw/fs-safe.git"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/**/*.js",
|
|
12
|
+
"dist/**/*.d.ts",
|
|
13
|
+
"dist/**/*.d.ts.map",
|
|
14
|
+
"README.md",
|
|
15
|
+
"CHANGELOG.md",
|
|
16
|
+
"SECURITY.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"type": "module",
|
|
20
|
+
"sideEffects": false,
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"default": "./dist/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./root": {
|
|
27
|
+
"types": "./dist/root.d.ts",
|
|
28
|
+
"default": "./dist/root.js"
|
|
29
|
+
},
|
|
30
|
+
"./config": {
|
|
31
|
+
"types": "./dist/config.d.ts",
|
|
32
|
+
"default": "./dist/config.js"
|
|
33
|
+
},
|
|
34
|
+
"./path": {
|
|
35
|
+
"types": "./dist/path.d.ts",
|
|
36
|
+
"default": "./dist/path.js"
|
|
37
|
+
},
|
|
38
|
+
"./advanced": {
|
|
39
|
+
"types": "./dist/advanced.d.ts",
|
|
40
|
+
"default": "./dist/advanced.js"
|
|
41
|
+
},
|
|
42
|
+
"./json": {
|
|
43
|
+
"types": "./dist/json.d.ts",
|
|
44
|
+
"default": "./dist/json.js"
|
|
45
|
+
},
|
|
46
|
+
"./store": {
|
|
47
|
+
"types": "./dist/store.d.ts",
|
|
48
|
+
"default": "./dist/store.js"
|
|
49
|
+
},
|
|
50
|
+
"./secret": {
|
|
51
|
+
"types": "./dist/secret.d.ts",
|
|
52
|
+
"default": "./dist/secret.js"
|
|
53
|
+
},
|
|
54
|
+
"./permissions": {
|
|
55
|
+
"types": "./dist/permissions-public.d.ts",
|
|
56
|
+
"default": "./dist/permissions-public.js"
|
|
57
|
+
},
|
|
58
|
+
"./secure-file": {
|
|
59
|
+
"types": "./dist/secure-file.d.ts",
|
|
60
|
+
"default": "./dist/secure-file.js"
|
|
61
|
+
},
|
|
62
|
+
"./file-lock": {
|
|
63
|
+
"types": "./dist/file-lock.d.ts",
|
|
64
|
+
"default": "./dist/file-lock.js"
|
|
65
|
+
},
|
|
66
|
+
"./walk": {
|
|
67
|
+
"types": "./dist/walk.d.ts",
|
|
68
|
+
"default": "./dist/walk.js"
|
|
69
|
+
},
|
|
70
|
+
"./temp": {
|
|
71
|
+
"types": "./dist/temp.d.ts",
|
|
72
|
+
"default": "./dist/temp.js"
|
|
73
|
+
},
|
|
74
|
+
"./atomic": {
|
|
75
|
+
"types": "./dist/atomic.d.ts",
|
|
76
|
+
"default": "./dist/atomic.js"
|
|
77
|
+
},
|
|
78
|
+
"./archive": {
|
|
79
|
+
"types": "./dist/archive.d.ts",
|
|
80
|
+
"default": "./dist/archive.js"
|
|
81
|
+
},
|
|
82
|
+
"./errors": {
|
|
83
|
+
"types": "./dist/errors.d.ts",
|
|
84
|
+
"default": "./dist/errors.js"
|
|
85
|
+
},
|
|
86
|
+
"./types": {
|
|
87
|
+
"types": "./dist/types.d.ts",
|
|
88
|
+
"default": "./dist/types.js"
|
|
89
|
+
},
|
|
90
|
+
"./test-hooks": {
|
|
91
|
+
"types": "./dist/test-hooks.d.ts",
|
|
92
|
+
"default": "./dist/test-hooks.js"
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"scripts": {
|
|
96
|
+
"benchmark": "node scripts/benchmark.mjs",
|
|
97
|
+
"build": "tsc -p tsconfig.json",
|
|
98
|
+
"prepack": "node scripts/prepack-build.mjs",
|
|
99
|
+
"test": "vitest run",
|
|
100
|
+
"test:coverage": "vitest run --coverage",
|
|
101
|
+
"check": "pnpm build && pnpm test",
|
|
102
|
+
"docs:site": "node scripts/build-docs-site.mjs"
|
|
103
|
+
},
|
|
104
|
+
"optionalDependencies": {
|
|
105
|
+
"jszip": "^3.10.1",
|
|
106
|
+
"tar": "7.5.13"
|
|
107
|
+
},
|
|
108
|
+
"devDependencies": {
|
|
109
|
+
"@types/node": "^22.15.19",
|
|
110
|
+
"@vitest/coverage-v8": "3.2.4",
|
|
111
|
+
"typescript": "^5.8.3",
|
|
112
|
+
"vitest": "^3.1.4"
|
|
113
|
+
},
|
|
114
|
+
"engines": {
|
|
115
|
+
"node": ">=20.11"
|
|
116
|
+
}
|
|
117
|
+
}
|