@openclaw/fs-safe 0.8.2 → 0.8.3
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 +9 -0
- package/README.md +1 -1
- package/dist/advanced.d.ts +2 -2
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +2 -2
- package/dist/archive-kind.d.ts +1 -0
- package/dist/archive-kind.d.ts.map +1 -1
- package/dist/archive-kind.js +6 -0
- package/dist/archive-limits.d.ts +0 -1
- package/dist/archive-limits.d.ts.map +1 -1
- package/dist/archive-limits.js +0 -3
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +5 -8
- package/dist/archive-parser-errors.d.ts +2 -0
- package/dist/archive-parser-errors.d.ts.map +1 -0
- package/dist/archive-parser-errors.js +12 -0
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +7 -13
- package/dist/archive-tar-wasm.d.ts +1 -1
- package/dist/archive-tar-wasm.d.ts.map +1 -1
- package/dist/archive-tar-wasm.js +5 -8
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +43 -6
- package/dist/atomic.d.ts +1 -1
- package/dist/atomic.d.ts.map +1 -1
- package/dist/atomic.js +1 -1
- package/dist/bounded-read-stream.d.ts +1 -5
- package/dist/bounded-read-stream.d.ts.map +1 -1
- package/dist/bounded-read-stream.js +5 -13
- package/dist/byte-budget.d.ts +0 -1
- package/dist/byte-budget.d.ts.map +1 -1
- package/dist/byte-budget.js +1 -1
- package/dist/directory-mode-owner.d.ts.map +1 -1
- package/dist/directory-mode-owner.js +2 -0
- package/dist/error-detail.d.ts +1 -0
- package/dist/error-detail.d.ts.map +1 -1
- package/dist/error-detail.js +6 -0
- package/dist/file-store-boundary.d.ts +1 -1
- package/dist/file-store-boundary.d.ts.map +1 -1
- package/dist/file-store-boundary.js +2 -13
- package/dist/file-store.js +1 -1
- package/dist/file-sync.d.ts +5 -0
- package/dist/file-sync.d.ts.map +1 -0
- package/dist/file-sync.js +19 -0
- package/dist/guarded-mutation.d.ts +3 -0
- package/dist/guarded-mutation.d.ts.map +1 -1
- package/dist/guarded-mutation.js +5 -1
- package/dist/home-dir.d.ts +0 -10
- package/dist/home-dir.d.ts.map +1 -1
- package/dist/home-dir.js +0 -28
- package/dist/json-durable-queue-ownership.d.ts +1 -0
- package/dist/json-durable-queue-ownership.d.ts.map +1 -1
- package/dist/json-durable-queue-ownership.js +139 -18
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +5 -10
- package/dist/json-store.d.ts.map +1 -1
- package/dist/json-store.js +1 -1
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +2 -3
- package/dist/move-path-cleanup.d.ts +1 -1
- package/dist/move-path-cleanup.d.ts.map +1 -1
- package/dist/move-path-cleanup.js +4 -2
- package/dist/move-path.d.ts +9 -0
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +53 -16
- package/dist/native-operations.d.ts +0 -1
- package/dist/native-operations.d.ts.map +1 -1
- package/dist/native-operations.js +0 -10
- package/dist/native-pinned-write-windows.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.js +5 -4
- package/dist/native-staged-file.d.ts +8 -1
- package/dist/native-staged-file.d.ts.map +1 -1
- package/dist/native-staged-file.js +30 -9
- package/dist/opened-realpath.d.ts +12 -1
- package/dist/opened-realpath.d.ts.map +1 -1
- package/dist/opened-realpath.js +8 -7
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +26 -2
- package/dist/path-policy.js +1 -1
- package/dist/permissions.d.ts +1 -0
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +3 -0
- package/dist/pinned-write.d.ts +1 -2
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +2 -11
- package/dist/private-temp-workspace.js +1 -1
- package/dist/publish-file.js +2 -2
- package/dist/replace-file-copy-source.d.ts.map +1 -1
- package/dist/replace-file-copy-source.js +2 -7
- package/dist/replace-file-descriptor.d.ts.map +1 -1
- package/dist/replace-file-descriptor.js +3 -16
- package/dist/replace-file-temp-owner.d.ts.map +1 -1
- package/dist/replace-file-temp-owner.js +2 -7
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +21 -8
- package/dist/root-errors.d.ts +1 -0
- package/dist/root-errors.d.ts.map +1 -1
- package/dist/root-errors.js +9 -0
- package/dist/root-file.d.ts.map +1 -1
- package/dist/root-file.js +17 -28
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +12 -33
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +49 -94
- package/dist/root-write-mode.d.ts +6 -0
- package/dist/root-write-mode.d.ts.map +1 -0
- package/dist/root-write-mode.js +60 -0
- package/dist/root-write-verification.js +2 -2
- package/dist/safe-path-segment.d.ts +1 -0
- package/dist/safe-path-segment.d.ts.map +1 -1
- package/dist/safe-path-segment.js +1 -1
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +2 -9
- package/dist/secret-read-async.d.ts.map +1 -1
- package/dist/secret-read-async.js +2 -10
- package/dist/secret-read-policy.d.ts +4 -0
- package/dist/secret-read-policy.d.ts.map +1 -1
- package/dist/secret-read-policy.js +12 -0
- package/dist/sibling-staged-file.d.ts.map +1 -1
- package/dist/sibling-staged-file.js +2 -7
- package/dist/sidecar-lock-policy.d.ts +0 -1
- package/dist/sidecar-lock-policy.d.ts.map +1 -1
- package/dist/sidecar-lock-policy.js +0 -4
- package/dist/string-coerce.d.ts +0 -9
- package/dist/string-coerce.d.ts.map +1 -1
- package/dist/string-coerce.js +0 -56
- package/dist/temp-target.d.ts.map +1 -1
- package/dist/temp-target.js +1 -12
- package/docs/atomic.md +53 -19
- package/docs/staged-file.md +6 -2
- package/docs/writing.md +5 -0
- package/package.json +8 -8
- package/dist/archive-tar-extract.d.ts +0 -11
- package/dist/archive-tar-extract.d.ts.map +0 -1
- package/dist/archive-tar-extract.js +0 -49
- package/dist/fsync.d.ts +0 -2
- package/dist/fsync.d.ts.map +0 -1
- package/dist/fsync.js +0 -1
- package/dist/json-durable-queue-retirement.d.ts +0 -9
- package/dist/json-durable-queue-retirement.d.ts.map +0 -1
- package/dist/json-durable-queue-retirement.js +0 -126
- package/dist/json-durable-queue-transfer-lock.d.ts +0 -2
- package/dist/json-durable-queue-transfer-lock.d.ts.map +0 -1
- package/dist/json-durable-queue-transfer-lock.js +0 -19
- package/dist/mode.d.ts +0 -2
- package/dist/mode.d.ts.map +0 -1
- package/dist/mode.js +0 -3
- package/dist/output-sibling.d.ts +0 -8
- package/dist/output-sibling.d.ts.map +0 -1
- package/dist/output-sibling.js +0 -30
- package/dist/read-error.d.ts +0 -2
- package/dist/read-error.d.ts.map +0 -1
- package/dist/read-error.js +0 -11
- package/dist/short-path.d.ts +0 -2
- package/dist/short-path.d.ts.map +0 -1
- package/dist/short-path.js +0 -7
- package/dist/staged-file.d.ts +0 -10
- package/dist/staged-file.d.ts.map +0 -1
- package/dist/staged-file.js +0 -15
|
@@ -1,21 +1,13 @@
|
|
|
1
1
|
import fsSync from "node:fs";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import { readFileHandleBounded } from "./bounded-read.js";
|
|
4
|
-
import { normalizeMaxBytes } from "./byte-budget.js";
|
|
5
4
|
import { assertNoUnsafeDeviceReadPath } from "./device-path.js";
|
|
6
5
|
import { FsSafeError } from "./errors.js";
|
|
7
|
-
import { resolveHomeRelativePath } from "./home-dir.js";
|
|
8
6
|
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
9
7
|
import { inspectFileIdentity } from "./strict-file-identity.js";
|
|
10
|
-
import { assertSecretFilePreview,
|
|
8
|
+
import { assertSecretFilePreview, resolveSecretReadPolicy, secretPathErrorCode, secretReadError, trimSecretFileContent, } from "./secret-read-policy.js";
|
|
11
9
|
export async function readSecretFile(filePath, label, options = {}) {
|
|
12
|
-
const resolvedPath =
|
|
13
|
-
if (!resolvedPath) {
|
|
14
|
-
throw new FsSafeError("invalid-path", `${label} file path is empty.`, { cause: undefined });
|
|
15
|
-
}
|
|
16
|
-
const maxBytes = normalizeMaxBytes(options.maxBytes, {
|
|
17
|
-
defaultValue: DEFAULT_SECRET_FILE_MAX_BYTES,
|
|
18
|
-
});
|
|
10
|
+
const { resolvedPath, maxBytes } = resolveSecretReadPolicy(filePath, label, options);
|
|
19
11
|
async function inspectInput(symlinkMessage) {
|
|
20
12
|
const stat = options.rejectSymlink
|
|
21
13
|
? await fs.lstat(resolvedPath, { bigint: true })
|
|
@@ -10,4 +10,8 @@ export declare function secretPathErrorCode(error: unknown): FsSafeErrorCode;
|
|
|
10
10
|
export declare function secretReadError(code: FsSafeErrorCode, action: "inspect" | "read", label: string, resolvedPath: string, error: unknown): FsSafeError;
|
|
11
11
|
export declare function assertSecretFilePreview(stat: BigIntStats, label: string, resolvedPath: string, maxBytes: number, rejectHardlinks: boolean): void;
|
|
12
12
|
export declare function trimSecretFileContent(raw: string, label: string, resolvedPath: string): string;
|
|
13
|
+
export declare function resolveSecretReadPolicy(filePath: string, label: string, options: SecretFileReadOptions): {
|
|
14
|
+
resolvedPath: string;
|
|
15
|
+
maxBytes: number;
|
|
16
|
+
};
|
|
13
17
|
//# sourceMappingURL=secret-read-policy.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secret-read-policy.d.ts","sourceRoot":"","sources":["../src/secret-read-policy.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"secret-read-policy.d.ts","sourceRoot":"","sources":["../src/secret-read-policy.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAEhE,eAAO,MAAM,6BAA6B,QAAY,CAAC;AAEvD,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AAEF,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,CAGnE;AAED,wBAAgB,eAAe,CAC7B,IAAI,EAAE,eAAe,EACrB,MAAM,EAAE,SAAS,GAAG,MAAM,EAC1B,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,OAAO,GACb,WAAW,CAGb;AAED,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,WAAW,EACjB,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,eAAe,EAAE,OAAO,GACvB,IAAI,CAUN;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAM9F;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,qBAAqB,GAAG;IACxG,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;CAClB,CASA"}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { normalizeMaxBytes } from "./byte-budget.js";
|
|
2
|
+
import { resolveHomeRelativePath } from "./home-dir.js";
|
|
1
3
|
import { FsSafeError } from "./errors.js";
|
|
2
4
|
export const DEFAULT_SECRET_FILE_MAX_BYTES = 16 * 1024;
|
|
3
5
|
export function secretPathErrorCode(error) {
|
|
@@ -26,3 +28,13 @@ export function trimSecretFileContent(raw, label, resolvedPath) {
|
|
|
26
28
|
}
|
|
27
29
|
return secret;
|
|
28
30
|
}
|
|
31
|
+
export function resolveSecretReadPolicy(filePath, label, options) {
|
|
32
|
+
const resolvedPath = resolveHomeRelativePath(filePath.trim());
|
|
33
|
+
if (!resolvedPath) {
|
|
34
|
+
throw new FsSafeError("invalid-path", `${label} file path is empty.`, { cause: undefined });
|
|
35
|
+
}
|
|
36
|
+
const maxBytes = normalizeMaxBytes(options.maxBytes, {
|
|
37
|
+
defaultValue: DEFAULT_SECRET_FILE_MAX_BYTES,
|
|
38
|
+
});
|
|
39
|
+
return { resolvedPath, maxBytes };
|
|
40
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sibling-staged-file.d.ts","sourceRoot":"","sources":["../src/sibling-staged-file.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sibling-staged-file.d.ts","sourceRoot":"","sources":["../src/sibling-staged-file.ts"],"names":[],"mappings":"AAmCA,wBAAsB,oBAAoB,CAAC,CAAC,EAAE,MAAM,EAAE;IACpD,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACxC,gBAAgB,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,MAAM,CAAC;IACxC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kEAAkE;IAClE,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,OAAO,CAAC;IACtB,aAAa,EAAE,OAAO,CAAC;CACxB,GAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,CAAC,CAAA;CAAE,CAAC,CAyG3C"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { syncFileBestEffort } from "./file-sync.js";
|
|
1
2
|
import fsSync, {} from "node:fs";
|
|
2
3
|
import fs, {} from "node:fs/promises";
|
|
3
4
|
import path from "node:path";
|
|
@@ -92,13 +93,7 @@ export async function writeCallbackSibling(params) {
|
|
|
92
93
|
}
|
|
93
94
|
if (params.syncTempFile) {
|
|
94
95
|
await assertCurrent(params.tempPath);
|
|
95
|
-
|
|
96
|
-
await handle.sync();
|
|
97
|
-
}
|
|
98
|
-
catch (error) {
|
|
99
|
-
if (error?.code !== "EPERM")
|
|
100
|
-
throw error;
|
|
101
|
-
}
|
|
96
|
+
await syncFileBestEffort(handle);
|
|
102
97
|
}
|
|
103
98
|
await assertCurrent(params.tempPath);
|
|
104
99
|
await fs.rename(params.tempPath, filePath);
|
|
@@ -4,7 +4,6 @@ export declare function validateSidecarLockTimeoutMs(timeoutMs: number | undefin
|
|
|
4
4
|
export declare function computeSidecarLockDelayMs(retry: SidecarLockRetryOptions, attempt: number): number;
|
|
5
5
|
export declare const maxTransientLockDenials = 8;
|
|
6
6
|
export declare function isTransientLockFileDenial(error: unknown, lockPath: string): boolean;
|
|
7
|
-
export declare function sidecarLockPayloadIsStale(payload: unknown, staleMs: number, nowMs: number): boolean;
|
|
8
7
|
export declare function sidecarLockPayloadCreatedAtMs(payload: unknown): number | null;
|
|
9
8
|
export declare function defaultSidecarLockShouldReclaim(params: {
|
|
10
9
|
lockPath: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sidecar-lock-policy.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-policy.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAQvE,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,uBAAuB,GAAG,IAAI,CAkBpF;AAED,wBAAgB,4BAA4B,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAGhF;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,uBAAuB,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAQjG;AASD,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAEzC,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAGnF;AAED,wBAAgB,
|
|
1
|
+
{"version":3,"file":"sidecar-lock-policy.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-policy.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAQvE,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,uBAAuB,GAAG,IAAI,CAkBpF;AAED,wBAAgB,4BAA4B,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAGhF;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,uBAAuB,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAQjG;AASD,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAEzC,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAGnF;AAED,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAU7E;AAED,wBAAsB,+BAA+B,CAAC,MAAM,EAAE;IAC5D,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf,GAAG,OAAO,CAAC,OAAO,CAAC,CAQnB"}
|
|
@@ -48,10 +48,6 @@ export function isTransientLockFileDenial(error, lockPath) {
|
|
|
48
48
|
const denial = error;
|
|
49
49
|
return process.platform === "win32" && denial?.code === "EPERM" && denial.path === lockPath;
|
|
50
50
|
}
|
|
51
|
-
export function sidecarLockPayloadIsStale(payload, staleMs, nowMs) {
|
|
52
|
-
const createdAtMs = sidecarLockPayloadCreatedAtMs(payload);
|
|
53
|
-
return createdAtMs !== null && nowMs - createdAtMs > staleMs;
|
|
54
|
-
}
|
|
55
51
|
export function sidecarLockPayloadCreatedAtMs(payload) {
|
|
56
52
|
const createdAt = payload &&
|
|
57
53
|
typeof payload === "object" &&
|
package/dist/string-coerce.d.ts
CHANGED
|
@@ -1,14 +1,5 @@
|
|
|
1
|
-
export declare function readStringValue(value: unknown): string | undefined;
|
|
2
1
|
export declare function normalizeNullableString(value: unknown): string | null;
|
|
3
2
|
export declare function normalizeOptionalString(value: unknown): string | undefined;
|
|
4
|
-
export declare function normalizeStringifiedOptionalString(value: unknown): string | undefined;
|
|
5
3
|
export declare function normalizeOptionalLowercaseString(value: unknown): string | undefined;
|
|
6
4
|
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
5
|
//# sourceMappingURL=string-coerce.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"string-coerce.d.ts","sourceRoot":"","sources":["../src/string-coerce.ts"],"names":[],"mappings":"AAAA,wBAAgB,
|
|
1
|
+
{"version":3,"file":"string-coerce.d.ts","sourceRoot":"","sources":["../src/string-coerce.ts"],"names":[],"mappings":"AAAA,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,gCAAgC,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAEnF;AAED,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEtE"}
|
package/dist/string-coerce.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
export function readStringValue(value) {
|
|
2
|
-
return typeof value === "string" ? value : undefined;
|
|
3
|
-
}
|
|
4
1
|
export function normalizeNullableString(value) {
|
|
5
2
|
if (typeof value !== "string") {
|
|
6
3
|
return null;
|
|
@@ -11,62 +8,9 @@ export function normalizeNullableString(value) {
|
|
|
11
8
|
export function normalizeOptionalString(value) {
|
|
12
9
|
return normalizeNullableString(value) ?? undefined;
|
|
13
10
|
}
|
|
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
11
|
export function normalizeOptionalLowercaseString(value) {
|
|
24
12
|
return normalizeOptionalString(value)?.toLowerCase();
|
|
25
13
|
}
|
|
26
14
|
export function normalizeLowercaseStringOrEmpty(value) {
|
|
27
15
|
return normalizeOptionalLowercaseString(value) ?? "";
|
|
28
16
|
}
|
|
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
|
-
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"temp-target.d.ts","sourceRoot":"","sources":["../src/temp-target.ts"],"names":[],"mappings":"AAQA,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;
|
|
1
|
+
{"version":3,"file":"temp-target.d.ts","sourceRoot":"","sources":["../src/temp-target.ts"],"names":[],"mappings":"AAQA,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;AA8DF,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,CAaT;AAsCD,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,CAwBpB;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"}
|
package/dist/temp-target.js
CHANGED
|
@@ -2,7 +2,7 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { sameFileIdentityForCleanup } from "./file-identity.js";
|
|
5
|
-
import { assertSafePathSegment, sanitizeSafePathSegment } from "./safe-path-segment.js";
|
|
5
|
+
import { assertSafePathSegment, sanitizeSafePathSegment, trimHyphenEdges } from "./safe-path-segment.js";
|
|
6
6
|
import { resolveSecureTempRoot } from "./secure-temp-dir.js";
|
|
7
7
|
import { registerTempPathForExit } from "./temp-cleanup.js";
|
|
8
8
|
const HYPHEN_CHAR_CODE = 0x2d;
|
|
@@ -14,17 +14,6 @@ const UPPERCASE_Z_CHAR_CODE = 0x5a;
|
|
|
14
14
|
const UNDERSCORE_CHAR_CODE = 0x5f;
|
|
15
15
|
const LOWERCASE_A_CHAR_CODE = 0x61;
|
|
16
16
|
const LOWERCASE_Z_CHAR_CODE = 0x7a;
|
|
17
|
-
function trimHyphenEdges(value) {
|
|
18
|
-
let start = 0;
|
|
19
|
-
let end = value.length;
|
|
20
|
-
while (start < end && value.charCodeAt(start) === HYPHEN_CHAR_CODE) {
|
|
21
|
-
start += 1;
|
|
22
|
-
}
|
|
23
|
-
while (end > start && value.charCodeAt(end - 1) === HYPHEN_CHAR_CODE) {
|
|
24
|
-
end -= 1;
|
|
25
|
-
}
|
|
26
|
-
return start === 0 && end === value.length ? value : value.slice(start, end);
|
|
27
|
-
}
|
|
28
17
|
function isExtensionCharCode(charCode) {
|
|
29
18
|
return ((charCode >= NUMBER_ZERO_CHAR_CODE && charCode <= NUMBER_NINE_CHAR_CODE) ||
|
|
30
19
|
(charCode >= UPPERCASE_A_CHAR_CODE && charCode <= UPPERCASE_Z_CHAR_CODE) ||
|
package/docs/atomic.md
CHANGED
|
@@ -173,7 +173,8 @@ metadata where lower latency matters more than crash-durability.
|
|
|
173
173
|
|
|
174
174
|
## `movePathWithCopyFallback`
|
|
175
175
|
|
|
176
|
-
Rename a path. If the rename fails with `EXDEV` (cross-device),
|
|
176
|
+
Rename a path. If the rename fails with `EXDEV` (cross-device), or `EPERM` on
|
|
177
|
+
Windows, fall back to
|
|
177
178
|
copying into a staged sibling path, renaming that staged path into place, and
|
|
178
179
|
then removing only the source entries that were copied. The fallback avoids
|
|
179
180
|
buffering regular files into memory and does not tighten the destination parent
|
|
@@ -215,39 +216,72 @@ identity becomes the next cleanup receipt. This accounts for the operation's
|
|
|
215
216
|
own link-count and ctime changes without suppressing unexpected external
|
|
216
217
|
mutations.
|
|
217
218
|
|
|
218
|
-
###
|
|
219
|
+
### Mutation authority and publication receipts
|
|
219
220
|
|
|
220
|
-
Pass `
|
|
221
|
-
caller-owned authorization. The helper captures
|
|
222
|
-
runs it synchronously after asynchronous preparation and
|
|
223
|
-
immediately before each rename
|
|
221
|
+
Pass `assertBeforeMutation` when a move depends on a revocable lease or another
|
|
222
|
+
caller-owned authorization. The helper captures the callback when called and
|
|
223
|
+
runs it synchronously after asynchronous preparation and identity checks,
|
|
224
|
+
immediately before each rename, source-file or source-symlink unlink, and
|
|
225
|
+
source-directory removal. `assertBeforeRename` remains available for callers
|
|
226
|
+
that only guard publication. When both are set, the rename check runs first,
|
|
227
|
+
then the mutation check, without yielding before dispatch.
|
|
224
228
|
|
|
225
229
|
```ts
|
|
230
|
+
type MovePathPublicationReceipt = Readonly<{
|
|
231
|
+
path: string;
|
|
232
|
+
dev: bigint;
|
|
233
|
+
ino: bigint;
|
|
234
|
+
}>;
|
|
235
|
+
|
|
226
236
|
type MovePathWithCopyFallbackOptions = {
|
|
227
237
|
from: string;
|
|
228
238
|
sourceHardlinks?: "allow" | "reject";
|
|
229
239
|
to: string;
|
|
230
240
|
assertBeforeRename?: () => void;
|
|
241
|
+
assertBeforeMutation?: () => void;
|
|
242
|
+
onDestinationPublished?: (receipt: MovePathPublicationReceipt) => void;
|
|
231
243
|
};
|
|
232
244
|
```
|
|
233
245
|
|
|
234
|
-
Throw to refuse
|
|
235
|
-
with `EXDEV` or `EPERM` codes; an authorization failure never starts a
|
|
236
|
-
fallback. A genuine rename failure may still require a second
|
|
246
|
+
Throw to refuse the next mutation. The original error is propagated, including
|
|
247
|
+
errors with `EXDEV` or `EPERM` codes; an authorization failure never starts a
|
|
248
|
+
copy fallback. A genuine rename failure may still require a second authority
|
|
237
249
|
check before publishing the staged copy.
|
|
238
250
|
|
|
239
|
-
|
|
240
|
-
or any other value
|
|
241
|
-
results are consumed
|
|
242
|
-
|
|
243
|
-
|
|
251
|
+
All three callbacks must return `undefined` synchronously. Returning a Promise,
|
|
252
|
+
thenable, or any other value fails with a `TypeError`; rejected asynchronous
|
|
253
|
+
results are consumed. Perform asynchronous policy checks before calling the
|
|
254
|
+
helper and use the authority callback to recheck the current owner at each
|
|
255
|
+
mutation boundary. All callbacks are captured before the first await.
|
|
256
|
+
|
|
257
|
+
`onDestinationPublished` runs exactly once after a successful rename resolves,
|
|
258
|
+
before awaited post-rename directory checks or source cleanup. It receives a
|
|
259
|
+
frozen receipt with the resolved absolute destination path and the exact bigint
|
|
260
|
+
device/inode identity observed on the rename source before dispatch: the
|
|
261
|
+
original source for a direct rename, or the staged copy for fallback. Failed
|
|
262
|
+
rename attempts never emit receipts. The return contract remains `Promise<void>`.
|
|
263
|
+
|
|
264
|
+
The receipt records an observed identity, not authorization, durable storage,
|
|
265
|
+
or an atomic guarantee against concurrent pathname replacement. Before recovery,
|
|
266
|
+
recheck caller authority and compare the retained identity with a fresh bigint
|
|
267
|
+
stat of the destination. In particular, unknown Windows device/inode values
|
|
268
|
+
must not be treated as proof of ownership. Do not derive ownership from a new
|
|
269
|
+
post-failure snapshot alone.
|
|
244
270
|
|
|
245
271
|
A refused rename leaves the source and destination unchanged; any private
|
|
246
|
-
staged copy follows
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
272
|
+
staged copy follows normal cleanup. If publication has already happened, a
|
|
273
|
+
callback error or later verification failure preserves the published destination
|
|
274
|
+
and stops further source cleanup. Revocation during cleanup leaves all
|
|
275
|
+
as-yet-unremoved source entries intact; entries already removed remain removed.
|
|
276
|
+
The caller retains the receipt even if the move later rejects and owns recovery
|
|
277
|
+
from this partial-move state. An observer that throws must retain its receipt
|
|
278
|
+
before throwing if recovery needs it.
|
|
279
|
+
|
|
280
|
+
These callbacks do not cancel already-dispatched operations or make an external
|
|
281
|
+
lease store atomic with the filesystem. `assertBeforeMutation` guards renames
|
|
282
|
+
and removal of copied source entries; private staging creation, writes, and
|
|
283
|
+
failed-staging cleanup remain owned by the helper. Omitting the new callbacks
|
|
284
|
+
preserves the existing move and source-identity behavior.
|
|
251
285
|
|
|
252
286
|
## Difference from `root()`
|
|
253
287
|
|
package/docs/staged-file.md
CHANGED
|
@@ -66,8 +66,12 @@ Strings are UTF-8. `mode` is the requested **published** mode and defaults to
|
|
|
66
66
|
`0600`; exact final modes, including `000`, are supported. The unpublished file
|
|
67
67
|
stays at `0600` throughout preparation and any awaited application checks.
|
|
68
68
|
After rename succeeds and the published entry passes identity validation, the
|
|
69
|
-
owner applies the requested mode through its retained file descriptor
|
|
70
|
-
|
|
69
|
+
owner applies the requested mode through its retained file descriptor. Content
|
|
70
|
+
was synchronized during preparation; publication always synchronizes the parent.
|
|
71
|
+
Modes retaining owner read/write skip the extra mode-only file sync, so a crash
|
|
72
|
+
may leave the tighter staged `0600` instead of the wider requested mode. Modes
|
|
73
|
+
removing owner read or write, and corrections of an observed wider staged mode,
|
|
74
|
+
retain the post-chmod file sync. No parent is created or chmodded.
|
|
71
75
|
Creation uses an exclusive, no-follow, close-on-exec open of a generated direct
|
|
72
76
|
child name. Writes use that descriptor. Inspection uses non-following metadata
|
|
73
77
|
operations, never a potentially blocking reopen of the leaf.
|
package/docs/writing.md
CHANGED
|
@@ -84,6 +84,11 @@ final verification uses a descriptor retained by the writer rather than reopenin
|
|
|
84
84
|
the published file. The requested mode is not relaxed for verification.
|
|
85
85
|
Publication verification compares exact bigint descriptor and pathname identities,
|
|
86
86
|
including large file indexes that cannot be represented by a JavaScript number.
|
|
87
|
+
Native publication syncs content before rename and always syncs the parent directory.
|
|
88
|
+
Modes that retain owner read/write skip the extra mode-only file sync: after a
|
|
89
|
+
crash, the file may retain staged mode `0o600` instead of the wider requested mode.
|
|
90
|
+
Modes that remove owner read or write, and corrections of observed wider staging
|
|
91
|
+
permissions, keep the post-chmod file sync.
|
|
87
92
|
Later reads still obey OS permissions, and access checks on a pre-existing
|
|
88
93
|
destination are unchanged. The explicit FUSE compatibility policy still requires
|
|
89
94
|
a readable destination to prove matching content when rename changes its identity.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/fs-safe",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.3",
|
|
4
4
|
"description": "Capability-style filesystem roots for Node.js apps that handle untrusted relative paths.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"filesystem",
|
|
@@ -156,13 +156,13 @@
|
|
|
156
156
|
"archive:producer-smoke": "node scripts/archive-producer-smoke.mjs"
|
|
157
157
|
},
|
|
158
158
|
"optionalDependencies": {
|
|
159
|
-
"@openclaw/fs-safe-darwin-arm64": "0.8.
|
|
160
|
-
"@openclaw/fs-safe-darwin-x64": "0.8.
|
|
161
|
-
"@openclaw/fs-safe-linux-arm64-gnu": "0.8.
|
|
162
|
-
"@openclaw/fs-safe-linux-arm64-musl": "0.8.
|
|
163
|
-
"@openclaw/fs-safe-linux-x64-gnu": "0.8.
|
|
164
|
-
"@openclaw/fs-safe-linux-x64-musl": "0.8.
|
|
165
|
-
"@openclaw/fs-safe-win32-x64-msvc": "0.8.
|
|
159
|
+
"@openclaw/fs-safe-darwin-arm64": "0.8.3",
|
|
160
|
+
"@openclaw/fs-safe-darwin-x64": "0.8.3",
|
|
161
|
+
"@openclaw/fs-safe-linux-arm64-gnu": "0.8.3",
|
|
162
|
+
"@openclaw/fs-safe-linux-arm64-musl": "0.8.3",
|
|
163
|
+
"@openclaw/fs-safe-linux-x64-gnu": "0.8.3",
|
|
164
|
+
"@openclaw/fs-safe-linux-x64-musl": "0.8.3",
|
|
165
|
+
"@openclaw/fs-safe-win32-x64-msvc": "0.8.3",
|
|
166
166
|
"jszip": "^3.10.1"
|
|
167
167
|
},
|
|
168
168
|
"devDependencies": {
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { type ExtractionDeadline } from "./archive-deadline.js";
|
|
2
|
-
import type { ResolvedArchiveExtractLimits, TarMeterLimits } from "./archive-limits.js";
|
|
3
|
-
import type { ExtractArchiveOptions } from "./archive-options.js";
|
|
4
|
-
export declare function extractWasmTar(params: {
|
|
5
|
-
archivePath: string;
|
|
6
|
-
options: ExtractArchiveOptions;
|
|
7
|
-
limits: ResolvedArchiveExtractLimits;
|
|
8
|
-
tarLimits: TarMeterLimits;
|
|
9
|
-
deadline: ExtractionDeadline;
|
|
10
|
-
}): Promise<void>;
|
|
11
|
-
//# sourceMappingURL=archive-tar-extract.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"archive-tar-extract.d.ts","sourceRoot":"","sources":["../src/archive-tar-extract.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,KAAK,EAAE,4BAA4B,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAExF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAQlE,wBAAsB,cAAc,CAAC,MAAM,EAAE;IAC3C,WAAW,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,qBAAqB,CAAC;IAAC,MAAM,EAAE,4BAA4B,CAAC;IAC1F,SAAS,EAAE,cAAc,CAAC;IAAC,QAAQ,EAAE,kBAAkB,CAAC;CACzD,GAAG,OAAO,CAAC,IAAI,CAAC,CAqChB"}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { Readable } from "node:stream";
|
|
4
|
-
import { stripArchivePath } from "./archive-entry.js";
|
|
5
|
-
import {} from "./archive-deadline.js";
|
|
6
|
-
import { mergePlannedArchiveIntoDestination } from "./archive-merge.js";
|
|
7
|
-
import { resolveArchiveEntryMode } from "./archive-policy.js";
|
|
8
|
-
import { prepareArchiveDestinationDir, preparePrivateArchiveOutputPath, withStagedArchiveDestination } from "./archive-staging.js";
|
|
9
|
-
import { createTarEntryPreflightChecker } from "./archive-tar.js";
|
|
10
|
-
import { inspectTar, replayTar } from "./archive-tar-stream.js";
|
|
11
|
-
import { runPinnedWriteHelper } from "./pinned-write.js";
|
|
12
|
-
export async function extractWasmTar(params) {
|
|
13
|
-
const { options, deadline, tarLimits } = params;
|
|
14
|
-
const manifest = [];
|
|
15
|
-
await inspectTar({ archivePath: params.archivePath, limits: tarLimits, signal: deadline.signal,
|
|
16
|
-
onMember: (entry) => { manifest.push(entry); } });
|
|
17
|
-
deadline.check();
|
|
18
|
-
const destinationRealDir = await prepareArchiveDestinationDir(options.destDir);
|
|
19
|
-
await withStagedArchiveDestination({ destinationRealDir, run: async (stagingPath) => {
|
|
20
|
-
const stagingDir = await fs.realpath(stagingPath);
|
|
21
|
-
const check = createTarEntryPreflightChecker({ rootDir: destinationRealDir,
|
|
22
|
-
stripComponents: options.stripComponents, limits: params.limits,
|
|
23
|
-
entryFilter: options.entryFilter, onFiltered: options.onFiltered });
|
|
24
|
-
const strip = Math.max(0, Math.floor(options.stripComponents ?? 0));
|
|
25
|
-
const accepted = manifest.filter((entry) => { deadline.check(); return check(entry); }).map((entry) => {
|
|
26
|
-
const kind = entry.type === "Directory" || entry.type === "GNUDumpDir" ? "directory" : "file";
|
|
27
|
-
return { ...entry, path: stripArchivePath(entry.path, strip), kind,
|
|
28
|
-
mode: resolveArchiveEntryMode({ kind, archivedMode: entry.mode, policy: options.entryModes }) };
|
|
29
|
-
});
|
|
30
|
-
await replayTar({ archivePath: params.archivePath, limits: tarLimits, signal: deadline.signal, members: accepted,
|
|
31
|
-
async consume(member, payload) {
|
|
32
|
-
deadline.check();
|
|
33
|
-
await preparePrivateArchiveOutputPath({ destinationDir: stagingDir, destinationRealDir: stagingDir,
|
|
34
|
-
relPath: member.path, outPath: path.join(stagingDir, member.path), originalPath: member.path,
|
|
35
|
-
isDirectory: member.kind === "directory", deadline });
|
|
36
|
-
if (member.kind === "file") {
|
|
37
|
-
await runPinnedWriteHelper({ rootPath: stagingDir, relativeParentPath: path.posix.dirname(member.path),
|
|
38
|
-
basename: path.posix.basename(member.path), mkdir: false, mode: 0o600, overwrite: false,
|
|
39
|
-
maxBytes: member.size, input: { kind: "stream", stream: Readable.from(payload) } });
|
|
40
|
-
}
|
|
41
|
-
deadline.check();
|
|
42
|
-
},
|
|
43
|
-
});
|
|
44
|
-
deadline.check();
|
|
45
|
-
await mergePlannedArchiveIntoDestination({ entries: accepted, sourceDir: stagingDir,
|
|
46
|
-
destinationDir: options.destDir, destinationRealDir, deadline });
|
|
47
|
-
deadline.check();
|
|
48
|
-
} });
|
|
49
|
-
}
|
package/dist/fsync.d.ts
DELETED
package/dist/fsync.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"fsync.d.ts","sourceRoot":"","sources":["../src/fsync.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAC"}
|
package/dist/fsync.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { syncDirectoryBestEffort } from "./directory-durability.js";
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
export declare function recoverDurableQueueRetirement(params: {
|
|
2
|
-
jsonPath: string;
|
|
3
|
-
processingPath: string;
|
|
4
|
-
}): Promise<void>;
|
|
5
|
-
export declare function retireDurableQueueSource(params: {
|
|
6
|
-
jsonPath: string;
|
|
7
|
-
processingPath: string;
|
|
8
|
-
}): Promise<void>;
|
|
9
|
-
//# sourceMappingURL=json-durable-queue-retirement.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"json-durable-queue-retirement.d.ts","sourceRoot":"","sources":["../src/json-durable-queue-retirement.ts"],"names":[],"mappings":"AAgFA,wBAAsB,6BAA6B,CAAC,MAAM,EAAE;IAC1D,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;CACxB,GAAG,OAAO,CAAC,IAAI,CAAC,CAmChB;AAED,wBAAsB,wBAAwB,CAAC,MAAM,EAAE;IACrD,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;CACxB,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBhB"}
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { syncDirectory } from "./directory-durability.js";
|
|
4
|
-
import { FsSafeError } from "./errors.js";
|
|
5
|
-
import { sameFileIdentityForCleanup } from "./file-identity.js";
|
|
6
|
-
const RETIREMENT_ROOT_NAME = ".fs-safe-retirements";
|
|
7
|
-
const RETIREMENT_ENTRY_NAME = "entry";
|
|
8
|
-
function getErrorCode(error) {
|
|
9
|
-
return error && typeof error === "object" && "code" in error
|
|
10
|
-
? String(error.code)
|
|
11
|
-
: null;
|
|
12
|
-
}
|
|
13
|
-
async function lstatOrNull(filePath) {
|
|
14
|
-
try {
|
|
15
|
-
return await fs.lstat(filePath, { bigint: true });
|
|
16
|
-
}
|
|
17
|
-
catch (error) {
|
|
18
|
-
if (getErrorCode(error) === "ENOENT")
|
|
19
|
-
return null;
|
|
20
|
-
throw error;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
function assertDirectory(identity, label) {
|
|
24
|
-
if (identity.isSymbolicLink() || !identity.isDirectory()) {
|
|
25
|
-
throw new FsSafeError("path-mismatch", `${label} is not an owned directory`);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
async function regularFileIdentity(filePath) {
|
|
29
|
-
const identity = await lstatOrNull(filePath);
|
|
30
|
-
if (!identity)
|
|
31
|
-
return null;
|
|
32
|
-
if (identity.isSymbolicLink() || !identity.isFile()) {
|
|
33
|
-
throw new FsSafeError("path-mismatch", "queue retirement path is not a regular file");
|
|
34
|
-
}
|
|
35
|
-
return identity;
|
|
36
|
-
}
|
|
37
|
-
function retirementPaths(jsonPath) {
|
|
38
|
-
const rootPath = path.join(path.dirname(jsonPath), RETIREMENT_ROOT_NAME);
|
|
39
|
-
const dirPath = path.join(rootPath, path.basename(jsonPath));
|
|
40
|
-
return { dirPath, entryPath: path.join(dirPath, RETIREMENT_ENTRY_NAME), rootPath };
|
|
41
|
-
}
|
|
42
|
-
async function inspectRetirementRoot(jsonPath) {
|
|
43
|
-
const rootPath = retirementPaths(jsonPath).rootPath;
|
|
44
|
-
const root = await lstatOrNull(rootPath);
|
|
45
|
-
if (!root)
|
|
46
|
-
return null;
|
|
47
|
-
assertDirectory(root, "queue retirement root");
|
|
48
|
-
return rootPath;
|
|
49
|
-
}
|
|
50
|
-
async function ensureRetirementRoot(jsonPath) {
|
|
51
|
-
const parentPath = path.dirname(jsonPath);
|
|
52
|
-
const rootPath = retirementPaths(jsonPath).rootPath;
|
|
53
|
-
await fs.mkdir(rootPath, { mode: 0o700 }).catch((error) => {
|
|
54
|
-
if (getErrorCode(error) !== "EEXIST")
|
|
55
|
-
throw error;
|
|
56
|
-
});
|
|
57
|
-
const root = await fs.lstat(rootPath, { bigint: true });
|
|
58
|
-
assertDirectory(root, "queue retirement root");
|
|
59
|
-
await syncDirectory(parentPath);
|
|
60
|
-
return rootPath;
|
|
61
|
-
}
|
|
62
|
-
async function removeRetirementRecord(params) {
|
|
63
|
-
await fs.rmdir(params.dirPath).catch((error) => {
|
|
64
|
-
if (getErrorCode(error) !== "ENOENT")
|
|
65
|
-
throw error;
|
|
66
|
-
});
|
|
67
|
-
await syncDirectory(params.rootPath);
|
|
68
|
-
}
|
|
69
|
-
export async function recoverDurableQueueRetirement(params) {
|
|
70
|
-
const rootPath = await inspectRetirementRoot(params.jsonPath);
|
|
71
|
-
if (!rootPath)
|
|
72
|
-
return;
|
|
73
|
-
const { dirPath, entryPath } = retirementPaths(params.jsonPath);
|
|
74
|
-
const dir = await lstatOrNull(dirPath);
|
|
75
|
-
if (!dir)
|
|
76
|
-
return;
|
|
77
|
-
assertDirectory(dir, "queue retirement record");
|
|
78
|
-
const names = await fs.readdir(dirPath);
|
|
79
|
-
if (names.length === 0) {
|
|
80
|
-
await removeRetirementRecord({ dirPath, rootPath });
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
if (names.length !== 1 || names[0] !== RETIREMENT_ENTRY_NAME) {
|
|
84
|
-
throw new FsSafeError("path-mismatch", "queue retirement record is ambiguous");
|
|
85
|
-
}
|
|
86
|
-
const entry = await regularFileIdentity(entryPath);
|
|
87
|
-
const processing = await regularFileIdentity(params.processingPath);
|
|
88
|
-
if (!entry || !processing) {
|
|
89
|
-
throw new FsSafeError("path-mismatch", "queue retirement ownership is incomplete");
|
|
90
|
-
}
|
|
91
|
-
if (!sameFileIdentityForCleanup(entry, processing)) {
|
|
92
|
-
const pending = await regularFileIdentity(params.jsonPath);
|
|
93
|
-
if (!pending) {
|
|
94
|
-
await fs.link(entryPath, params.jsonPath);
|
|
95
|
-
await syncDirectory(path.dirname(params.jsonPath));
|
|
96
|
-
}
|
|
97
|
-
else if (sameFileIdentityForCleanup(pending, entry)) {
|
|
98
|
-
await syncDirectory(path.dirname(params.jsonPath));
|
|
99
|
-
}
|
|
100
|
-
else if (pending.nlink > 1n) {
|
|
101
|
-
throw new FsSafeError("path-mismatch", "queue replacement identity is ambiguous");
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
await fs.unlink(entryPath);
|
|
105
|
-
await syncDirectory(dirPath);
|
|
106
|
-
await removeRetirementRecord({ dirPath, rootPath });
|
|
107
|
-
}
|
|
108
|
-
export async function retireDurableQueueSource(params) {
|
|
109
|
-
await recoverDurableQueueRetirement(params);
|
|
110
|
-
const rootPath = await ensureRetirementRoot(params.jsonPath);
|
|
111
|
-
const { dirPath, entryPath } = retirementPaths(params.jsonPath);
|
|
112
|
-
await fs.mkdir(dirPath, { mode: 0o700 });
|
|
113
|
-
await syncDirectory(rootPath);
|
|
114
|
-
try {
|
|
115
|
-
await fs.rename(params.jsonPath, entryPath);
|
|
116
|
-
}
|
|
117
|
-
catch (error) {
|
|
118
|
-
if (getErrorCode(error) !== "ENOENT")
|
|
119
|
-
throw error;
|
|
120
|
-
await removeRetirementRecord({ dirPath, rootPath });
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
await syncDirectory(dirPath);
|
|
124
|
-
await syncDirectory(path.dirname(params.jsonPath));
|
|
125
|
-
await recoverDurableQueueRetirement(params);
|
|
126
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"json-durable-queue-transfer-lock.d.ts","sourceRoot":"","sources":["../src/json-durable-queue-transfer-lock.ts"],"names":[],"mappings":"AAKA,wBAAsB,qBAAqB,CAAC,CAAC,EAC3C,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACpB,OAAO,CAAC,CAAC,CAAC,CAqBZ"}
|