@hachej/boring-files 0.4.46 → 0.4.48
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/workspace.js +37 -5
- package/package.json +1 -1
package/dist/workspace.js
CHANGED
|
@@ -8,6 +8,7 @@ import { journalConnectionOf, sqliteBatchOf } from './sqlite-batch.js';
|
|
|
8
8
|
import { isTemporary, mediaTypeOf, temporaryFileName } from './file-types.js';
|
|
9
9
|
export { isTemporary } from './file-types.js';
|
|
10
10
|
const sameBytes = (left, right) => left.length === right.length && left.every((byte, index) => byte === right[index]);
|
|
11
|
+
const ASIDE_FOLDER_MAX_AGE_MS = 60 * 60 * 1000;
|
|
11
12
|
const queues = new WeakMap();
|
|
12
13
|
/** One queue per file system object: two providers over the same `fs` share it. */
|
|
13
14
|
function queueFor(fs) {
|
|
@@ -158,16 +159,40 @@ export function createWorkspaceProvider(options) {
|
|
|
158
159
|
return { kind: 'file', bytes, revision: await blobRevision(bytes) };
|
|
159
160
|
}
|
|
160
161
|
/**
|
|
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
|
|
162
|
+
* 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
|
|
163
|
+
* never recursive on the folder itself, so a file landing meanwhile stays. A file system that cannot remove a directory that way (Pi's Node one)
|
|
164
|
+
* has it renamed aside first: no new file can land there, and it is only deleted if it is still empty, otherwise it goes back.
|
|
163
165
|
*/
|
|
166
|
+
const removeAside = async (folder) => {
|
|
167
|
+
const aside = `${folder.slice(0, folder.lastIndexOf('/'))}/${temporaryFileName(randomUUID())}`;
|
|
168
|
+
const moved = await fs.renameFile(folder, aside, fsContext()).catch(() => undefined);
|
|
169
|
+
if (!moved?.ok)
|
|
170
|
+
return false;
|
|
171
|
+
const left = await fs.listDir(aside, fsContext()).catch(() => undefined);
|
|
172
|
+
if (left?.ok && !left.value.length)
|
|
173
|
+
return (await fs.remove(aside, { recursive: true }, fsContext()).catch(() => undefined))?.ok === true;
|
|
174
|
+
if ((await fs.renameFile(aside, folder, fsContext()).catch(() => undefined))?.ok)
|
|
175
|
+
return false;
|
|
176
|
+
// Another writer recreated the folder meanwhile: what landed joins it, entry by entry, and a name already taken there stays aside rather than overwrite.
|
|
177
|
+
const present = await fs.listDir(folder, fsContext()).catch(() => undefined);
|
|
178
|
+
if (!present?.ok || !left?.ok)
|
|
179
|
+
return false;
|
|
180
|
+
const taken = new Set(present.value.map(entry => entry.name));
|
|
181
|
+
for (const entry of left.value)
|
|
182
|
+
if (!taken.has(entry.name))
|
|
183
|
+
await fs.renameFile(entry.path, `${folder}/${entry.name}`, fsContext()).catch(() => undefined);
|
|
184
|
+
const rest = await fs.listDir(aside, fsContext()).catch(() => undefined);
|
|
185
|
+
if (rest?.ok && !rest.value.length)
|
|
186
|
+
await fs.remove(aside, { recursive: true }, fsContext()).catch(() => undefined);
|
|
187
|
+
return false;
|
|
188
|
+
};
|
|
164
189
|
const pruneEmptied = async (path) => {
|
|
165
190
|
for (let folder = path.slice(0, Math.max(0, path.lastIndexOf('/'))); folder; folder = folder.slice(0, Math.max(0, folder.lastIndexOf('/')))) {
|
|
166
191
|
const listed = await fs.listDir(absolute(folder), fsContext()).catch(() => undefined);
|
|
167
192
|
if (!listed?.ok || listed.value.length)
|
|
168
193
|
return;
|
|
169
194
|
const gone = await fs.remove(absolute(folder), { recursive: false }, fsContext()).catch(() => undefined);
|
|
170
|
-
if (!gone?.ok)
|
|
195
|
+
if (!gone?.ok && !await removeAside(absolute(folder)))
|
|
171
196
|
return;
|
|
172
197
|
}
|
|
173
198
|
};
|
|
@@ -180,8 +205,15 @@ export function createWorkspaceProvider(options) {
|
|
|
180
205
|
return;
|
|
181
206
|
for (const entry of listing.value) {
|
|
182
207
|
if (entry.kind === 'directory') {
|
|
183
|
-
if (entry.name
|
|
184
|
-
|
|
208
|
+
if (entry.name === '.git' || entry.name === 'node_modules')
|
|
209
|
+
continue;
|
|
210
|
+
await walk(entry.path);
|
|
211
|
+
// A folder set aside by a process that died: removed only if nothing landed in it. Old enough that no running provider is still pruning it.
|
|
212
|
+
if (isTemporary(entry.path) && entry.mtimeMs < startedAt - ASIDE_FOLDER_MAX_AGE_MS) {
|
|
213
|
+
const left = await fs.listDir(entry.path, fsContext());
|
|
214
|
+
if (left.ok && !left.value.length)
|
|
215
|
+
await fs.remove(entry.path, { recursive: true }, fsContext());
|
|
216
|
+
}
|
|
185
217
|
}
|
|
186
218
|
else if (entry.kind === 'file' && isTemporary(entry.path) && entry.mtimeMs < startedAt)
|
|
187
219
|
await fs.remove(entry.path, { force: true }, fsContext());
|