@summerscar/dsh-mnemon-sync 0.1.4 → 0.1.5
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/index.js +53 -7
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
import { createHash } from 'node:crypto'
|
|
22
22
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
23
|
+
import { hostname } from 'node:os'
|
|
23
24
|
import { dirname, join, resolve } from 'node:path'
|
|
24
25
|
|
|
25
26
|
import { SyncHistory, SyncScheduler } from './scheduler.js'
|
|
@@ -83,9 +84,44 @@ async function readJsonBody(req) {
|
|
|
83
84
|
return JSON.parse(text)
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
/**
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
/**
|
|
88
|
+
* The name this machine goes by in an archive name.
|
|
89
|
+
*
|
|
90
|
+
* Read from dsh-mnemon's own identity file (`state/machine.json`) so a backup
|
|
91
|
+
* uploaded from several machines stays tellable apart. Falls back to the
|
|
92
|
+
* hostname, then to a fixed word, so the key never ends up with an empty
|
|
93
|
+
* segment. Characters that would be awkward in a filename — slashes, spaces,
|
|
94
|
+
* anything a WebDAV path segment must escape — collapse to a single dash.
|
|
95
|
+
*/
|
|
96
|
+
export function deviceName(home) {
|
|
97
|
+
let label = ''
|
|
98
|
+
try {
|
|
99
|
+
const identity = JSON.parse(readFileSync(join(home, 'state', 'machine.json'), 'utf8'))
|
|
100
|
+
if (typeof identity?.label === 'string') label = identity.label
|
|
101
|
+
} catch {
|
|
102
|
+
// No identity file yet: the hostname is the same fallback dsh-mnemon uses.
|
|
103
|
+
}
|
|
104
|
+
if (label.trim() === '') {
|
|
105
|
+
try {
|
|
106
|
+
label = hostname()
|
|
107
|
+
} catch {
|
|
108
|
+
label = ''
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const cleaned = label.trim().replace(/[^\p{L}\p{N}._-]+/gu, '-').replace(/^-+|-+$/gu, '')
|
|
112
|
+
return cleaned === '' ? 'device' : cleaned.slice(0, 60)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* `mnemon-sync-<device>-2026-11-09T00-53-57.903Z.zip`
|
|
117
|
+
*
|
|
118
|
+
* The device name sits after the prefix and before the timestamp, so archives
|
|
119
|
+
* from different machines sort together by time within one device and are
|
|
120
|
+
* still obvious at a glance in a shared folder.
|
|
121
|
+
*/
|
|
122
|
+
export function archiveKey(at = new Date(), device = 'device') {
|
|
123
|
+
const stamp = at.toISOString().replace(/:/gu, '-')
|
|
124
|
+
return `${ARCHIVE_PREFIX}${device}-${stamp}.zip`
|
|
89
125
|
}
|
|
90
126
|
|
|
91
127
|
export function apply(ctx, config = {}) {
|
|
@@ -108,7 +144,7 @@ export function apply(ctx, config = {}) {
|
|
|
108
144
|
try {
|
|
109
145
|
const snapshot = await buildSnapshot(home, staging, { components: current.components })
|
|
110
146
|
verifySnapshot(staging)
|
|
111
|
-
const key = archiveKey()
|
|
147
|
+
const key = archiveKey(new Date(), deviceName(home))
|
|
112
148
|
const archive = createZip(collect(staging), { level: 6 })
|
|
113
149
|
const uploaded = await uploadArchive(target, key, archive, { signal })
|
|
114
150
|
const removed = await applyRetention(history, target, current.retention.keep, { signal })
|
|
@@ -610,11 +646,21 @@ function collect(root, prefix = '') {
|
|
|
610
646
|
* as removed.
|
|
611
647
|
*/
|
|
612
648
|
async function applyRetention(history, target, keep, options) {
|
|
649
|
+
// Sort by when the run happened, not by key. With a device name in the key,
|
|
650
|
+
// lexical order groups by device first, so "keep the newest N" would delete
|
|
651
|
+
// this machine's newest archive before another machine's oldest one.
|
|
613
652
|
const uploaded = history.read()
|
|
614
653
|
.filter((run) => run.ok && typeof run.key === 'string')
|
|
615
|
-
.map((run) => run.key)
|
|
616
|
-
const
|
|
617
|
-
const
|
|
654
|
+
.map((run) => ({ key: run.key, at: Date.parse(run.finishedAt ?? run.startedAt ?? '') || 0 }))
|
|
655
|
+
const seen = new Set()
|
|
656
|
+
const unique = []
|
|
657
|
+
for (const entry of uploaded) {
|
|
658
|
+
if (seen.has(entry.key)) continue
|
|
659
|
+
seen.add(entry.key)
|
|
660
|
+
unique.push(entry)
|
|
661
|
+
}
|
|
662
|
+
unique.sort((left, right) => right.at - left.at)
|
|
663
|
+
const overflow = unique.slice(keep).map((entry) => entry.key)
|
|
618
664
|
const removed = []
|
|
619
665
|
for (const key of overflow) {
|
|
620
666
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@summerscar/dsh-mnemon-sync",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Back up and restore dsh-mnemon memory components (memory spaces, project documents, runtime memory) to S3-compatible storage or WebDAV.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|