@hachej/boring-files 0.4.34 → 0.4.36
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/contracts.d.ts +4 -0
- package/dist/publication-input.js +2 -2
- package/dist/sqlite-filesystem.js +1 -1
- package/dist/workspace.js +37 -4
- package/package.json +1 -1
package/dist/contracts.d.ts
CHANGED
|
@@ -189,11 +189,15 @@ export interface ResourceRenameRequest {
|
|
|
189
189
|
readonly to: ResourceLocator;
|
|
190
190
|
readonly expectedRevision: string;
|
|
191
191
|
readonly operationId: string;
|
|
192
|
+
/** Also remove the folders this leaves empty: a folder operation in the tree, never a single-file change, since another consumer may have its cwd there. */
|
|
193
|
+
readonly pruneEmptied?: true;
|
|
192
194
|
}
|
|
193
195
|
export interface ResourceDeleteRequest {
|
|
194
196
|
readonly path: ResourceLocator;
|
|
195
197
|
readonly expectedRevision: string;
|
|
196
198
|
readonly operationId: string;
|
|
199
|
+
/** Also remove the folders this leaves empty: a folder operation in the tree, never a single-file change, since another consumer may have its cwd there. */
|
|
200
|
+
readonly pruneEmptied?: true;
|
|
197
201
|
}
|
|
198
202
|
/** Conditional regular-file mutations; receipts and lookup use the publication journal. */
|
|
199
203
|
export interface ResourceRenamer {
|
|
@@ -99,11 +99,11 @@ export function renameSnapshot(value) {
|
|
|
99
99
|
throw new TypeError('Rename requires one provider and view');
|
|
100
100
|
if (from.resource.path === to.resource.path || from.resource.path.startsWith(`${to.resource.path}/`) || to.resource.path.startsWith(`${from.resource.path}/`))
|
|
101
101
|
throw new TypeError('Rename requires independent file paths');
|
|
102
|
-
return { from, to, expectedRevision: identifier(input.expectedRevision), operationId: identifier(input.operationId) };
|
|
102
|
+
return { from, to, expectedRevision: identifier(input.expectedRevision), operationId: identifier(input.operationId), ...(input.pruneEmptied === true ? { pruneEmptied: true } : {}) };
|
|
103
103
|
}
|
|
104
104
|
export function deleteSnapshot(value) {
|
|
105
105
|
const input = object(value);
|
|
106
|
-
return { path: locator(input.path), expectedRevision: identifier(input.expectedRevision), operationId: identifier(input.operationId) };
|
|
106
|
+
return { path: locator(input.path), expectedRevision: identifier(input.expectedRevision), operationId: identifier(input.operationId), ...(input.pruneEmptied === true ? { pruneEmptied: true } : {}) };
|
|
107
107
|
}
|
|
108
108
|
export function deletePublication(request) {
|
|
109
109
|
return { operationId: request.operationId, atomicity: 'all-or-nothing', changes: [{ kind: 'delete', target: { ...request.path, revision: request.expectedRevision } }] };
|
|
@@ -439,7 +439,7 @@ export function openSqliteFileSystem(options) {
|
|
|
439
439
|
setMaxFileBytes: limit => { assertLimit(limit); maxFileBytes = limit; },
|
|
440
440
|
bytes: path => { const found = row(normalize(path)); return !found ? null : found.kind === 'directory' ? undefined : bytes(normalize(path)); },
|
|
441
441
|
write: (path, content) => { const absolute = normalize(path); makeDirectory(dirname(absolute), true); write(absolute, content); notify(); },
|
|
442
|
-
rename: (from, to) => { rename(normalize(from),
|
|
442
|
+
rename: (from, to) => { const target = normalize(to); makeDirectory(dirname(target), true); rename(normalize(from), target); notify(); },
|
|
443
443
|
remove: path => { remove(normalize(path)); notify(); },
|
|
444
444
|
});
|
|
445
445
|
return fs;
|
package/dist/workspace.js
CHANGED
|
@@ -157,6 +157,20 @@ export function createWorkspaceProvider(options) {
|
|
|
157
157
|
const bytes = Uint8Array.from(result.value);
|
|
158
158
|
return { kind: 'file', bytes, revision: await blobRevision(bytes) };
|
|
159
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* A folder exists through its files: the one a move or delete left empty goes, up to the first that still holds something. Best effort and
|
|
162
|
+
* never recursive, so a file landing meanwhile stays; a file system that cannot remove a directory that way (Pi's Node one) keeps the folder.
|
|
163
|
+
*/
|
|
164
|
+
const pruneEmptied = async (path) => {
|
|
165
|
+
for (let folder = path.slice(0, Math.max(0, path.lastIndexOf('/'))); folder; folder = folder.slice(0, Math.max(0, folder.lastIndexOf('/')))) {
|
|
166
|
+
const listed = await fs.listDir(absolute(folder), fsContext()).catch(() => undefined);
|
|
167
|
+
if (!listed?.ok || listed.value.length)
|
|
168
|
+
return;
|
|
169
|
+
const gone = await fs.remove(absolute(folder), { recursive: false }, fsContext()).catch(() => undefined);
|
|
170
|
+
if (!gone?.ok)
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
};
|
|
160
174
|
/** Once per provider, before its first operation: remove temporary files that an earlier process left behind. */
|
|
161
175
|
let swept;
|
|
162
176
|
const sweep = () => swept ??= (async () => {
|
|
@@ -216,7 +230,7 @@ export function createWorkspaceProvider(options) {
|
|
|
216
230
|
changes: committedChanges(stored.changes, ref),
|
|
217
231
|
};
|
|
218
232
|
}
|
|
219
|
-
const mutate = async (request, access) => {
|
|
233
|
+
const mutate = async (request, access, prune = false) => {
|
|
220
234
|
const moving = 'from' in request ? request : undefined;
|
|
221
235
|
const publication = 'changes' in request ? request : undefined;
|
|
222
236
|
const digest = moving ? await renameDigest(moving) : await publicationDigest(publication);
|
|
@@ -365,11 +379,28 @@ export function createWorkspaceProvider(options) {
|
|
|
365
379
|
applying = true;
|
|
366
380
|
if (!together)
|
|
367
381
|
journal.complete(operation, record, versions);
|
|
382
|
+
if (prune && moving)
|
|
383
|
+
await pruneEmptied(moving.from.resource.path);
|
|
384
|
+
else if (prune)
|
|
385
|
+
for (const plan of plans)
|
|
386
|
+
if (plan.change.kind === 'delete')
|
|
387
|
+
await pruneEmptied(plan.path);
|
|
368
388
|
}
|
|
369
389
|
else if (moving || plans[0].change.kind === 'delete') {
|
|
370
390
|
applying = true;
|
|
391
|
+
// Like a publication, a move creates the folder it lands in: a folder renames file by file, so the first file finds none.
|
|
392
|
+
const landing = moving ? absolute(moving.to.resource.path) : '';
|
|
393
|
+
if (moving) {
|
|
394
|
+
const made = await fs.createDir(landing.slice(0, landing.lastIndexOf('/')) || '/', { recursive: true }, fsContext(access.signal));
|
|
395
|
+
if (!made.ok) {
|
|
396
|
+
if (made.error.code === 'unknown' || made.error.code === 'aborted')
|
|
397
|
+
throw made.error;
|
|
398
|
+
return refuse(made.error.message);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
const leaving = moving ? moving.from.resource.path : plans[0].path;
|
|
371
402
|
const result = moving
|
|
372
|
-
? await fs.renameFile(absolute(moving.from.resource.path),
|
|
403
|
+
? await fs.renameFile(absolute(moving.from.resource.path), landing, fsContext(access.signal))
|
|
373
404
|
: await fs.remove(absolute(plans[0].path), { recursive: false }, fsContext(access.signal));
|
|
374
405
|
if (!result.ok) {
|
|
375
406
|
if (result.error.code === 'unknown' || result.error.code === 'aborted')
|
|
@@ -377,6 +408,8 @@ export function createWorkspaceProvider(options) {
|
|
|
377
408
|
return refuse(result.error.message);
|
|
378
409
|
}
|
|
379
410
|
journal.complete(operation, record, versions);
|
|
411
|
+
if (prune)
|
|
412
|
+
await pruneEmptied(leaving);
|
|
380
413
|
}
|
|
381
414
|
else {
|
|
382
415
|
const { change, path, unchanged } = plans[0];
|
|
@@ -419,8 +452,8 @@ export function createWorkspaceProvider(options) {
|
|
|
419
452
|
});
|
|
420
453
|
};
|
|
421
454
|
const publish = async (input, context) => mutate(publicationSnapshot(input), accessSnapshot(context));
|
|
422
|
-
const rename = async (input, context) =>
|
|
423
|
-
const remove = async (input, context) => mutate(deletePublication(
|
|
455
|
+
const rename = async (input, context) => { const request = renameSnapshot(input); return mutate(request, accessSnapshot(context), request.pruneEmptied === true); };
|
|
456
|
+
const remove = async (input, context) => { const request = deleteSnapshot(input); return mutate(deletePublication(request), accessSnapshot(context), request.pruneEmptied === true); };
|
|
424
457
|
const lookup = async (operationId, context) => {
|
|
425
458
|
const id = identifier(operationId);
|
|
426
459
|
const access = accessSnapshot(context);
|