@rljson/fs-agent 0.0.23 → 0.0.24
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 +41 -1
- package/dist/fs-agent.js +39 -2
- package/package.json +1 -1
package/dist/fs-agent.d.ts
CHANGED
|
@@ -131,6 +131,28 @@ 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
|
+
* A prune smaller than this many files is always allowed through.
|
|
136
|
+
*
|
|
137
|
+
* Below it, "most of the folder" is not a meaningful statement: emptying a
|
|
138
|
+
* three-file folder is an ordinary edit, and a guard that blocked it would
|
|
139
|
+
* fire constantly on small trees and be turned off.
|
|
140
|
+
*/
|
|
141
|
+
export declare const MASS_DELETE_MIN_FILES = 100;
|
|
142
|
+
/**
|
|
143
|
+
* Above this share of the folder, a prune is treated as suspicious rather than
|
|
144
|
+
* intentional.
|
|
145
|
+
*/
|
|
146
|
+
export declare const MASS_DELETE_MAX_RATIO = 0.3;
|
|
147
|
+
/**
|
|
148
|
+
* A restore that could not put the folder into the state the tree describes.
|
|
149
|
+
*
|
|
150
|
+
* The distinction that matters to callers is not why. It is that the folder
|
|
151
|
+
* does NOT match the tree afterwards, so the ref must not be recorded as
|
|
152
|
+
* applied and the resulting state must not be advertised to peers.
|
|
153
|
+
*/
|
|
154
|
+
export declare class RestoreIncompleteError extends Error {
|
|
155
|
+
}
|
|
134
156
|
/**
|
|
135
157
|
* Thrown when a restore wrote everything it could but at least one file was
|
|
136
158
|
* held open by another process.
|
|
@@ -142,10 +164,28 @@ export declare const ATOMIC_TMP_PREFIX = ".fsagent-tmp-";
|
|
|
142
164
|
* ref as applied — would make one locked file look like an edit that everyone
|
|
143
165
|
* else must adopt.
|
|
144
166
|
*/
|
|
145
|
-
export declare class PartialRestoreError extends
|
|
167
|
+
export declare class PartialRestoreError extends RestoreIncompleteError {
|
|
146
168
|
readonly lockedPaths: string[];
|
|
147
169
|
constructor(lockedPaths: string[]);
|
|
148
170
|
}
|
|
171
|
+
/**
|
|
172
|
+
* Thrown when `cleanTarget` would have deleted most of the folder.
|
|
173
|
+
*
|
|
174
|
+
* The dangerous direction of sync is a POPULATED node receiving a tree that
|
|
175
|
+
* lacks its files: a peer that comes up empty — a fresh clone, a folder not
|
|
176
|
+
* yet mounted, a bootstrap that raced its own first scan — advertises an empty
|
|
177
|
+
* tree, and every other node faithfully deletes everything it has.
|
|
178
|
+
*
|
|
179
|
+
* Nothing downstream can tell that apart from a genuine bulk deletion, so the
|
|
180
|
+
* judgement has to be made here, and it is deliberately biased: refusing a
|
|
181
|
+
* real mass delete costs one manual step, applying a false one costs the data.
|
|
182
|
+
*/
|
|
183
|
+
export declare class MassDeleteRefusedError extends RestoreIncompleteError {
|
|
184
|
+
readonly wouldPrune: number;
|
|
185
|
+
readonly totalFiles: number;
|
|
186
|
+
readonly incomingFiles: number;
|
|
187
|
+
constructor(wouldPrune: number, totalFiles: number, incomingFiles: number);
|
|
188
|
+
}
|
|
149
189
|
export declare class FsAgent {
|
|
150
190
|
private _scanner;
|
|
151
191
|
private _adapter;
|
package/dist/fs-agent.js
CHANGED
|
@@ -1022,7 +1022,11 @@ 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
|
-
|
|
1025
|
+
const MASS_DELETE_MIN_FILES = 100;
|
|
1026
|
+
const MASS_DELETE_MAX_RATIO = 0.3;
|
|
1027
|
+
class RestoreIncompleteError extends Error {
|
|
1028
|
+
}
|
|
1029
|
+
class PartialRestoreError extends RestoreIncompleteError {
|
|
1026
1030
|
constructor(lockedPaths) {
|
|
1027
1031
|
super(
|
|
1028
1032
|
`restore could not write ${lockedPaths.length} locked file${lockedPaths.length === 1 ? "" : "s"}: ${lockedPaths.join(", ")}`
|
|
@@ -1031,6 +1035,19 @@ class PartialRestoreError extends Error {
|
|
|
1031
1035
|
this.name = "PartialRestoreError";
|
|
1032
1036
|
}
|
|
1033
1037
|
}
|
|
1038
|
+
class MassDeleteRefusedError extends RestoreIncompleteError {
|
|
1039
|
+
constructor(wouldPrune, totalFiles, incomingFiles) {
|
|
1040
|
+
super(
|
|
1041
|
+
// No pluralisation: the guard only fires above MASS_DELETE_MIN_FILES,
|
|
1042
|
+
// so this is never one file.
|
|
1043
|
+
`refusing to prune ${wouldPrune} of ${totalFiles} local files: the incoming tree has ${incomingFiles === 0 ? "NO files at all" : `only ${incomingFiles}`}, which looks like a peer that came up empty rather than a deletion. Nothing was deleted.`
|
|
1044
|
+
);
|
|
1045
|
+
this.wouldPrune = wouldPrune;
|
|
1046
|
+
this.totalFiles = totalFiles;
|
|
1047
|
+
this.incomingFiles = incomingFiles;
|
|
1048
|
+
this.name = "MassDeleteRefusedError";
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1034
1051
|
class FsAgent {
|
|
1035
1052
|
_scanner;
|
|
1036
1053
|
_adapter;
|
|
@@ -1347,6 +1364,26 @@ ${err.stack}` : String(err);
|
|
|
1347
1364
|
);
|
|
1348
1365
|
}
|
|
1349
1366
|
if (options?.cleanTarget) {
|
|
1367
|
+
let wouldPrune = 0;
|
|
1368
|
+
for (const existing of preRestore) {
|
|
1369
|
+
if (!expectedFiles.has(existing)) wouldPrune++;
|
|
1370
|
+
}
|
|
1371
|
+
if (wouldPrune > MASS_DELETE_MIN_FILES && (expectedFiles.size === 0 || wouldPrune / preRestore.size > MASS_DELETE_MAX_RATIO)) {
|
|
1372
|
+
console.error(
|
|
1373
|
+
`[FsAgent] MASS DELETE REFUSED on ${target}: the incoming tree would remove ${wouldPrune} of ${preRestore.size} files (incoming tree has ${expectedFiles.size}). Nothing was deleted. If this deletion is real, it has to be applied deliberately.`
|
|
1374
|
+
);
|
|
1375
|
+
this._writeSyncError(
|
|
1376
|
+
"restore/massDeleteGuard",
|
|
1377
|
+
new Error(
|
|
1378
|
+
`refused to prune ${wouldPrune}/${preRestore.size} files; incoming tree had ${expectedFiles.size}`
|
|
1379
|
+
)
|
|
1380
|
+
);
|
|
1381
|
+
throw new MassDeleteRefusedError(
|
|
1382
|
+
wouldPrune,
|
|
1383
|
+
preRestore.size,
|
|
1384
|
+
expectedFiles.size
|
|
1385
|
+
);
|
|
1386
|
+
}
|
|
1350
1387
|
await this._pruneExtraneous(
|
|
1351
1388
|
target,
|
|
1352
1389
|
expectedDirs,
|
|
@@ -2156,7 +2193,7 @@ ${err.stack}` : String(err);
|
|
|
2156
2193
|
this._currentRef = postRestoreRef;
|
|
2157
2194
|
return;
|
|
2158
2195
|
} catch (err) {
|
|
2159
|
-
if (err instanceof
|
|
2196
|
+
if (err instanceof RestoreIncompleteError) {
|
|
2160
2197
|
try {
|
|
2161
2198
|
const halfApplied = await this._scanner.scan();
|
|
2162
2199
|
this._lastSentContentKey = this._contentKeyFromTree(halfApplied);
|