@wangxiaocuo/dsh-session-trash 0.5.1 → 0.6.1
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/README.en.md +21 -16
- package/README.md +21 -17
- package/client.js +62 -4
- package/lib/concurrency.js +30 -0
- package/lib/eligibility.js +150 -52
- package/lib/fsops.js +19 -0
- package/lib/http.js +5 -0
- package/lib/inventory.js +224 -36
- package/lib/members.js +95 -0
- package/lib/state.js +9 -21
- package/lib/store.js +69 -8
- package/lib/trash-service.js +832 -382
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +7 -2
package/lib/store.js
CHANGED
|
@@ -2,10 +2,11 @@ import fs from 'node:fs/promises'
|
|
|
2
2
|
import os from 'node:os'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { TrashError } from './errors.js'
|
|
5
|
-
import { isInside, removePathWithRetry, renameWithRetry, samePath } from './fsops.js'
|
|
5
|
+
import { isInside, isSafeMemberId, removePathWithRetry, renameWithRetry, samePath } from './fsops.js'
|
|
6
|
+
import { isValidMemberList, parentMember } from './members.js'
|
|
6
7
|
|
|
7
8
|
/** On-disk index schema version understood by this release. */
|
|
8
|
-
export const INDEX_VERSION =
|
|
9
|
+
export const INDEX_VERSION = 2
|
|
9
10
|
|
|
10
11
|
/** A lock older than this is assumed abandoned and may be taken over. */
|
|
11
12
|
export const LOCK_STALE_MS = 10 * 60 * 1000
|
|
@@ -21,6 +22,20 @@ export function emptyIndex(now = Date.now()) {
|
|
|
21
22
|
return { version: INDEX_VERSION, updatedAt: now, entries: [] }
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Coerce one durable entry to the v2 shape. A v1 entry names a single
|
|
27
|
+
* directory, which becomes the parent member of a one-member tree.
|
|
28
|
+
*
|
|
29
|
+
* @param {object} entry entry from either durable shape.
|
|
30
|
+
* @returns {object} the v2 entry.
|
|
31
|
+
*/
|
|
32
|
+
function normalizeEntry(entry) {
|
|
33
|
+
if (entry === null || typeof entry !== 'object') return entry
|
|
34
|
+
if (Array.isArray(entry.members) && entry.members.length > 0) return entry
|
|
35
|
+
const parent = parentMember(entry)
|
|
36
|
+
return parent === null ? entry : { ...entry, members: [parent] }
|
|
37
|
+
}
|
|
38
|
+
|
|
24
39
|
/**
|
|
25
40
|
* Validate the persisted index shape strictly enough that a truncated or
|
|
26
41
|
* hand-edited file enters protection mode instead of silently vanishing.
|
|
@@ -30,7 +45,7 @@ export function emptyIndex(now = Date.now()) {
|
|
|
30
45
|
*/
|
|
31
46
|
export function isValidIndex(value) {
|
|
32
47
|
if (value === null || typeof value !== 'object' || Array.isArray(value)) return false
|
|
33
|
-
if (value.version !== INDEX_VERSION) return false
|
|
48
|
+
if (value.version !== INDEX_VERSION && value.version !== 1) return false
|
|
34
49
|
if (typeof value.updatedAt !== 'number' || !Number.isFinite(value.updatedAt)) return false
|
|
35
50
|
if (!Array.isArray(value.entries)) return false
|
|
36
51
|
const seen = new Set()
|
|
@@ -42,6 +57,7 @@ export function isValidIndex(value) {
|
|
|
42
57
|
if (typeof entry.trashedAt !== 'number' || !Number.isFinite(entry.trashedAt)) return false
|
|
43
58
|
if (seen.has(entry.sessionId)) return false
|
|
44
59
|
seen.add(entry.sessionId)
|
|
60
|
+
if (value.version === INDEX_VERSION && !isValidMemberList(entry)) return false
|
|
45
61
|
}
|
|
46
62
|
return true
|
|
47
63
|
}
|
|
@@ -90,6 +106,8 @@ export class TrashStore {
|
|
|
90
106
|
this.lockOwned = false
|
|
91
107
|
this.lockTimer = null
|
|
92
108
|
this.queue = Promise.resolve()
|
|
109
|
+
/** Whether the last load read an older index version that must be re-committed. */
|
|
110
|
+
this.needsUpgrade = false
|
|
93
111
|
}
|
|
94
112
|
|
|
95
113
|
/**
|
|
@@ -108,6 +126,10 @@ export class TrashStore {
|
|
|
108
126
|
} else if (this.status === 'recovered') {
|
|
109
127
|
this.logger.warn(`trash index recovered from backup: ${this.error}`)
|
|
110
128
|
}
|
|
129
|
+
if (this.needsUpgrade) {
|
|
130
|
+
await this.writeIndex(this.index.entries)
|
|
131
|
+
this.logger.info(`upgraded the trash index to version ${INDEX_VERSION}`)
|
|
132
|
+
}
|
|
111
133
|
return this.describe()
|
|
112
134
|
}
|
|
113
135
|
|
|
@@ -150,12 +172,34 @@ export class TrashStore {
|
|
|
150
172
|
|
|
151
173
|
/**
|
|
152
174
|
* @param {string} trashId object identity.
|
|
153
|
-
* @returns {string} the moved session directory inside the object directory.
|
|
175
|
+
* @returns {string} the moved parent session directory inside the object directory.
|
|
154
176
|
*/
|
|
155
177
|
sessionPath(trashId) {
|
|
156
178
|
return path.join(this.objectDir(trashId), 'session')
|
|
157
179
|
}
|
|
158
180
|
|
|
181
|
+
/**
|
|
182
|
+
* @param {string} trashId object identity.
|
|
183
|
+
* @param {string} sessionId descendant session identity.
|
|
184
|
+
* @returns {string} the moved descendant directory inside the object directory.
|
|
185
|
+
*/
|
|
186
|
+
childPath(trashId, sessionId) {
|
|
187
|
+
if (!isSafeMemberId(sessionId)) {
|
|
188
|
+
throw new TrashError('IO_ERROR', 'Refusing an unsafe member directory name inside the trash object.')
|
|
189
|
+
}
|
|
190
|
+
return path.join(this.objectDir(trashId), 'children', sessionId)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* @param {string} trashId object identity.
|
|
195
|
+
* @param {number} index member position; `0` is the visible parent.
|
|
196
|
+
* @param {string} sessionId member identity.
|
|
197
|
+
* @returns {string} the directory that owns this member inside the object.
|
|
198
|
+
*/
|
|
199
|
+
memberPath(trashId, index, sessionId) {
|
|
200
|
+
return index === 0 ? this.sessionPath(trashId) : this.childPath(trashId, sessionId)
|
|
201
|
+
}
|
|
202
|
+
|
|
159
203
|
/**
|
|
160
204
|
* @param {string} trashId object identity.
|
|
161
205
|
* @returns {string} the self-describing metadata path inside the object directory.
|
|
@@ -309,15 +353,17 @@ export class TrashStore {
|
|
|
309
353
|
}
|
|
310
354
|
|
|
311
355
|
/**
|
|
312
|
-
* Load the index, falling back to the previous committed backup.
|
|
356
|
+
* Load the index, falling back to the previous committed backup. An older
|
|
357
|
+
* schema is normalized in memory and re-committed by {@link init}.
|
|
313
358
|
*
|
|
314
359
|
* @returns {Promise<void>} resolution once `index` and `status` are set.
|
|
315
360
|
*/
|
|
316
361
|
async load() {
|
|
362
|
+
this.needsUpgrade = false
|
|
317
363
|
try {
|
|
318
364
|
const data = await this.readJson(this.indexPath)
|
|
319
365
|
if (!isValidIndex(data)) throw new Error('unsupported index shape or version')
|
|
320
|
-
this.index = data
|
|
366
|
+
this.index = this.normalizeIndex(data)
|
|
321
367
|
this.status = 'ok'
|
|
322
368
|
this.error = null
|
|
323
369
|
return
|
|
@@ -332,7 +378,7 @@ export class TrashStore {
|
|
|
332
378
|
try {
|
|
333
379
|
const backup = await this.readJson(this.bakPath)
|
|
334
380
|
if (!isValidIndex(backup)) throw new Error('unsupported backup shape or version')
|
|
335
|
-
this.index = backup
|
|
381
|
+
this.index = this.normalizeIndex(backup)
|
|
336
382
|
this.status = 'recovered'
|
|
337
383
|
this.error = primary
|
|
338
384
|
return
|
|
@@ -345,6 +391,19 @@ export class TrashStore {
|
|
|
345
391
|
}
|
|
346
392
|
}
|
|
347
393
|
|
|
394
|
+
/**
|
|
395
|
+
* @param {{ version: number, updatedAt: number, entries: readonly object[] }} data parsed index document.
|
|
396
|
+
* @returns {{ version: number, updatedAt: number, entries: object[] }} the v2 document.
|
|
397
|
+
*/
|
|
398
|
+
normalizeIndex(data) {
|
|
399
|
+
if (data.version !== INDEX_VERSION) this.needsUpgrade = true
|
|
400
|
+
return {
|
|
401
|
+
version: INDEX_VERSION,
|
|
402
|
+
updatedAt: data.updatedAt,
|
|
403
|
+
entries: data.entries.map((entry) => normalizeEntry(entry)),
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
348
407
|
/**
|
|
349
408
|
* Atomically commit the index: write a temporary file, preserve the previous
|
|
350
409
|
* commit as `.bak`, then rename over the live index.
|
|
@@ -353,7 +412,8 @@ export class TrashStore {
|
|
|
353
412
|
* @returns {Promise<{ version: number, updatedAt: number, entries: readonly object[] }>} the committed document.
|
|
354
413
|
*/
|
|
355
414
|
async writeIndex(entries) {
|
|
356
|
-
const
|
|
415
|
+
const normalized = entries.map((entry) => normalizeEntry(entry))
|
|
416
|
+
const data = { version: INDEX_VERSION, updatedAt: Date.now(), entries: normalized }
|
|
357
417
|
const json = `${JSON.stringify(data, null, 2)}\n`
|
|
358
418
|
try {
|
|
359
419
|
const handle = await fs.open(this.tmpPath, 'w', 0o600)
|
|
@@ -379,6 +439,7 @@ export class TrashStore {
|
|
|
379
439
|
this.index = data
|
|
380
440
|
this.status = 'ok'
|
|
381
441
|
this.error = null
|
|
442
|
+
this.needsUpgrade = false
|
|
382
443
|
return data
|
|
383
444
|
}
|
|
384
445
|
|