mikser-io 9.84.0 → 9.88.0
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/docs/diagnostics.md +9 -1
- package/docs/plugins.md +47 -2
- package/package.json +1 -1
- package/src/instance.js +30 -0
- package/src/plugins/assets.js +89 -26
- package/src/plugins/files.js +57 -4
- package/src/source.js +14 -1
package/docs/diagnostics.md
CHANGED
|
@@ -767,7 +767,7 @@ answer is about your request. It is also faster, because a forwarded command
|
|
|
767
767
|
never imports the config or the plugin graph, which is most of what a one-shot
|
|
768
768
|
spends its time on.
|
|
769
769
|
|
|
770
|
-
|
|
770
|
+
Four things it refuses or reports rather than guessing:
|
|
771
771
|
|
|
772
772
|
- **A different config.** If you resolve `mikser.config.prod.js` and the
|
|
773
773
|
instance is running `mikser.config.js`, it refuses. Building with the wrong
|
|
@@ -779,6 +779,14 @@ Three things it refuses or reports rather than guessing:
|
|
|
779
779
|
to *become* the instance, which is not something a running one can do for
|
|
780
780
|
you — it would have to open a port in your process. They exit 1 and say so,
|
|
781
781
|
rather than building and leaving nothing on the port.
|
|
782
|
+
- **`--clear`.** Clearing removes the output folder and closes, unlinks and
|
|
783
|
+
reopens the cache database — a boot operation, which is why the wipe lives
|
|
784
|
+
where the database is opened and nowhere else. A running instance holds that
|
|
785
|
+
handle, has its manifest and plugin state loaded from what would be deleted,
|
|
786
|
+
and may be serving out of the folder being removed. It exits 1 and tells you
|
|
787
|
+
to stop the instance. It used to be accepted and ignored, so the build
|
|
788
|
+
reported success with nothing cleared; a half-clear would have been worse
|
|
789
|
+
than either, since afterwards nobody could say what state the folder was in.
|
|
782
790
|
There is no opt-out. A flag for running a second engine on a held folder
|
|
783
791
|
only ever enabled the accident this surface prevents, and stopping the
|
|
784
792
|
instance serves every case it was reached for.
|
package/docs/plugins.md
CHANGED
|
@@ -198,15 +198,40 @@ files({
|
|
|
198
198
|
```
|
|
199
199
|
|
|
200
200
|
**Entity properties set:**
|
|
201
|
-
- `id`: `/
|
|
201
|
+
- `id`: `/files/{relativePath}` — never carries the `outputFolder` prefix
|
|
202
202
|
- `collection`: `'files'`
|
|
203
203
|
- `type`: `'file'`
|
|
204
204
|
- `format`: File extension
|
|
205
|
-
- `
|
|
205
|
+
- `uri`: the **source** file, the same meaning it has in every other
|
|
206
|
+
collection. It named the published symlink until 9.86.0, which made `uri`
|
|
207
|
+
mean the source for a document and the destination for a file — see below
|
|
208
|
+
- `source`: the same path; kept because presets read it
|
|
209
|
+
- `name` / `meta.url`: where the file is **published**, carrying the
|
|
210
|
+
`outputFolder` prefix when one is configured
|
|
206
211
|
- `checksum`: MD5 checksum
|
|
212
|
+
- `link`: the resolved target, when the source is itself a symlink
|
|
207
213
|
|
|
208
214
|
**Watch support:** Yes.
|
|
209
215
|
|
|
216
|
+
**Deleted files are reconciled by the scan**, not only by the watcher. A file
|
|
217
|
+
removed while nothing was watching used to leave a dangling symlink in the
|
|
218
|
+
deployed output, its catalog row, and anything derived from it — on every
|
|
219
|
+
later build. Neither a plain rebuild nor `--force` removed them; only
|
|
220
|
+
`--clear` did.
|
|
221
|
+
|
|
222
|
+
The scan now compares what it found against what the catalog holds and removes
|
|
223
|
+
the difference, scoped by `uri` to the files folder so entities another plugin
|
|
224
|
+
emitted into this collection (a CSV, a drive, an API) are left alone. It says
|
|
225
|
+
`Files removed: N no longer on disk` when it acts and nothing when it does not.
|
|
226
|
+
|
|
227
|
+
`--force` reconciles deletions too, as of 9.88.0. It used to skip this and the
|
|
228
|
+
equivalent sweep for documents and layouts, which made it the one flag that
|
|
229
|
+
could not fix what it is reached for — nothing about `--force` wipes the
|
|
230
|
+
catalog (that is `--clear`), so a row for a file that no longer exists simply
|
|
231
|
+
survived. Only `--clear` cleaned up.
|
|
232
|
+
|
|
233
|
+
The matching cleanup for derivatives lives in `assets` — see below.
|
|
234
|
+
|
|
210
235
|
---
|
|
211
236
|
|
|
212
237
|
### `layouts` *(sibling: `mikser-io-layouts`)*
|
|
@@ -419,6 +444,26 @@ The assets plugin processes whatever's on disk. Source files don't have to start
|
|
|
419
444
|
|
|
420
445
|
Yes — the plugin watches both the source files and the preset modules. Editing a preset re-processes every input that matches it; editing a source re-processes just that input.
|
|
421
446
|
|
|
447
|
+
|
|
448
|
+
**Derivatives with no source are removed.** A derivative outlived its source:
|
|
449
|
+
deleting a file removed its catalog row and its published copy and left the
|
|
450
|
+
derived file, because the delete handler dropped the in-memory mapping and
|
|
451
|
+
nothing on disk. Narrowing a preset's `match` left one the same way. A stale
|
|
452
|
+
derivative passes every check — the url resolves and the bytes are there —
|
|
453
|
+
so the only cleanup was `--clear`, or deleting it by hand.
|
|
454
|
+
|
|
455
|
+
The post-cycle pass that already walks the revision markers now also asks, per
|
|
456
|
+
derivative, whether it still has a source that this preset still covers. It is
|
|
457
|
+
answered from the **catalog**, not from the delete event: a delete entry is
|
|
458
|
+
sparse (`{ id, type, collection }`) in every source plugin, so it cannot say
|
|
459
|
+
where the derivative went, while the catalog answers whatever route the orphan
|
|
460
|
+
arrived by — including ones that predate the fix. Reported as
|
|
461
|
+
`Assets removed: N derivative(s) with no source`.
|
|
462
|
+
|
|
463
|
+
It does nothing when the catalog is empty. Every check concludes "no source,
|
|
464
|
+
therefore orphan", and an empty catalog answers that for every derivative on
|
|
465
|
+
the site, so a failed import would otherwise delete the whole assets folder.
|
|
466
|
+
|
|
422
467
|
---
|
|
423
468
|
|
|
424
469
|
### `resources`
|
package/package.json
CHANGED
package/src/instance.js
CHANGED
|
@@ -330,6 +330,35 @@ function refuseUnknownFlags(socket, request) {
|
|
|
330
330
|
return true
|
|
331
331
|
}
|
|
332
332
|
|
|
333
|
+
// `--clear` against a running instance.
|
|
334
|
+
//
|
|
335
|
+
// Clearing is a BOOT operation. It removes the output folder, and it closes,
|
|
336
|
+
// unlinks and reopens the cache database — which is why the wipe lives at the
|
|
337
|
+
// point the database is opened and nowhere else. An instance has that handle
|
|
338
|
+
// open with prepared statements held against it all over the engine, its
|
|
339
|
+
// manifest and plugin state loaded from what is about to be deleted, and a
|
|
340
|
+
// server answering requests out of the folder being removed. There is no
|
|
341
|
+
// moment mid-run where this can be done and still mean what the flag means.
|
|
342
|
+
//
|
|
343
|
+
// It travelled on the request and was never read, so `mikser --clear` against
|
|
344
|
+
// a watcher rebuilt normally and exited 0 having cleared nothing: the flag
|
|
345
|
+
// looked honoured. Refusing says the only thing that helps, which is that the
|
|
346
|
+
// instance has to stop first.
|
|
347
|
+
//
|
|
348
|
+
// A half-clear would be worse than either: it would report success for an
|
|
349
|
+
// operation the caller can no longer describe.
|
|
350
|
+
function refuseClear(socket, request) {
|
|
351
|
+
if (!request.clear) return false
|
|
352
|
+
frame(socket, {
|
|
353
|
+
type: 'refused',
|
|
354
|
+
reason: 'a mikser instance is running in this folder, and --clear cannot run while it does.',
|
|
355
|
+
detail: 'Clearing removes the output folder and reopens the cache database, both of which happen '
|
|
356
|
+
+ 'once at startup — the running instance holds that database open and serves out of that '
|
|
357
|
+
+ 'folder. Stop it and run this again.',
|
|
358
|
+
})
|
|
359
|
+
return true
|
|
360
|
+
}
|
|
361
|
+
|
|
333
362
|
function refuseStale(socket, movedFile) {
|
|
334
363
|
frame(socket, {
|
|
335
364
|
type: 'refused',
|
|
@@ -451,6 +480,7 @@ export function serveInstance() {
|
|
|
451
480
|
// original incident, and it is wrong whether or not it
|
|
452
481
|
// writes anything.
|
|
453
482
|
if (refuseUnknownFlags(socket, request)) return
|
|
483
|
+
if (refuseClear(socket, request)) return
|
|
454
484
|
const wrongConfig = configMismatch(request.config)
|
|
455
485
|
if (wrongConfig) return refuseConfig(socket, request, wrongConfig)
|
|
456
486
|
const movedFile = await configStale()
|
package/src/plugins/assets.js
CHANGED
|
@@ -141,8 +141,32 @@ export function assets(options = {}) {
|
|
|
141
141
|
// entity, and it is the hottest call in a render. Nothing is looked up
|
|
142
142
|
// when the url is built. This runs at most a handful of times, after
|
|
143
143
|
// everything has settled, and only when something is already wrong.
|
|
144
|
+
// A derivative wears the PRESET's extension, so a source is found by stem:
|
|
145
|
+
// `media/hero.webp` came from `media/hero.jpg`.
|
|
146
|
+
const stemOf = (value) => value.slice(0, value.length - path.extname(value).length)
|
|
147
|
+
|
|
148
|
+
// stem(entity.name) -> entities, built once per cycle.
|
|
149
|
+
//
|
|
150
|
+
// Two callers ask the same question from opposite directions — "why is
|
|
151
|
+
// this derivative missing" and "does this derivative still have a source"
|
|
152
|
+
// — so they share one index rather than each walking the catalog.
|
|
144
153
|
let sourceIndex = null
|
|
145
154
|
let sourceIndexCycle
|
|
155
|
+
async function sourceByStem() {
|
|
156
|
+
const cycle = runtime.state?.cycle?.id ?? null
|
|
157
|
+
if (sourceIndexCycle !== cycle) { sourceIndex = null; sourceIndexCycle = cycle }
|
|
158
|
+
if (!sourceIndex) {
|
|
159
|
+
sourceIndex = new Map()
|
|
160
|
+
for await (const candidate of iterateEntities({ collection: { $ne: collection } })) {
|
|
161
|
+
if (typeof candidate.name !== 'string') continue
|
|
162
|
+
const key = stemOf(candidate.name)
|
|
163
|
+
if (!sourceIndex.has(key)) sourceIndex.set(key, [])
|
|
164
|
+
sourceIndex.get(key).push(candidate)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return sourceIndex
|
|
168
|
+
}
|
|
169
|
+
|
|
146
170
|
async function explainMissing(destination) {
|
|
147
171
|
const assetsName = runtime.options.assets
|
|
148
172
|
const parts = String(destination).replace(/^\/+/, '').split('/')
|
|
@@ -155,27 +179,12 @@ export function assets(options = {}) {
|
|
|
155
179
|
return `no preset named '${preset}' is configured (configured: ${configured.join(', ') || 'none'})`
|
|
156
180
|
}
|
|
157
181
|
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
// Built once per cycle and only on this path: a build with nothing
|
|
163
|
-
// broken never walks the catalog for it.
|
|
164
|
-
const cycle = runtime.state?.cycle?.id ?? null
|
|
165
|
-
if (sourceIndexCycle !== cycle) { sourceIndex = null; sourceIndexCycle = cycle }
|
|
166
|
-
if (!sourceIndex) {
|
|
167
|
-
sourceIndex = new Map()
|
|
168
|
-
for await (const candidate of iterateEntities({ collection: { $ne: collection } })) {
|
|
169
|
-
if (typeof candidate.name !== 'string') continue
|
|
170
|
-
const key = stem(candidate.name)
|
|
171
|
-
if (!sourceIndex.has(key)) sourceIndex.set(key, [])
|
|
172
|
-
sourceIndex.get(key).push(candidate)
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const source = (sourceIndex.get(stem(name)) ?? [])[0]
|
|
182
|
+
// Built only on this path: a build with nothing broken never walks
|
|
183
|
+
// the catalog for it.
|
|
184
|
+
const index = await sourceByStem()
|
|
185
|
+
const source = (index.get(stemOf(name)) ?? [])[0]
|
|
177
186
|
if (!source) {
|
|
178
|
-
return `no source file is named '${
|
|
187
|
+
return `no source file is named '${stemOf(name)}' — nothing would produce this derivative `
|
|
179
188
|
+ 'under any preset'
|
|
180
189
|
}
|
|
181
190
|
|
|
@@ -726,25 +735,79 @@ export function assets(options = {}) {
|
|
|
726
735
|
} catch { /* a count is not worth failing a build over */ }
|
|
727
736
|
|
|
728
737
|
let revisions = await globby('**/*.md5', { cwd: runtime.options.assetsFolder })
|
|
738
|
+
|
|
739
|
+
// Is the catalog answerable at all?
|
|
740
|
+
//
|
|
741
|
+
// Every check below concludes "no source, therefore orphan", and an
|
|
742
|
+
// EMPTY catalog answers that for every derivative on the site. A
|
|
743
|
+
// failed import or a scan that has not run yet would then delete the
|
|
744
|
+
// whole assets folder — a rebuild of every derivative at best, and at
|
|
745
|
+
// worst it happens on the machine that serves them. So the sweep only
|
|
746
|
+
// runs when there is something to be absent from.
|
|
747
|
+
const catalogSize = countEntities({ collection: { $ne: collection } })
|
|
748
|
+
const orphaned = []
|
|
749
|
+
|
|
729
750
|
for (let revision of revisions) {
|
|
730
751
|
const [preset] = revision.split(path.sep)
|
|
731
752
|
const [assetsRevision] = revision.split('.').slice(-2, -1)
|
|
732
753
|
|
|
733
754
|
if (!presets[preset]) {
|
|
734
755
|
const assetsPresetFolder = path.join(runtime.options.assetsFolder, preset)
|
|
735
|
-
const
|
|
756
|
+
// `let`. This was `const` with an assignment inside the try,
|
|
757
|
+
// so removing a preset folder threw TypeError into the empty
|
|
758
|
+
// catch on every pass and the log line was unreachable — the
|
|
759
|
+
// rm had already happened, so nothing looked wrong.
|
|
760
|
+
let assetsPresetRemoved = false
|
|
736
761
|
try {
|
|
737
762
|
await rm(assetsPresetFolder, { recursive: true, force: true })
|
|
738
763
|
assetsPresetRemoved = true
|
|
739
|
-
} catch { }
|
|
764
|
+
} catch { /* already gone, or not ours to remove */ }
|
|
740
765
|
if (assetsPresetRemoved) {
|
|
741
766
|
logger.debug('Assets preset removed: %s', assetsPresetFolder)
|
|
742
767
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
768
|
+
continue
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
if (Number.parseInt(assetsRevision) < presets[preset].checksum) {
|
|
772
|
+
await unlink(path.join(runtime.options.assetsFolder, revision))
|
|
747
773
|
}
|
|
774
|
+
|
|
775
|
+
// A derivative whose source is gone, or which this preset no
|
|
776
|
+
// longer covers.
|
|
777
|
+
//
|
|
778
|
+
// Deleting the source removed its catalog row and its published
|
|
779
|
+
// file, and left the derivative — the delete handler dropped the
|
|
780
|
+
// in-memory mapping and nothing on disk, so the only cleanup was
|
|
781
|
+
// --clear or doing it by hand. Narrowing a preset's `match` left
|
|
782
|
+
// one the same way, with nothing said.
|
|
783
|
+
//
|
|
784
|
+
// Answered from the catalog rather than from the delete event: a
|
|
785
|
+
// delete entry is sparse ({ id, type, collection }) by convention
|
|
786
|
+
// in every source plugin, so it cannot say where the derivative
|
|
787
|
+
// went. What still HAS a source is a question the catalog answers
|
|
788
|
+
// whatever route the orphan arrived by.
|
|
789
|
+
if (!catalogSize) continue
|
|
790
|
+
const derivative = revision.replace(/\.[^.]+\.md5$/, '')
|
|
791
|
+
const name = derivative.split(path.sep).slice(1).join('/')
|
|
792
|
+
const index = await sourceByStem()
|
|
793
|
+
const source = (index.get(stemOf(name)) ?? [])[0]
|
|
794
|
+
if (source && presetsSelecting(source).includes(preset)) continue
|
|
795
|
+
orphaned.push({
|
|
796
|
+
derivative,
|
|
797
|
+
marker: revision,
|
|
798
|
+
reason: source ? `preset '${preset}' no longer covers ${source.id}` : 'its source is gone',
|
|
799
|
+
})
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
for (const { derivative, marker } of orphaned) {
|
|
803
|
+
await unlink(path.join(runtime.options.assetsFolder, derivative)).catch(() => { /* already gone */ })
|
|
804
|
+
await unlink(path.join(runtime.options.assetsFolder, marker)).catch(() => { /* already gone */ })
|
|
805
|
+
}
|
|
806
|
+
if (orphaned.length) {
|
|
807
|
+
logger.info('Assets removed: %d derivative(s) with no source — %s',
|
|
808
|
+
orphaned.length,
|
|
809
|
+
orphaned.slice(0, 3).map(o => `${o.derivative} (${o.reason})`).join(', ')
|
|
810
|
+
+ (orphaned.length > 3 ? ` and ${orphaned.length - 3} more` : ''))
|
|
748
811
|
}
|
|
749
812
|
})
|
|
750
813
|
|
package/src/plugins/files.js
CHANGED
|
@@ -3,6 +3,7 @@ import { mkdir, symlink, unlink, lstat, realpath } from 'fs/promises'
|
|
|
3
3
|
import { globby } from 'globby'
|
|
4
4
|
import pMap from 'p-map'
|
|
5
5
|
import { checksumsByCollection } from '../catalog.js'
|
|
6
|
+
import { sweepDeleted } from '../source.js'
|
|
6
7
|
|
|
7
8
|
export function files(options = {}) {
|
|
8
9
|
return ({
|
|
@@ -38,10 +39,18 @@ export function files(options = {}) {
|
|
|
38
39
|
return { uri, source }
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
// The goal state is "the link is not there", so a link that is
|
|
43
|
+
// already gone is success, not an error. It genuinely happens: someone
|
|
44
|
+
// deletes a stale one by hand, and before this the next reconciliation
|
|
45
|
+
// threw on it.
|
|
41
46
|
async function removeLink(relativePath) {
|
|
42
47
|
let uri = path.join(runtime.options.outputFolder, relativePath)
|
|
43
48
|
if (options.outputFolder) uri = path.join(runtime.options.outputFolder, options.outputFolder, relativePath)
|
|
44
|
-
|
|
49
|
+
try {
|
|
50
|
+
await unlink(path.resolve(uri))
|
|
51
|
+
} catch (err) {
|
|
52
|
+
if (err.code !== 'ENOENT') throw err
|
|
53
|
+
}
|
|
45
54
|
}
|
|
46
55
|
|
|
47
56
|
async function link(source) {
|
|
@@ -58,12 +67,24 @@ export function files(options = {}) {
|
|
|
58
67
|
const source = path.join(runtime.options.filesFolder, relativePath)
|
|
59
68
|
const format = path.extname(relativePath).substring(1).toLowerCase()
|
|
60
69
|
const id = path.join(`/${collection}`, relativePath)
|
|
61
|
-
let uri = path.join(runtime.options.outputFolder, relativePath)
|
|
62
70
|
let name = relativePath
|
|
63
71
|
if (options.outputFolder) {
|
|
64
|
-
uri = path.join(runtime.options.outputFolder, options.outputFolder, relativePath)
|
|
65
72
|
name = path.join(options.outputFolder, relativePath)
|
|
66
73
|
}
|
|
74
|
+
// The SOURCE file, as in every other collection.
|
|
75
|
+
//
|
|
76
|
+
// It used to be the symlink in the output, which made `uri` mean
|
|
77
|
+
// one thing for documents and layouts and the opposite here — and
|
|
78
|
+
// three separate pieces of code carry scars from it: the render
|
|
79
|
+
// file helper keys its edges on `id` with a comment explaining
|
|
80
|
+
// that a uri edge "matches nothing" for files, locateEntityFile
|
|
81
|
+
// rejected every file entity as living outside its own collection
|
|
82
|
+
// folder, and sweepDeleted could not be called at all because it
|
|
83
|
+
// scopes by uri rooted at the folder a source owns.
|
|
84
|
+
//
|
|
85
|
+
// Where the bytes are PUBLISHED is meta.url, which every consumer
|
|
86
|
+
// already reads (ADR-0011).
|
|
87
|
+
const uri = source
|
|
67
88
|
|
|
68
89
|
let synced = true
|
|
69
90
|
switch (action) {
|
|
@@ -145,6 +166,7 @@ export function files(options = {}) {
|
|
|
145
166
|
})
|
|
146
167
|
|
|
147
168
|
onImport(async () => {
|
|
169
|
+
const logger = useLogger()
|
|
148
170
|
await mkdir(runtime.options.outputFolder, { recursive: true })
|
|
149
171
|
if (options.outputFolder) await mkdir(path.join(runtime.options.outputFolder, options.outputFolder), { recursive: true })
|
|
150
172
|
|
|
@@ -167,13 +189,19 @@ export function files(options = {}) {
|
|
|
167
189
|
// repair path short of deleting them.
|
|
168
190
|
const forced = runtime.options.force || runtime.catalog?.cacheInvalidated
|
|
169
191
|
const priorChecksums = checksumsByCollection(collection)
|
|
192
|
+
const scanned = new Set()
|
|
170
193
|
await pMap(paths, async relativePath => {
|
|
171
|
-
const {
|
|
194
|
+
const { source } = await ensureLink(relativePath)
|
|
195
|
+
const uri = source
|
|
172
196
|
let name = relativePath
|
|
173
197
|
if (options.outputFolder) {
|
|
174
198
|
name = path.join(options.outputFolder, relativePath)
|
|
175
199
|
}
|
|
176
200
|
const id = path.join(`/${collection}`, relativePath)
|
|
201
|
+
// Recorded before the checksum gate. The gate decides whether
|
|
202
|
+
// to re-emit; the sweep asks what still EXISTS, and a file
|
|
203
|
+
// skipped for being unchanged very much exists.
|
|
204
|
+
scanned.add(id)
|
|
177
205
|
const newChecksum = await checksum(source)
|
|
178
206
|
updateProgress()
|
|
179
207
|
// Gate: if the catalog already has this entity with the same
|
|
@@ -196,6 +224,31 @@ export function files(options = {}) {
|
|
|
196
224
|
link: await link(source),
|
|
197
225
|
})
|
|
198
226
|
}, { concurrency: 16 })
|
|
227
|
+
|
|
228
|
+
// What the catalog holds that the disk no longer does.
|
|
229
|
+
//
|
|
230
|
+
// The scan enumerates what exists and never asked the opposite
|
|
231
|
+
// question, so deleting a file with nothing watching left three
|
|
232
|
+
// things behind on every subsequent build: a DANGLING SYMLINK in
|
|
233
|
+
// the deployed output, the catalog row, and anything derived from
|
|
234
|
+
// it. Neither a plain rebuild nor --force removed them; only
|
|
235
|
+
// --clear did, or a watcher that happened to be running at the
|
|
236
|
+
// moment the file disappeared.
|
|
237
|
+
//
|
|
238
|
+
// ownerPrefix is the files folder, which is only a meaningful
|
|
239
|
+
// scope now that `uri` names the source. It keeps the sweep off
|
|
240
|
+
// entities another plugin emitted into this collection — a CSV, a
|
|
241
|
+
// drive, an API — which is the accident the parameter exists to
|
|
242
|
+
// prevent.
|
|
243
|
+
//
|
|
244
|
+
// Skipped under --force, like every other sweep: that guard lives
|
|
245
|
+
// in sweepDeleted and is shared with documents and layouts.
|
|
246
|
+
const deleted = await sweepDeleted(collection, scanned, async (entity) => {
|
|
247
|
+
await removeLink(path.relative(runtime.options.filesFolder, entity.uri))
|
|
248
|
+
await deleteEntity(entity)
|
|
249
|
+
logger.debug('files removed (file gone): %s', entity.name)
|
|
250
|
+
}, runtime.options.filesFolder)
|
|
251
|
+
if (deleted) logger.info('Files removed: %d no longer on disk', deleted)
|
|
199
252
|
})
|
|
200
253
|
|
|
201
254
|
return {
|
package/src/source.js
CHANGED
|
@@ -130,7 +130,20 @@ export async function gateChecksum(file, id, { reload = false, priorChecksums, b
|
|
|
130
130
|
// LRU population the scan already does, and we get rid of the 7GB JS
|
|
131
131
|
// heap peak in return.
|
|
132
132
|
export async function sweepDeleted(collection, scanned, onDelete, ownerPrefix) {
|
|
133
|
-
|
|
133
|
+
// --force does NOT skip this any more.
|
|
134
|
+
//
|
|
135
|
+
// It used to, on a stated premise — "operator wants a full rebuild;
|
|
136
|
+
// deletes still flow naturally on the rebuild" — that is not true and can
|
|
137
|
+
// be disproved in one command: delete a document, run `mikser --force`,
|
|
138
|
+
// and both the entity and its output are still there, while an ordinary
|
|
139
|
+
// build removes them. Nothing about --force wipes the catalog (that is
|
|
140
|
+
// --clear, via forceWipe), so a row for a file that no longer exists
|
|
141
|
+
// simply survives.
|
|
142
|
+
//
|
|
143
|
+
// Which made --force the one flag that could not fix what it is reached
|
|
144
|
+
// for. It defeats the checksum GATE; the scanned set is built from the
|
|
145
|
+
// glob before any gate runs, so it is exactly as complete under --force as
|
|
146
|
+
// without it, and the sweep is no less accurate for running.
|
|
134
147
|
if (!ownerPrefix) {
|
|
135
148
|
throw new Error(
|
|
136
149
|
'sweepDeleted: ownerPrefix is required — pass the absolute folder ' +
|