@rljson/fs-agent 0.0.22 → 0.0.23
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/dist/fs-agent.d.ts +28 -0
- package/dist/fs-agent.js +56 -12
- package/package.json +1 -1
package/dist/fs-agent.d.ts
CHANGED
|
@@ -131,6 +131,21 @@ export declare const ATOMIC_TMP_PREFIX = ".fsagent-tmp-";
|
|
|
131
131
|
/**
|
|
132
132
|
* Orchestrates filesystem operations with tree structures and blob storage
|
|
133
133
|
*/
|
|
134
|
+
/**
|
|
135
|
+
* Thrown when a restore wrote everything it could but at least one file was
|
|
136
|
+
* held open by another process.
|
|
137
|
+
*
|
|
138
|
+
* Not a failure of the restore so much as a "not yet": the bytes are still
|
|
139
|
+
* available, the file is simply busy. It is an error rather than a silent
|
|
140
|
+
* partial success because the folder does NOT match the tree afterwards, and
|
|
141
|
+
* anything that treats it as if it did — advertising the state, recording the
|
|
142
|
+
* ref as applied — would make one locked file look like an edit that everyone
|
|
143
|
+
* else must adopt.
|
|
144
|
+
*/
|
|
145
|
+
export declare class PartialRestoreError extends Error {
|
|
146
|
+
readonly lockedPaths: string[];
|
|
147
|
+
constructor(lockedPaths: string[]);
|
|
148
|
+
}
|
|
134
149
|
export declare class FsAgent {
|
|
135
150
|
private _scanner;
|
|
136
151
|
private _adapter;
|
|
@@ -162,6 +177,8 @@ export declare class FsAgent {
|
|
|
162
177
|
/** Files written vs left alone by the current {@link restore}. */
|
|
163
178
|
private _restoreWritten;
|
|
164
179
|
private _restoreSkipped;
|
|
180
|
+
/** Paths the current {@link restore} could not write because they were held open. */
|
|
181
|
+
private _restoreLocked;
|
|
165
182
|
/**
|
|
166
183
|
* What this agent last wrote to each absolute path, so a repeat restore can
|
|
167
184
|
* recognise its own work without re-reading the file.
|
|
@@ -304,6 +321,17 @@ export declare class FsAgent {
|
|
|
304
321
|
* is the only case where the scanner's view describes these files
|
|
305
322
|
*/
|
|
306
323
|
private _restoreTree;
|
|
324
|
+
/**
|
|
325
|
+
* Whether a caught value means "another process is holding this file".
|
|
326
|
+
*
|
|
327
|
+
* Windows reports a locked file as EPERM or EBUSY; EACCES covers the
|
|
328
|
+
* permission-denied shape. Deliberately narrow — anything else is a real
|
|
329
|
+
* write failure and must still abort, because a restore that shrugged off
|
|
330
|
+
* every error would report success while leaving the folder wrong.
|
|
331
|
+
* @param err - The caught value.
|
|
332
|
+
* @returns `true` for a lock-shaped error.
|
|
333
|
+
*/
|
|
334
|
+
private static _isLocked;
|
|
307
335
|
/**
|
|
308
336
|
* The content identity this agent believes is on disk at `filePath`, or
|
|
309
337
|
* `undefined` when it has no basis for an opinion.
|
package/dist/fs-agent.js
CHANGED
|
@@ -1022,6 +1022,15 @@ const DEFAULT_TIMEOUTS = {
|
|
|
1022
1022
|
const DISCONNECT_PAUSE_MAX_MS = 3e4;
|
|
1023
1023
|
const SYNC_ERROR_FILE = ".sync-errors.log";
|
|
1024
1024
|
const ATOMIC_TMP_PREFIX = ".fsagent-tmp-";
|
|
1025
|
+
class PartialRestoreError extends Error {
|
|
1026
|
+
constructor(lockedPaths) {
|
|
1027
|
+
super(
|
|
1028
|
+
`restore could not write ${lockedPaths.length} locked file${lockedPaths.length === 1 ? "" : "s"}: ${lockedPaths.join(", ")}`
|
|
1029
|
+
);
|
|
1030
|
+
this.lockedPaths = lockedPaths;
|
|
1031
|
+
this.name = "PartialRestoreError";
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1025
1034
|
class FsAgent {
|
|
1026
1035
|
_scanner;
|
|
1027
1036
|
_adapter;
|
|
@@ -1053,6 +1062,8 @@ class FsAgent {
|
|
|
1053
1062
|
/** Files written vs left alone by the current {@link restore}. */
|
|
1054
1063
|
_restoreWritten = 0;
|
|
1055
1064
|
_restoreSkipped = 0;
|
|
1065
|
+
/** Paths the current {@link restore} could not write because they were held open. */
|
|
1066
|
+
_restoreLocked = [];
|
|
1056
1067
|
/**
|
|
1057
1068
|
* What this agent last wrote to each absolute path, so a repeat restore can
|
|
1058
1069
|
* recognise its own work without re-reading the file.
|
|
@@ -1323,6 +1334,7 @@ ${err.stack}` : String(err);
|
|
|
1323
1334
|
const preRestore = options?.cleanTarget ? await this._collectAllFiles(target) : /* @__PURE__ */ new Set();
|
|
1324
1335
|
this._restoreWritten = 0;
|
|
1325
1336
|
this._restoreSkipped = 0;
|
|
1337
|
+
this._restoreLocked = [];
|
|
1326
1338
|
await this._restoreTree(
|
|
1327
1339
|
tree.rootHash,
|
|
1328
1340
|
tree.trees,
|
|
@@ -1342,6 +1354,9 @@ ${err.stack}` : String(err);
|
|
|
1342
1354
|
preRestore
|
|
1343
1355
|
);
|
|
1344
1356
|
}
|
|
1357
|
+
if (this._restoreLocked.length > 0) {
|
|
1358
|
+
throw new PartialRestoreError([...this._restoreLocked]);
|
|
1359
|
+
}
|
|
1345
1360
|
}
|
|
1346
1361
|
/**
|
|
1347
1362
|
* Recursively collects the absolute paths of all files under `currentDir`.
|
|
@@ -1406,18 +1421,26 @@ ${err.stack}` : String(err);
|
|
|
1406
1421
|
);
|
|
1407
1422
|
}
|
|
1408
1423
|
await mkdir(dirname(filePath), { recursive: true });
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1424
|
+
try {
|
|
1425
|
+
await FsAgent._atomicWriteFile(filePath, fileBlob.content);
|
|
1426
|
+
this._restoreWritten++;
|
|
1427
|
+
if (meta.mtime) {
|
|
1428
|
+
const mtime = new Date(meta.mtime);
|
|
1429
|
+
await utimes(filePath, mtime, mtime);
|
|
1430
|
+
}
|
|
1431
|
+
if (meta.size !== void 0 && meta.mtime !== void 0) {
|
|
1432
|
+
this._restoredBlobs.set(filePath, {
|
|
1433
|
+
blobId: meta.blobId,
|
|
1434
|
+
size: meta.size,
|
|
1435
|
+
mtime: meta.mtime
|
|
1436
|
+
});
|
|
1437
|
+
}
|
|
1438
|
+
} catch (error) {
|
|
1439
|
+
if (!FsAgent._isLocked(error)) throw error;
|
|
1440
|
+
console.warn(
|
|
1441
|
+
`[FsAgent] restore: "${meta.relativePath}" is held open by another process (${error.code}) — skipped, will retry`
|
|
1442
|
+
);
|
|
1443
|
+
this._restoreLocked.push(meta.relativePath);
|
|
1421
1444
|
}
|
|
1422
1445
|
}
|
|
1423
1446
|
} else if (meta.type === "directory") {
|
|
@@ -1430,6 +1453,20 @@ ${err.stack}` : String(err);
|
|
|
1430
1453
|
}
|
|
1431
1454
|
}
|
|
1432
1455
|
}
|
|
1456
|
+
/**
|
|
1457
|
+
* Whether a caught value means "another process is holding this file".
|
|
1458
|
+
*
|
|
1459
|
+
* Windows reports a locked file as EPERM or EBUSY; EACCES covers the
|
|
1460
|
+
* permission-denied shape. Deliberately narrow — anything else is a real
|
|
1461
|
+
* write failure and must still abort, because a restore that shrugged off
|
|
1462
|
+
* every error would report success while leaving the folder wrong.
|
|
1463
|
+
* @param err - The caught value.
|
|
1464
|
+
* @returns `true` for a lock-shaped error.
|
|
1465
|
+
*/
|
|
1466
|
+
static _isLocked(err) {
|
|
1467
|
+
const code = err?.code;
|
|
1468
|
+
return code === "EPERM" || code === "EBUSY" || code === "EACCES";
|
|
1469
|
+
}
|
|
1433
1470
|
/**
|
|
1434
1471
|
* The content identity this agent believes is on disk at `filePath`, or
|
|
1435
1472
|
* `undefined` when it has no basis for an opinion.
|
|
@@ -2119,6 +2156,13 @@ ${err.stack}` : String(err);
|
|
|
2119
2156
|
this._currentRef = postRestoreRef;
|
|
2120
2157
|
return;
|
|
2121
2158
|
} catch (err) {
|
|
2159
|
+
if (err instanceof PartialRestoreError) {
|
|
2160
|
+
try {
|
|
2161
|
+
const halfApplied = await this._scanner.scan();
|
|
2162
|
+
this._lastSentContentKey = this._contentKeyFromTree(halfApplied);
|
|
2163
|
+
} catch {
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2122
2166
|
if (attempt === maxAttempts) {
|
|
2123
2167
|
if (recoveryAttempt >= this._timeouts.recoveryRetries) {
|
|
2124
2168
|
console.error(
|