@summerscar/dsh-mnemon-sync 0.1.0 → 0.1.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/client.js +35 -2
- package/index.js +38 -13
- package/package.json +1 -1
- package/scheduler.js +16 -0
package/client.js
CHANGED
|
@@ -56,6 +56,7 @@ window.__ModuleLoader__.load({
|
|
|
56
56
|
componentSpaces: '{count} 个空间',
|
|
57
57
|
backups: '备份列表',
|
|
58
58
|
listBackups: '从目标列出备份',
|
|
59
|
+
refresh: '刷新',
|
|
59
60
|
backupsHint: '列出远端已存在的备份包,可查看明细并恢复。',
|
|
60
61
|
noBackups: '目标上还没有备份包',
|
|
61
62
|
backupName: '文件名',
|
|
@@ -118,7 +119,6 @@ window.__ModuleLoader__.load({
|
|
|
118
119
|
backingUp: '备份中…',
|
|
119
120
|
backupDone: '备份完成',
|
|
120
121
|
backupFailed: '备份失败',
|
|
121
|
-
never: '从未',
|
|
122
122
|
history: '运行记录',
|
|
123
123
|
noHistory: '还没有运行记录',
|
|
124
124
|
ok: '成功',
|
|
@@ -129,6 +129,9 @@ window.__ModuleLoader__.load({
|
|
|
129
129
|
duration: '耗时',
|
|
130
130
|
size: '体积',
|
|
131
131
|
componentsCount: '组件',
|
|
132
|
+
clearHistory: '清空记录',
|
|
133
|
+
historyCleared: '运行记录已清空',
|
|
134
|
+
clearFailed: '清空失败',
|
|
132
135
|
error: '错误',
|
|
133
136
|
loading: '读取中…',
|
|
134
137
|
loadFailed: '读取失败',
|
|
@@ -157,6 +160,7 @@ window.__ModuleLoader__.load({
|
|
|
157
160
|
componentSpaces: '{count} spaces',
|
|
158
161
|
backups: 'Backups',
|
|
159
162
|
listBackups: 'List from target',
|
|
163
|
+
refresh: 'Refresh',
|
|
160
164
|
backupsHint: 'List the archives already on the target, inspect one, and restore it.',
|
|
161
165
|
noBackups: 'No archive on the target yet',
|
|
162
166
|
backupName: 'File',
|
|
@@ -229,6 +233,9 @@ window.__ModuleLoader__.load({
|
|
|
229
233
|
duration: 'Duration',
|
|
230
234
|
size: 'Size',
|
|
231
235
|
componentsCount: 'Components',
|
|
236
|
+
clearHistory: 'Clear log',
|
|
237
|
+
historyCleared: 'Run history cleared',
|
|
238
|
+
clearFailed: 'Could not clear',
|
|
232
239
|
error: 'Error',
|
|
233
240
|
loading: 'Loading\u2026',
|
|
234
241
|
loadFailed: 'Could not load',
|
|
@@ -657,6 +664,24 @@ window.__ModuleLoader__.load({
|
|
|
657
664
|
}
|
|
658
665
|
}
|
|
659
666
|
|
|
667
|
+
/**
|
|
668
|
+
* Drop the run log. Archives on the target are not touched — this is a
|
|
669
|
+
* display reset, and the page says so before the button is pressed.
|
|
670
|
+
*/
|
|
671
|
+
const clearHistory = async () => {
|
|
672
|
+
setBusy(true)
|
|
673
|
+
setNotice(null)
|
|
674
|
+
try {
|
|
675
|
+
await api('/clear-history', { method: 'POST', body: '{}' })
|
|
676
|
+
await load()
|
|
677
|
+
setNotice({ ok: true, text: t('historyCleared') })
|
|
678
|
+
} catch (cause) {
|
|
679
|
+
setNotice({ ok: false, text: `${t('clearFailed')}: ${cause.message}` })
|
|
680
|
+
} finally {
|
|
681
|
+
setBusy(false)
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
660
685
|
const loadArchives = async () => {
|
|
661
686
|
setArchivesBusy(true)
|
|
662
687
|
setArchivesError(null)
|
|
@@ -1082,7 +1107,15 @@ window.__ModuleLoader__.load({
|
|
|
1082
1107
|
|
|
1083
1108
|
// history
|
|
1084
1109
|
h('section', { style: styles.card },
|
|
1085
|
-
h('
|
|
1110
|
+
h('div', { style: styles.row },
|
|
1111
|
+
h('h3', { style: { ...styles.cardTitle, marginRight: 'auto' } }, t('history')),
|
|
1112
|
+
// The list keeps the newest 20 on its own; this clears what is left.
|
|
1113
|
+
history.length === 0 ? null : h('button', {
|
|
1114
|
+
style: styles.button,
|
|
1115
|
+
disabled: busy,
|
|
1116
|
+
onClick: () => void clearHistory(),
|
|
1117
|
+
}, t('clearHistory')),
|
|
1118
|
+
),
|
|
1086
1119
|
history.length === 0
|
|
1087
1120
|
? h('p', { style: styles.note }, t('noHistory'))
|
|
1088
1121
|
: h('table', { style: styles.table },
|
package/index.js
CHANGED
|
@@ -199,19 +199,8 @@ export function apply(ctx, config = {}) {
|
|
|
199
199
|
|
|
200
200
|
if (req.method === 'PUT' && route === '/settings') {
|
|
201
201
|
const body = await readJsonBody(req)
|
|
202
|
-
const previous = settings()
|
|
203
202
|
const incoming = body?.settings ?? body
|
|
204
|
-
|
|
205
|
-
if (incoming !== null && typeof incoming === 'object' && incoming.target !== null && typeof incoming.target === 'object') {
|
|
206
|
-
const target = incoming.target
|
|
207
|
-
if (target.kind === 'webdav' && isMasked(target.password)) {
|
|
208
|
-
target.password = previous.target.kind === 'webdav' ? previous.target.password : ''
|
|
209
|
-
}
|
|
210
|
-
if (target.kind === 's3' && isMasked(target.secretAccessKey)) {
|
|
211
|
-
target.secretAccessKey = previous.target.kind === 's3' ? previous.target.secretAccessKey : ''
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
const saved = writeSettings(home, normalizeSettings(incoming))
|
|
203
|
+
const saved = writeSettings(home, normalizeSettings(restoreMaskedSecrets(incoming, settings())))
|
|
215
204
|
scheduler.refresh()
|
|
216
205
|
sendJson(res, 200, { settings: maskSettings(saved) })
|
|
217
206
|
return
|
|
@@ -225,7 +214,10 @@ export function apply(ctx, config = {}) {
|
|
|
225
214
|
const body = await readJsonBody(req)
|
|
226
215
|
try {
|
|
227
216
|
if (body?.settings !== undefined) {
|
|
228
|
-
|
|
217
|
+
// The same masked-secret rule as saving settings: the page always
|
|
218
|
+
// sends the mask it was shown, and writing that mask as the real
|
|
219
|
+
// password would silently break the next upload with a 401.
|
|
220
|
+
const saved = writeSettings(home, normalizeSettings(restoreMaskedSecrets(body.settings, settings())))
|
|
229
221
|
scheduler.refresh()
|
|
230
222
|
const result = await scheduler.runNow('manual')
|
|
231
223
|
sendJson(res, 200, { result, settings: maskSettings(saved) })
|
|
@@ -239,6 +231,12 @@ export function apply(ctx, config = {}) {
|
|
|
239
231
|
return
|
|
240
232
|
}
|
|
241
233
|
|
|
234
|
+
if (req.method === 'POST' && route === '/clear-history') {
|
|
235
|
+
// Only the local run log is cleared; archives on the target are untouched.
|
|
236
|
+
sendJson(res, 200, { history: history.clear() })
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
|
|
242
240
|
if (req.method === 'POST' && route === '/refresh-schedule') {
|
|
243
241
|
scheduler.refresh()
|
|
244
242
|
sendJson(res, 200, { scheduler: scheduler.snapshot(settings()) })
|
|
@@ -557,6 +555,33 @@ function isMasked(value) {
|
|
|
557
555
|
return typeof value === 'string' && value !== '' && /^•+$/u.test(value)
|
|
558
556
|
}
|
|
559
557
|
|
|
558
|
+
/**
|
|
559
|
+
* Put the real secrets back into a settings payload that arrived masked.
|
|
560
|
+
*
|
|
561
|
+
* The page never receives a secret — `maskSettings` replaces it with bullets —
|
|
562
|
+
* and echoes those bullets back on every save. Treating them as a value would
|
|
563
|
+
* overwrite the real credential with eight bullet characters, which is exactly
|
|
564
|
+
* what made a correctly configured WebDAV target answer 401: the stored
|
|
565
|
+
* password really was `••••••••`.
|
|
566
|
+
*
|
|
567
|
+
* Any write path that accepts settings from the browser must run this first.
|
|
568
|
+
*/
|
|
569
|
+
function restoreMaskedSecrets(incoming, previous) {
|
|
570
|
+
if (incoming === null || typeof incoming !== 'object') return incoming
|
|
571
|
+
const target = incoming.target
|
|
572
|
+
if (target === null || typeof target !== 'object') return incoming
|
|
573
|
+
if (target.kind === 'webdav' && isMasked(target.password)) {
|
|
574
|
+
target.password = previous.target.kind === 'webdav' ? previous.target.password : ''
|
|
575
|
+
}
|
|
576
|
+
if (target.kind === 's3' && isMasked(target.secretAccessKey)) {
|
|
577
|
+
target.secretAccessKey = previous.target.kind === 's3' ? previous.target.secretAccessKey : ''
|
|
578
|
+
}
|
|
579
|
+
if (target.kind === 's3' && isMasked(target.sessionToken)) {
|
|
580
|
+
target.sessionToken = previous.target.kind === 's3' ? previous.target.sessionToken : ''
|
|
581
|
+
}
|
|
582
|
+
return incoming
|
|
583
|
+
}
|
|
584
|
+
|
|
560
585
|
/**
|
|
561
586
|
* Resolve the dsh-mnemon extension SDK.
|
|
562
587
|
*
|
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.1",
|
|
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",
|
package/scheduler.js
CHANGED
|
@@ -72,6 +72,22 @@ export class SyncHistory {
|
|
|
72
72
|
latest() {
|
|
73
73
|
return this.read()[0]
|
|
74
74
|
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Drop the whole run history.
|
|
78
|
+
*
|
|
79
|
+
* History is a display convenience, not a record of truth: the archives
|
|
80
|
+
* themselves live on the target, and retention keys off the same list, so
|
|
81
|
+
* clearing it simply stops the page from showing past runs. Retention then
|
|
82
|
+
* has nothing to delete, which is the honest outcome — after clearing, this
|
|
83
|
+
* plugin no longer knows which keys it uploaded.
|
|
84
|
+
*/
|
|
85
|
+
clear() {
|
|
86
|
+
const path = this.path()
|
|
87
|
+
mkdirSync(join(this.root, STATE_DIRECTORY), { recursive: true, mode: 0o700 })
|
|
88
|
+
writeFileSync(path, `${JSON.stringify({ version: 1, runs: [] }, null, 2)}\n`, { mode: 0o600 })
|
|
89
|
+
return []
|
|
90
|
+
}
|
|
75
91
|
}
|
|
76
92
|
|
|
77
93
|
/**
|