@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,235 @@
|
|
|
1
|
+
import fsSync from "node:fs";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
const GLOBAL_STATE_KEY = Symbol.for("fsSafe.sidecarLockManagers");
|
|
5
|
+
function getGlobalManagers() {
|
|
6
|
+
const globalWithState = globalThis;
|
|
7
|
+
if (!globalWithState[GLOBAL_STATE_KEY]) {
|
|
8
|
+
globalWithState[GLOBAL_STATE_KEY] = new Map();
|
|
9
|
+
}
|
|
10
|
+
return globalWithState[GLOBAL_STATE_KEY];
|
|
11
|
+
}
|
|
12
|
+
function resolveManagerState(key) {
|
|
13
|
+
const managers = getGlobalManagers();
|
|
14
|
+
let state = managers.get(key);
|
|
15
|
+
if (!state) {
|
|
16
|
+
state = { cleanupRegistered: false, held: new Map() };
|
|
17
|
+
managers.set(key, state);
|
|
18
|
+
}
|
|
19
|
+
return state;
|
|
20
|
+
}
|
|
21
|
+
async function readJsonPayload(lockPath) {
|
|
22
|
+
try {
|
|
23
|
+
const parsed = JSON.parse(await fs.readFile(lockPath, "utf8"));
|
|
24
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
25
|
+
? parsed
|
|
26
|
+
: null;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function resolveNormalizedTargetPath(targetPath) {
|
|
33
|
+
const resolved = path.resolve(targetPath);
|
|
34
|
+
const dir = path.dirname(resolved);
|
|
35
|
+
await fs.mkdir(dir, { recursive: true });
|
|
36
|
+
try {
|
|
37
|
+
return path.join(await fs.realpath(dir), path.basename(resolved));
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return resolved;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function computeDelayMs(retry, attempt) {
|
|
44
|
+
const minTimeout = retry.minTimeout ?? 50;
|
|
45
|
+
const maxTimeout = retry.maxTimeout ?? 1000;
|
|
46
|
+
const factor = retry.factor ?? 1;
|
|
47
|
+
const base = Math.min(maxTimeout, Math.max(minTimeout, minTimeout * factor ** attempt));
|
|
48
|
+
const jitter = retry.randomize ? 1 + Math.random() : 1;
|
|
49
|
+
return Math.min(maxTimeout, Math.round(base * jitter));
|
|
50
|
+
}
|
|
51
|
+
async function defaultShouldReclaim(params) {
|
|
52
|
+
const createdAt = typeof params.payload?.createdAt === "string" ? params.payload.createdAt : "";
|
|
53
|
+
const createdAtMs = Date.parse(createdAt);
|
|
54
|
+
if (Number.isFinite(createdAtMs) && params.nowMs - createdAtMs > params.staleMs) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const stat = await fs.stat(params.lockPath);
|
|
59
|
+
return params.nowMs - stat.mtimeMs > params.staleMs;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function releaseAllLocksSync(state) {
|
|
66
|
+
for (const [normalizedTargetPath, held] of state.held) {
|
|
67
|
+
void held.handle.close().catch(() => undefined);
|
|
68
|
+
try {
|
|
69
|
+
fsSync.rmSync(held.lockPath, { force: true });
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Best-effort process-exit cleanup.
|
|
73
|
+
}
|
|
74
|
+
state.held.delete(normalizedTargetPath);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function releaseHeldLock(state, normalizedTargetPath, held, opts = {}) {
|
|
78
|
+
const current = state.held.get(normalizedTargetPath);
|
|
79
|
+
if (current !== held) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
if (opts.force) {
|
|
83
|
+
held.count = 0;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
held.count -= 1;
|
|
87
|
+
if (held.count > 0) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (held.releasePromise) {
|
|
92
|
+
await held.releasePromise.catch(() => undefined);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
state.held.delete(normalizedTargetPath);
|
|
96
|
+
held.releasePromise = (async () => {
|
|
97
|
+
await held.handle.close().catch(() => undefined);
|
|
98
|
+
await fs.rm(held.lockPath, { force: true }).catch(() => undefined);
|
|
99
|
+
})();
|
|
100
|
+
try {
|
|
101
|
+
await held.releasePromise;
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
held.releasePromise = undefined;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export function createSidecarLockManager(key) {
|
|
109
|
+
const state = resolveManagerState(key);
|
|
110
|
+
function ensureExitCleanupRegistered() {
|
|
111
|
+
if (state.cleanupRegistered) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
state.cleanupRegistered = true;
|
|
115
|
+
process.on("exit", () => releaseAllLocksSync(state));
|
|
116
|
+
}
|
|
117
|
+
async function acquire(options) {
|
|
118
|
+
ensureExitCleanupRegistered();
|
|
119
|
+
const normalizedTargetPath = await resolveNormalizedTargetPath(options.targetPath);
|
|
120
|
+
const lockPath = options.lockPath ?? `${normalizedTargetPath}.lock`;
|
|
121
|
+
const held = state.held.get(normalizedTargetPath);
|
|
122
|
+
if (held && options.allowReentrant) {
|
|
123
|
+
held.count += 1;
|
|
124
|
+
const release = () => releaseHeldLock(state, normalizedTargetPath, held).then(() => undefined);
|
|
125
|
+
return {
|
|
126
|
+
lockPath,
|
|
127
|
+
normalizedTargetPath,
|
|
128
|
+
release,
|
|
129
|
+
[Symbol.asyncDispose]: release,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const startedAt = Date.now();
|
|
133
|
+
const retry = options.retry ?? {};
|
|
134
|
+
const maxRetries = options.timeoutMs === Number.POSITIVE_INFINITY ? undefined : retry.retries;
|
|
135
|
+
let attempt = 0;
|
|
136
|
+
while (true) {
|
|
137
|
+
let handle = null;
|
|
138
|
+
try {
|
|
139
|
+
handle = await fs.open(lockPath, "wx");
|
|
140
|
+
const createdHeld = {
|
|
141
|
+
count: 1,
|
|
142
|
+
handle,
|
|
143
|
+
lockPath,
|
|
144
|
+
acquiredAt: Date.now(),
|
|
145
|
+
metadata: options.metadata ?? {},
|
|
146
|
+
};
|
|
147
|
+
state.held.set(normalizedTargetPath, createdHeld);
|
|
148
|
+
await handle.writeFile(`${JSON.stringify(await options.payload(), null, 2)}\n`, "utf8");
|
|
149
|
+
const release = () => releaseHeldLock(state, normalizedTargetPath, createdHeld).then(() => undefined);
|
|
150
|
+
return {
|
|
151
|
+
lockPath,
|
|
152
|
+
normalizedTargetPath,
|
|
153
|
+
release,
|
|
154
|
+
[Symbol.asyncDispose]: release,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
if (handle) {
|
|
159
|
+
const current = state.held.get(normalizedTargetPath);
|
|
160
|
+
if (current?.handle === handle) {
|
|
161
|
+
state.held.delete(normalizedTargetPath);
|
|
162
|
+
}
|
|
163
|
+
await handle.close().catch(() => undefined);
|
|
164
|
+
await fs.rm(lockPath, { force: true }).catch(() => undefined);
|
|
165
|
+
}
|
|
166
|
+
if (err.code !== "EEXIST") {
|
|
167
|
+
throw err;
|
|
168
|
+
}
|
|
169
|
+
const nowMs = Date.now();
|
|
170
|
+
const payload = await readJsonPayload(lockPath);
|
|
171
|
+
const shouldReclaim = options.shouldReclaim ?? defaultShouldReclaim;
|
|
172
|
+
if (await shouldReclaim({
|
|
173
|
+
lockPath,
|
|
174
|
+
normalizedTargetPath,
|
|
175
|
+
payload,
|
|
176
|
+
staleMs: options.staleMs,
|
|
177
|
+
nowMs,
|
|
178
|
+
heldByThisProcess: state.held.has(normalizedTargetPath),
|
|
179
|
+
})) {
|
|
180
|
+
await fs.rm(lockPath, { force: true }).catch(() => undefined);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const elapsed = Date.now() - startedAt;
|
|
184
|
+
if ((options.timeoutMs !== undefined &&
|
|
185
|
+
options.timeoutMs !== Number.POSITIVE_INFINITY &&
|
|
186
|
+
elapsed >= options.timeoutMs) ||
|
|
187
|
+
(maxRetries !== undefined && attempt >= maxRetries)) {
|
|
188
|
+
throw Object.assign(new Error(`file lock timeout for ${normalizedTargetPath}`), {
|
|
189
|
+
code: "file_lock_timeout",
|
|
190
|
+
lockPath,
|
|
191
|
+
normalizedTargetPath,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
const remaining = options.timeoutMs === undefined || options.timeoutMs === Number.POSITIVE_INFINITY
|
|
195
|
+
? Number.POSITIVE_INFINITY
|
|
196
|
+
: Math.max(0, options.timeoutMs - elapsed);
|
|
197
|
+
const delay = Math.min(computeDelayMs(retry, attempt), remaining);
|
|
198
|
+
attempt += 1;
|
|
199
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async function withLock(options, fn) {
|
|
204
|
+
const lock = await acquire(options);
|
|
205
|
+
try {
|
|
206
|
+
return await fn();
|
|
207
|
+
}
|
|
208
|
+
finally {
|
|
209
|
+
await lock.release();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async function drain() {
|
|
213
|
+
for (const [normalizedTargetPath, held] of Array.from(state.held.entries())) {
|
|
214
|
+
await releaseHeldLock(state, normalizedTargetPath, held, { force: true }).catch(() => undefined);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function reset() {
|
|
218
|
+
releaseAllLocksSync(state);
|
|
219
|
+
}
|
|
220
|
+
function heldEntries() {
|
|
221
|
+
return Array.from(state.held.entries()).map(([normalizedTargetPath, held]) => ({
|
|
222
|
+
normalizedTargetPath,
|
|
223
|
+
lockPath: held.lockPath,
|
|
224
|
+
acquiredAt: held.acquiredAt,
|
|
225
|
+
metadata: held.metadata,
|
|
226
|
+
forceRelease: () => releaseHeldLock(state, normalizedTargetPath, held, { force: true }),
|
|
227
|
+
}));
|
|
228
|
+
}
|
|
229
|
+
return { acquire, withLock, drain, reset, heldEntries };
|
|
230
|
+
}
|
|
231
|
+
export async function withSidecarLock(targetPath, options, fn) {
|
|
232
|
+
const manager = createSidecarLockManager(options.managerKey ?? `fs-safe.sidecar-lock:${targetPath}`);
|
|
233
|
+
const { managerKey: _managerKey, ...acquireOptions } = options;
|
|
234
|
+
return await manager.withLock({ ...acquireOptions, targetPath }, fn);
|
|
235
|
+
}
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { fileStore, fileStoreSync, type FileStore, type FileStoreOptions, type FileStorePruneOptions, type FileStoreReadOptions, type FileStoreSync, type FileStoreWriteOptions, } from "./file-store.js";
|
|
2
|
+
export { jsonStore, type JsonStore, type JsonFileStoreOptions, type JsonStoreLockOptions, type JsonStoreOptions, } from "./json-store.js";
|
|
3
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,aAAa,EACb,KAAK,SAAS,EACd,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EACzB,KAAK,aAAa,EAClB,KAAK,qBAAqB,GAC3B,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,SAAS,EACT,KAAK,SAAS,EACd,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,GACtB,MAAM,iBAAiB,CAAC"}
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare function readStringValue(value: unknown): string | undefined;
|
|
2
|
+
export declare function normalizeNullableString(value: unknown): string | null;
|
|
3
|
+
export declare function normalizeOptionalString(value: unknown): string | undefined;
|
|
4
|
+
export declare function normalizeStringifiedOptionalString(value: unknown): string | undefined;
|
|
5
|
+
export declare function normalizeOptionalLowercaseString(value: unknown): string | undefined;
|
|
6
|
+
export declare function normalizeLowercaseStringOrEmpty(value: unknown): string;
|
|
7
|
+
export declare function normalizeFastMode(raw?: string | boolean | null): boolean | undefined;
|
|
8
|
+
export declare function lowercasePreservingWhitespace(value: string): string;
|
|
9
|
+
export declare function localeLowercasePreservingWhitespace(value: string): string;
|
|
10
|
+
export declare function resolvePrimaryStringValue(value: unknown): string | undefined;
|
|
11
|
+
export declare function normalizeOptionalThreadValue(value: unknown): string | number | undefined;
|
|
12
|
+
export declare function normalizeOptionalStringifiedId(value: unknown): string | undefined;
|
|
13
|
+
export declare function hasNonEmptyString(value: unknown): value is string;
|
|
14
|
+
//# sourceMappingURL=string-coerce.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"string-coerce.d.ts","sourceRoot":"","sources":["../src/string-coerce.ts"],"names":[],"mappings":"AAAA,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAElE;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAMrE;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAE1E;AAED,wBAAgB,kCAAkC,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAQrF;AAED,wBAAgB,gCAAgC,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAEnF;AAED,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEtE;AAED,wBAAgB,iBAAiB,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,OAAO,GAAG,SAAS,CAepF;AAED,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEnE;AAED,wBAAgB,mCAAmC,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEzE;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAQ5E;AAED,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAKxF;AAED,wBAAgB,8BAA8B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAGjF;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAEjE"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export function readStringValue(value) {
|
|
2
|
+
return typeof value === "string" ? value : undefined;
|
|
3
|
+
}
|
|
4
|
+
export function normalizeNullableString(value) {
|
|
5
|
+
if (typeof value !== "string") {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
const trimmed = value.trim();
|
|
9
|
+
return trimmed ? trimmed : null;
|
|
10
|
+
}
|
|
11
|
+
export function normalizeOptionalString(value) {
|
|
12
|
+
return normalizeNullableString(value) ?? undefined;
|
|
13
|
+
}
|
|
14
|
+
export function normalizeStringifiedOptionalString(value) {
|
|
15
|
+
if (typeof value === "string") {
|
|
16
|
+
return normalizeOptionalString(value);
|
|
17
|
+
}
|
|
18
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
19
|
+
return normalizeOptionalString(String(value));
|
|
20
|
+
}
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
export function normalizeOptionalLowercaseString(value) {
|
|
24
|
+
return normalizeOptionalString(value)?.toLowerCase();
|
|
25
|
+
}
|
|
26
|
+
export function normalizeLowercaseStringOrEmpty(value) {
|
|
27
|
+
return normalizeOptionalLowercaseString(value) ?? "";
|
|
28
|
+
}
|
|
29
|
+
export function normalizeFastMode(raw) {
|
|
30
|
+
if (typeof raw === "boolean") {
|
|
31
|
+
return raw;
|
|
32
|
+
}
|
|
33
|
+
if (!raw) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
const key = normalizeLowercaseStringOrEmpty(raw);
|
|
37
|
+
if (["off", "false", "no", "0", "disable", "disabled", "normal"].includes(key)) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
if (["on", "true", "yes", "1", "enable", "enabled", "fast"].includes(key)) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
export function lowercasePreservingWhitespace(value) {
|
|
46
|
+
return value.toLowerCase();
|
|
47
|
+
}
|
|
48
|
+
export function localeLowercasePreservingWhitespace(value) {
|
|
49
|
+
return value.toLocaleLowerCase();
|
|
50
|
+
}
|
|
51
|
+
export function resolvePrimaryStringValue(value) {
|
|
52
|
+
if (typeof value === "string") {
|
|
53
|
+
return normalizeOptionalString(value);
|
|
54
|
+
}
|
|
55
|
+
if (!value || typeof value !== "object") {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
return normalizeOptionalString(value.primary);
|
|
59
|
+
}
|
|
60
|
+
export function normalizeOptionalThreadValue(value) {
|
|
61
|
+
if (typeof value === "number") {
|
|
62
|
+
return Number.isFinite(value) ? Math.trunc(value) : undefined;
|
|
63
|
+
}
|
|
64
|
+
return normalizeOptionalString(value);
|
|
65
|
+
}
|
|
66
|
+
export function normalizeOptionalStringifiedId(value) {
|
|
67
|
+
const normalized = normalizeOptionalThreadValue(value);
|
|
68
|
+
return normalized == null ? undefined : String(normalized);
|
|
69
|
+
}
|
|
70
|
+
export function hasNonEmptyString(value) {
|
|
71
|
+
return normalizeOptionalString(value) !== undefined;
|
|
72
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type AssertNoSymlinkParentsOptions = {
|
|
2
|
+
rootDir: string;
|
|
3
|
+
targetPath: string;
|
|
4
|
+
allowMissing?: boolean;
|
|
5
|
+
allowOutsideRoot?: boolean;
|
|
6
|
+
allowRootChildSymlink?: boolean;
|
|
7
|
+
requireDirectories?: boolean;
|
|
8
|
+
messagePrefix?: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function assertNoSymlinkParents(params: AssertNoSymlinkParentsOptions): Promise<void>;
|
|
11
|
+
export declare function assertNoSymlinkParentsSync(params: AssertNoSymlinkParentsOptions): void;
|
|
12
|
+
//# sourceMappingURL=symlink-parents.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"symlink-parents.d.ts","sourceRoot":"","sources":["../src/symlink-parents.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,6BAA6B,GAAG;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAyBF,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,6BAA6B,GACpC,OAAO,CAAC,IAAI,CAAC,CA0Bf;AAED,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,6BAA6B,GACpC,IAAI,CA0BN"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import fsSync from "node:fs";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { isNotFoundPathError } from "./path.js";
|
|
5
|
+
function resolvePathWalk(params) {
|
|
6
|
+
const root = path.resolve(params.rootDir);
|
|
7
|
+
const target = path.resolve(params.targetPath);
|
|
8
|
+
const relative = path.relative(root, target);
|
|
9
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
10
|
+
if (params.allowOutsideRoot) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
throw new Error(`${params.messagePrefix ?? "Path"} must stay under ${root}.`);
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
root,
|
|
17
|
+
segments: relative && relative !== "." ? relative.split(path.sep).filter(Boolean) : [],
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function formatUnsafePath(params, current) {
|
|
21
|
+
return `${params.messagePrefix ?? "Path"} must not traverse symlinked directory: ${current}`;
|
|
22
|
+
}
|
|
23
|
+
export async function assertNoSymlinkParents(params) {
|
|
24
|
+
const walk = resolvePathWalk(params);
|
|
25
|
+
if (!walk) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
let current = walk.root;
|
|
29
|
+
for (const segment of walk.segments) {
|
|
30
|
+
current = path.join(current, segment);
|
|
31
|
+
try {
|
|
32
|
+
const stat = await fs.lstat(current);
|
|
33
|
+
if (stat.isSymbolicLink()) {
|
|
34
|
+
if (params.allowRootChildSymlink && path.dirname(current) === walk.root) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
throw new Error(formatUnsafePath(params, current));
|
|
38
|
+
}
|
|
39
|
+
if (params.requireDirectories && !stat.isDirectory()) {
|
|
40
|
+
throw new Error(`${params.messagePrefix ?? "Path"} must traverse directories: ${current}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
if (isNotFoundPathError(err) && params.allowMissing !== false) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export function assertNoSymlinkParentsSync(params) {
|
|
52
|
+
const walk = resolvePathWalk(params);
|
|
53
|
+
if (!walk) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
let current = walk.root;
|
|
57
|
+
for (const segment of walk.segments) {
|
|
58
|
+
current = path.join(current, segment);
|
|
59
|
+
try {
|
|
60
|
+
const stat = fsSync.lstatSync(current);
|
|
61
|
+
if (stat.isSymbolicLink()) {
|
|
62
|
+
if (params.allowRootChildSymlink && path.dirname(current) === walk.root) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
throw new Error(formatUnsafePath(params, current));
|
|
66
|
+
}
|
|
67
|
+
if (params.requireDirectories && !stat.isDirectory()) {
|
|
68
|
+
throw new Error(`${params.messagePrefix ?? "Path"} must traverse directories: ${current}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
if (err.code === "ENOENT" && params.allowMissing !== false) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
throw err;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function registerTempPathForExit(tempPath: string, options?: {
|
|
2
|
+
recursive?: boolean;
|
|
3
|
+
}): () => void;
|
|
4
|
+
export declare function __cleanupRegisteredTempPathsForTest(): void;
|
|
5
|
+
export declare function __cleanupRegisteredTempPathForTest(tempPath: string): void;
|
|
6
|
+
//# sourceMappingURL=temp-cleanup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"temp-cleanup.d.ts","sourceRoot":"","sources":["../src/temp-cleanup.ts"],"names":[],"mappings":"AAqBA,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,OAAO,CAAA;CAAE,GAChC,MAAM,IAAI,CAYZ;AAED,wBAAgB,mCAAmC,IAAI,IAAI,CAE1D;AAED,wBAAgB,kCAAkC,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAUzE"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import fsSync from "node:fs";
|
|
2
|
+
const tempCleanupEntries = new Map();
|
|
3
|
+
let cleanupRegistered = false;
|
|
4
|
+
function cleanupRegisteredTempPathsSync() {
|
|
5
|
+
for (const entry of tempCleanupEntries.values()) {
|
|
6
|
+
try {
|
|
7
|
+
fsSync.rmSync(entry.path, { force: true, recursive: entry.recursive });
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
// Process-exit cleanup is best-effort.
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
tempCleanupEntries.clear();
|
|
14
|
+
}
|
|
15
|
+
export function registerTempPathForExit(tempPath, options) {
|
|
16
|
+
if (!cleanupRegistered) {
|
|
17
|
+
cleanupRegistered = true;
|
|
18
|
+
process.once("exit", cleanupRegisteredTempPathsSync);
|
|
19
|
+
}
|
|
20
|
+
tempCleanupEntries.set(tempPath, {
|
|
21
|
+
path: tempPath,
|
|
22
|
+
recursive: options?.recursive === true,
|
|
23
|
+
});
|
|
24
|
+
return () => {
|
|
25
|
+
tempCleanupEntries.delete(tempPath);
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function __cleanupRegisteredTempPathsForTest() {
|
|
29
|
+
cleanupRegisteredTempPathsSync();
|
|
30
|
+
}
|
|
31
|
+
export function __cleanupRegisteredTempPathForTest(tempPath) {
|
|
32
|
+
const entry = tempCleanupEntries.get(tempPath);
|
|
33
|
+
if (!entry) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
fsSync.rmSync(entry.path, { force: true, recursive: entry.recursive });
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
tempCleanupEntries.delete(tempPath);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type TempFile = {
|
|
2
|
+
dir: string;
|
|
3
|
+
path: string;
|
|
4
|
+
file(fileName?: string): string;
|
|
5
|
+
cleanup: () => Promise<void>;
|
|
6
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
7
|
+
};
|
|
8
|
+
export declare function sanitizeTempFileName(fileName: string): string;
|
|
9
|
+
export declare function buildRandomTempFilePath(params: {
|
|
10
|
+
rootDir?: string;
|
|
11
|
+
prefix: string;
|
|
12
|
+
extension?: string;
|
|
13
|
+
now?: number;
|
|
14
|
+
uuid?: string;
|
|
15
|
+
}): string;
|
|
16
|
+
export declare function tempFile(params: {
|
|
17
|
+
rootDir?: string;
|
|
18
|
+
prefix: string;
|
|
19
|
+
fileName?: string;
|
|
20
|
+
onCleanupError?: (error: unknown) => void;
|
|
21
|
+
}): Promise<TempFile>;
|
|
22
|
+
export declare function withTempFile<T>(params: {
|
|
23
|
+
rootDir?: string;
|
|
24
|
+
prefix: string;
|
|
25
|
+
fileName?: string;
|
|
26
|
+
onCleanupError?: (error: unknown) => void;
|
|
27
|
+
}, fn: (tmpPath: string) => Promise<T>): Promise<T>;
|
|
28
|
+
//# sourceMappingURL=temp-target.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"temp-target.d.ts","sourceRoot":"","sources":["../src/temp-target.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,QAAQ,GAAG;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAChC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,CAAC;AAiBF,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAI7D;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC9C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,GAAG,MAAM,CAWT;AAyBD,wBAAsB,QAAQ,CAAC,MAAM,EAAE;IACrC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CAC3C,GAAG,OAAO,CAAC,QAAQ,CAAC,CAqBpB;AAED,wBAAsB,YAAY,CAAC,CAAC,EAClC,MAAM,EAAE;IACN,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CAC3C,EACD,EAAE,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,GAClC,OAAO,CAAC,CAAC,CAAC,CAOZ"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { resolveSecureTempRoot } from "./secure-temp-dir.js";
|
|
5
|
+
import { registerTempPathForExit } from "./temp-cleanup.js";
|
|
6
|
+
function sanitizePrefix(prefix) {
|
|
7
|
+
const normalized = prefix.replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
8
|
+
return normalized || "tmp";
|
|
9
|
+
}
|
|
10
|
+
function sanitizeExtension(extension) {
|
|
11
|
+
if (!extension) {
|
|
12
|
+
return "";
|
|
13
|
+
}
|
|
14
|
+
const normalized = extension.startsWith(".") ? extension : `.${extension}`;
|
|
15
|
+
const suffix = normalized.match(/[a-zA-Z0-9._-]+$/)?.[0] ?? "";
|
|
16
|
+
const token = suffix.replace(/^[._-]+/, "");
|
|
17
|
+
return token ? `.${token}` : "";
|
|
18
|
+
}
|
|
19
|
+
export function sanitizeTempFileName(fileName) {
|
|
20
|
+
const base = path.basename(fileName).replace(/[^a-zA-Z0-9._-]+/g, "-");
|
|
21
|
+
const normalized = base.replace(/^-+|-+$/g, "");
|
|
22
|
+
return normalized || "download.bin";
|
|
23
|
+
}
|
|
24
|
+
export function buildRandomTempFilePath(params) {
|
|
25
|
+
const rootDir = resolveTempRoot(params.rootDir);
|
|
26
|
+
const prefix = sanitizePrefix(params.prefix);
|
|
27
|
+
const extension = sanitizeExtension(params.extension);
|
|
28
|
+
const nowCandidate = params.now;
|
|
29
|
+
const now = typeof nowCandidate === "number" && Number.isFinite(nowCandidate)
|
|
30
|
+
? Math.trunc(nowCandidate)
|
|
31
|
+
: Date.now();
|
|
32
|
+
const uuid = params.uuid?.trim() || crypto.randomUUID();
|
|
33
|
+
return path.join(rootDir, `${prefix}-${now}-${uuid}${extension}`);
|
|
34
|
+
}
|
|
35
|
+
function isNodeErrorWithCode(err, code) {
|
|
36
|
+
return (typeof err === "object" &&
|
|
37
|
+
err !== null &&
|
|
38
|
+
"code" in err &&
|
|
39
|
+
err.code === code);
|
|
40
|
+
}
|
|
41
|
+
async function cleanupTempDir(dir, onCleanupError) {
|
|
42
|
+
try {
|
|
43
|
+
await rm(dir, { recursive: true, force: true });
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
if (!isNodeErrorWithCode(err, "ENOENT")) {
|
|
47
|
+
onCleanupError?.(err);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function resolveTempRoot(rootDir) {
|
|
52
|
+
return rootDir ?? resolveSecureTempRoot({ fallbackPrefix: "fs-safe" });
|
|
53
|
+
}
|
|
54
|
+
export async function tempFile(params) {
|
|
55
|
+
const rootDir = resolveTempRoot(params.rootDir);
|
|
56
|
+
const prefix = `${sanitizePrefix(params.prefix)}-`;
|
|
57
|
+
const dir = await mkdtemp(path.join(rootDir, prefix));
|
|
58
|
+
const unregisterTempDir = registerTempPathForExit(dir, { recursive: true });
|
|
59
|
+
const file = (fileName) => path.join(dir, sanitizeTempFileName(fileName ?? params.fileName ?? "download.bin"));
|
|
60
|
+
const cleanup = async () => {
|
|
61
|
+
try {
|
|
62
|
+
await cleanupTempDir(dir, params.onCleanupError);
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
unregisterTempDir();
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
return {
|
|
69
|
+
dir,
|
|
70
|
+
path: file(),
|
|
71
|
+
file,
|
|
72
|
+
cleanup,
|
|
73
|
+
[Symbol.asyncDispose]: cleanup,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export async function withTempFile(params, fn) {
|
|
77
|
+
const target = await tempFile(params);
|
|
78
|
+
try {
|
|
79
|
+
return await fn(target.path);
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
await target.cleanup();
|
|
83
|
+
}
|
|
84
|
+
}
|
package/dist/temp.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { tempWorkspace, type TempWorkspace, type TempWorkspaceOptions, tempWorkspaceSync, type TempWorkspaceSync, withTempWorkspace, withTempWorkspaceSync, } from "./private-temp-workspace.js";
|
|
2
|
+
export { resolveSecureTempRoot, type ResolveSecureTempRootOptions } from "./secure-temp-dir.js";
|
|
3
|
+
//# sourceMappingURL=temp.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"temp.d.ts","sourceRoot":"","sources":["../src/temp.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,KAAK,aAAa,EAClB,KAAK,oBAAoB,EACzB,iBAAiB,EACjB,KAAK,iBAAiB,EACtB,iBAAiB,EACjB,qBAAqB,GACtB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,qBAAqB,EAAE,KAAK,4BAA4B,EAAE,MAAM,sBAAsB,CAAC"}
|
package/dist/temp.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { FileHandle } from "node:fs/promises";
|
|
2
|
+
export type FsSafeTestHooks = {
|
|
3
|
+
afterPreOpenLstat?: (filePath: string) => Promise<void> | void;
|
|
4
|
+
beforeOpen?: (filePath: string, flags: number) => Promise<void> | void;
|
|
5
|
+
afterOpen?: (filePath: string, handle: FileHandle) => Promise<void> | void;
|
|
6
|
+
};
|
|
7
|
+
export declare function getFsSafeTestHooks(): FsSafeTestHooks | undefined;
|
|
8
|
+
export declare function __setFsSafeTestHooksForTest(hooks?: FsSafeTestHooks): void;
|
|
9
|
+
//# sourceMappingURL=test-hooks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-hooks.d.ts","sourceRoot":"","sources":["../src/test-hooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,MAAM,MAAM,eAAe,GAAG;IAC5B,iBAAiB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC/D,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACvE,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CAC5E,CAAC;AAQF,wBAAgB,kBAAkB,IAAI,eAAe,GAAG,SAAS,CAEhE;AAED,wBAAgB,2BAA2B,CAAC,KAAK,CAAC,EAAE,eAAe,GAAG,IAAI,CAKzE"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
let fsSafeTestHooks;
|
|
2
|
+
function allowFsSafeTestHooks() {
|
|
3
|
+
return process.env.NODE_ENV === "test" || process.env.VITEST === "true";
|
|
4
|
+
}
|
|
5
|
+
export function getFsSafeTestHooks() {
|
|
6
|
+
return fsSafeTestHooks;
|
|
7
|
+
}
|
|
8
|
+
export function __setFsSafeTestHooksForTest(hooks) {
|
|
9
|
+
if (hooks && !allowFsSafeTestHooks()) {
|
|
10
|
+
throw new Error("__setFsSafeTestHooksForTest is only available in tests");
|
|
11
|
+
}
|
|
12
|
+
fsSafeTestHooks = hooks;
|
|
13
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type WriteTextAtomicOptions = {
|
|
2
|
+
mode?: number;
|
|
3
|
+
dirMode?: number;
|
|
4
|
+
trailingNewline?: boolean;
|
|
5
|
+
};
|
|
6
|
+
export declare function writeTextAtomic(filePath: string, content: string, options?: WriteTextAtomicOptions): Promise<void>;
|
|
7
|
+
//# sourceMappingURL=text-atomic.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text-atomic.d.ts","sourceRoot":"","sources":["../src/text-atomic.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,sBAAsB,GAAG;IACnC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AAEF,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,IAAI,CAAC,CAWf"}
|