pinokiod 8.0.46 → 8.0.47
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.
|
@@ -1,5 +1,11 @@
|
|
|
1
|
+
const CANDIDATE_SIZE_BASE = process.platform === "win32" ? 1024 : 1000
|
|
2
|
+
const CANDIDATE_SIZE_OPTIONS = [10, 50, 100, 500]
|
|
3
|
+
.map((value) => value * CANDIDATE_SIZE_BASE ** 2)
|
|
4
|
+
.concat(CANDIDATE_SIZE_BASE ** 3)
|
|
5
|
+
|
|
1
6
|
module.exports = {
|
|
2
|
-
SIZE_THRESHOLD:
|
|
7
|
+
SIZE_THRESHOLD: CANDIDATE_SIZE_OPTIONS[2],
|
|
8
|
+
CANDIDATE_SIZE_OPTIONS,
|
|
3
9
|
TMP_SUFFIX: ".pinokio-dedup-tmp",
|
|
4
10
|
SHA256_RE: /^[0-9a-f]{64}$/,
|
|
5
11
|
ENTRY_BATCH_SIZE: 256,
|
package/kernel/vault/index.js
CHANGED
|
@@ -7,8 +7,8 @@ const Sweeper = require('./sweeper')
|
|
|
7
7
|
const { walkBatches, statMany } = require('./walker')
|
|
8
8
|
const { fileSnapshot, sameSnapshot, sameContentState } = require('./snapshot')
|
|
9
9
|
const {
|
|
10
|
-
SIZE_THRESHOLD, TMP_SUFFIX, SHA256_RE,
|
|
11
|
-
HASH_INACTIVITY_MS
|
|
10
|
+
SIZE_THRESHOLD, CANDIDATE_SIZE_OPTIONS, TMP_SUFFIX, SHA256_RE,
|
|
11
|
+
DIR_CONCURRENCY, STAT_CONCURRENCY, HASH_INACTIVITY_MS
|
|
12
12
|
} = require('./constants')
|
|
13
13
|
|
|
14
14
|
// Shared model store engine (spec/requirements/shared-model-store.md).
|
|
@@ -171,9 +171,10 @@ class Vault {
|
|
|
171
171
|
if (this.fileActionProgress === progress) this.fileActionProgress = null
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
|
-
startScan(scopeId = null) {
|
|
174
|
+
startScan(scopeId = null, sizeThreshold = this.sizeThreshold) {
|
|
175
175
|
if (!this.enabled || !this.sweeper) return { started: false, disabled: true }
|
|
176
176
|
if (this.scanPromise) return { started: false, already_running: true }
|
|
177
|
+
this.sizeThreshold = sizeThreshold
|
|
177
178
|
this.scanError = null
|
|
178
179
|
this.scanScopeId = scopeId
|
|
179
180
|
this.scanPromise = this.runExclusive(() => this.sweeper.scan(scopeId))
|
|
@@ -204,11 +205,19 @@ class Vault {
|
|
|
204
205
|
}
|
|
205
206
|
}
|
|
206
207
|
}
|
|
207
|
-
case "scan":
|
|
208
|
+
case "scan": {
|
|
208
209
|
if (payload.scope_id && !this.scanSource(payload.scope_id)) {
|
|
209
210
|
return { error: "That scan location is no longer available." }
|
|
210
211
|
}
|
|
211
|
-
|
|
212
|
+
let sizeThreshold = this.sizeThreshold
|
|
213
|
+
if (payload.candidate_size != null) {
|
|
214
|
+
if (!CANDIDATE_SIZE_OPTIONS.includes(payload.candidate_size)) {
|
|
215
|
+
return { error: "Choose a valid minimum file size." }
|
|
216
|
+
}
|
|
217
|
+
sizeThreshold = payload.candidate_size
|
|
218
|
+
}
|
|
219
|
+
return this.startScan(payload.scope_id || null, sizeThreshold)
|
|
220
|
+
}
|
|
212
221
|
case "deduplicate":
|
|
213
222
|
if (typeof payload.path === "string" && payload.path) {
|
|
214
223
|
return this.runMutation(() => this.runFileAction({
|
package/package.json
CHANGED
package/server/public/vault.css
CHANGED
|
@@ -29,7 +29,10 @@ body.dark.vault-page {
|
|
|
29
29
|
min-height: 0;
|
|
30
30
|
overflow: hidden;
|
|
31
31
|
}
|
|
32
|
-
.vault-embed-main .vault-shell {
|
|
32
|
+
.vault-embed-main .vault-shell {
|
|
33
|
+
width: 100%;
|
|
34
|
+
height: 100%;
|
|
35
|
+
}
|
|
33
36
|
body.vault-page .task-container {
|
|
34
37
|
display: flex;
|
|
35
38
|
min-height: 0;
|
|
@@ -924,6 +927,8 @@ body.dark .vault-row-action .vault-text-button:hover:not(:disabled) {
|
|
|
924
927
|
}
|
|
925
928
|
@media (max-width: 820px) {
|
|
926
929
|
body.vault-page .task-container { display: block; overflow-y: auto; }
|
|
930
|
+
.vault-embed-main { overflow-y: auto; }
|
|
931
|
+
.vault-embed-main .vault-shell { height: auto; }
|
|
927
932
|
.vault-shell,
|
|
928
933
|
.vault-shell .task-shell-body,
|
|
929
934
|
.vault-body { height: auto; overflow: visible; }
|
package/server/public/vault.js
CHANGED
|
@@ -42,6 +42,7 @@ const COPY = {
|
|
|
42
42
|
scan_app: "Scan this app",
|
|
43
43
|
scan_again: "Scan again",
|
|
44
44
|
scanning: "Scanning…",
|
|
45
|
+
minimum_file_size: "Minimum file size to scan",
|
|
45
46
|
scanning_elsewhere: "Another location is being scanned",
|
|
46
47
|
scanning_elsewhere_hint: "This app can be scanned when the current scan finishes.",
|
|
47
48
|
vault_options: "Save space options",
|
|
@@ -168,7 +169,7 @@ const COPY = {
|
|
|
168
169
|
no_activity_hint: "Scans and actions will be recorded here.",
|
|
169
170
|
view_all: "View all files",
|
|
170
171
|
show_all_locations: "Show all locations",
|
|
171
|
-
tracked_note: "Only files
|
|
172
|
+
tracked_note: "Only files {size} and larger appear here. Files keep their current locations.",
|
|
172
173
|
duplicate_note: "Only files waiting for review are shown.",
|
|
173
174
|
reclaimable_note: "These copies are no longer used by any configured location. Deleting them frees disk space.",
|
|
174
175
|
converted: "Deduplicated",
|
|
@@ -197,6 +198,11 @@ const statusUrl = (progress = false) => {
|
|
|
197
198
|
return `/info/dedup${suffix ? `?${suffix}` : ""}`
|
|
198
199
|
}
|
|
199
200
|
const reviewedScanKey = `pinokio:vault:reviewed-scan:${SCOPE_ID || "global"}`
|
|
201
|
+
const candidateSizeKey = "pinokio:vault:candidate-size"
|
|
202
|
+
const candidateSizeBase = document.body.dataset.platform === "win32" ? 1024 : 1000
|
|
203
|
+
const candidateSizeOptions = [10, 50, 100, 500]
|
|
204
|
+
.map((value) => value * candidateSizeBase ** 2)
|
|
205
|
+
.concat(candidateSizeBase ** 3)
|
|
200
206
|
|
|
201
207
|
const state = {
|
|
202
208
|
data: null,
|
|
@@ -224,6 +230,10 @@ const esc = (value) => String(value == null ? "" : value).replace(/[&<>"']/g, (c
|
|
|
224
230
|
}[char]))
|
|
225
231
|
const attr = esc
|
|
226
232
|
const fmt = window.PinokioFormatStorageSize
|
|
233
|
+
const candidateSize = () => {
|
|
234
|
+
const value = Number(el("vault-candidate-size").value)
|
|
235
|
+
return candidateSizeOptions.includes(value) ? value : candidateSizeOptions[2]
|
|
236
|
+
}
|
|
227
237
|
const countLabel = (count, singular = COPY.file, plural = COPY.files) => `${count} ${count === 1 ? singular : plural}`
|
|
228
238
|
const basename = (value) => String(value || "").split(/[\\/]/).filter(Boolean).pop() || ""
|
|
229
239
|
const dirname = (value) => {
|
|
@@ -875,6 +885,7 @@ const renderOverview = () => {
|
|
|
875
885
|
? `<i class="fa-solid fa-circle-notch fa-spin"></i>${esc(COPY.scanning)}`
|
|
876
886
|
: `<i class="fa-solid fa-rotate"></i>${esc(last ? COPY.scan_again : (IS_APP_MODE ? COPY.scan_app : COPY.scan))}`
|
|
877
887
|
el("btn-scan").disabled = activeScan
|
|
888
|
+
el("vault-candidate-size").disabled = activeScan
|
|
878
889
|
const optionsButton = el("btn-vault-options")
|
|
879
890
|
const repairTitle = el("vault-repair-title")
|
|
880
891
|
const repairDescription = el("vault-repair-description")
|
|
@@ -1114,7 +1125,7 @@ const render = () => {
|
|
|
1114
1125
|
el("vault-explorer").style.display = "none"
|
|
1115
1126
|
return
|
|
1116
1127
|
}
|
|
1117
|
-
el("vault-explorer").style.display = "
|
|
1128
|
+
el("vault-explorer").style.display = ""
|
|
1118
1129
|
const items = buildItems()
|
|
1119
1130
|
renderViews(items)
|
|
1120
1131
|
renderLocations(items)
|
|
@@ -1133,7 +1144,9 @@ const render = () => {
|
|
|
1133
1144
|
} else {
|
|
1134
1145
|
el("vault-pane-footer").textContent = state.view === "duplicates"
|
|
1135
1146
|
? COPY.duplicate_note
|
|
1136
|
-
: state.view === "reclaimable"
|
|
1147
|
+
: state.view === "reclaimable"
|
|
1148
|
+
? COPY.reclaimable_note
|
|
1149
|
+
: COPY.tracked_note.replace("{size}", fmt(candidateSize()))
|
|
1137
1150
|
}
|
|
1138
1151
|
}
|
|
1139
1152
|
|
|
@@ -1468,7 +1481,11 @@ document.addEventListener("click", async (event) => {
|
|
|
1468
1481
|
state.scanProblemsOpen = false
|
|
1469
1482
|
state.feedback = null
|
|
1470
1483
|
try {
|
|
1471
|
-
const result = await post({
|
|
1484
|
+
const result = await post({
|
|
1485
|
+
action: "scan",
|
|
1486
|
+
scope_id: SCOPE_ID,
|
|
1487
|
+
candidate_size: candidateSize()
|
|
1488
|
+
})
|
|
1472
1489
|
if (result.error) throw new Error(result.error)
|
|
1473
1490
|
await refresh()
|
|
1474
1491
|
} catch (error) {
|
|
@@ -1584,12 +1601,29 @@ document.addEventListener("input", (event) => {
|
|
|
1584
1601
|
renderActionProgress()
|
|
1585
1602
|
})
|
|
1586
1603
|
document.addEventListener("change", (event) => {
|
|
1604
|
+
if (event.target.id === "vault-candidate-size") {
|
|
1605
|
+
try { localStorage.setItem(candidateSizeKey, String(candidateSize())) } catch (error) {}
|
|
1606
|
+
render()
|
|
1607
|
+
return
|
|
1608
|
+
}
|
|
1587
1609
|
if (event.target.id !== "vault-status-filter") return
|
|
1588
1610
|
state.statusFilter = event.target.value
|
|
1589
1611
|
render()
|
|
1590
1612
|
})
|
|
1591
1613
|
|
|
1592
1614
|
el("btn-scan").textContent = COPY.scan
|
|
1615
|
+
const candidateSizeSelect = el("vault-candidate-size")
|
|
1616
|
+
candidateSizeSelect.setAttribute("aria-label", COPY.minimum_file_size)
|
|
1617
|
+
candidateSizeSelect.innerHTML = candidateSizeOptions
|
|
1618
|
+
.map((size) => `<option value="${size}">${fmt(size)}+</option>`)
|
|
1619
|
+
.join("")
|
|
1620
|
+
candidateSizeSelect.value = String(candidateSizeOptions[2])
|
|
1621
|
+
try {
|
|
1622
|
+
const storedCandidateSize = Number(localStorage.getItem(candidateSizeKey))
|
|
1623
|
+
if (candidateSizeOptions.includes(storedCandidateSize)) {
|
|
1624
|
+
candidateSizeSelect.value = String(storedCandidateSize)
|
|
1625
|
+
}
|
|
1626
|
+
} catch (error) {}
|
|
1593
1627
|
const addSourceButton = el("btn-add-source")
|
|
1594
1628
|
if (addSourceButton) {
|
|
1595
1629
|
addSourceButton.setAttribute("aria-label", COPY.add_external_folder)
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
<section class='vault-overview'>
|
|
5
5
|
<div class='vault-metrics' id='vault-metrics'></div>
|
|
6
6
|
<div class='vault-overview-actions'>
|
|
7
|
+
<select class='vault-select' id='vault-candidate-size'></select>
|
|
7
8
|
<button class='vault-button' id='btn-scan' type='button'></button>
|
|
8
9
|
<% if (!appMode) { %>
|
|
9
10
|
<details class='vault-advanced' id='vault-advanced'>
|
package/test/vault-ui.test.js
CHANGED
|
@@ -7,6 +7,7 @@ const crypto = require('crypto')
|
|
|
7
7
|
const ejs = require('ejs')
|
|
8
8
|
const { JSDOM } = require('jsdom')
|
|
9
9
|
const Vault = require('../kernel/vault')
|
|
10
|
+
const { CANDIDATE_SIZE_OPTIONS } = require('../kernel/vault/constants')
|
|
10
11
|
|
|
11
12
|
const sha256 = (buf) => crypto.createHash('sha256').update(buf).digest('hex')
|
|
12
13
|
|
|
@@ -66,6 +67,29 @@ describe('vault dashboard backend (phase 4)', () => {
|
|
|
66
67
|
return { content, hash, a, b }
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
test('scan requests accept only supported minimum file sizes', async () => {
|
|
71
|
+
const { vault } = await makeEnv()
|
|
72
|
+
let startedWith = null
|
|
73
|
+
vault.startScan = (scopeId, sizeThreshold) => {
|
|
74
|
+
startedWith = { scopeId, sizeThreshold }
|
|
75
|
+
return { started: true }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const accepted = await vault.perform('scan', {
|
|
79
|
+
candidate_size: CANDIDATE_SIZE_OPTIONS[1]
|
|
80
|
+
})
|
|
81
|
+
assert.deepStrictEqual(accepted, { started: true })
|
|
82
|
+
assert.deepStrictEqual(startedWith, {
|
|
83
|
+
scopeId: null,
|
|
84
|
+
sizeThreshold: CANDIDATE_SIZE_OPTIONS[1]
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
startedWith = null
|
|
88
|
+
const rejected = await vault.perform('scan', { candidate_size: 1 })
|
|
89
|
+
assert.deepStrictEqual(rejected, { error: 'Choose a valid minimum file size.' })
|
|
90
|
+
assert.strictEqual(startedWith, null)
|
|
91
|
+
})
|
|
92
|
+
|
|
69
93
|
test('item 15: undo of a conversion batch restores independent files', async () => {
|
|
70
94
|
const { home, vault } = await makeEnv()
|
|
71
95
|
const { content, a, b } = await makeSharedPair(home, vault)
|
|
@@ -1090,6 +1114,9 @@ describe('vault dashboard backend (phase 4)', () => {
|
|
|
1090
1114
|
assert.match(vaultPage, /\.vault-button\.primary \{[\s\S]*?background:\s*var\(--task-accent-contrast\)[\s\S]*?color:\s*#101828/)
|
|
1091
1115
|
assert.match(vaultPage, /id="btn-review-result"/)
|
|
1092
1116
|
assert.match(vaultPage, /class='vault-button' id='btn-scan'/)
|
|
1117
|
+
assert.match(vaultPage, /id='vault-candidate-size'/)
|
|
1118
|
+
assert.match(vaultPage, /candidate_size:\s*candidateSize\(\)/)
|
|
1119
|
+
assert.match(vaultPage, /localStorage\.setItem\(candidateSizeKey/)
|
|
1093
1120
|
assert.match(vaultPage, /id='vault-storage-details'/)
|
|
1094
1121
|
assert.match(vaultPage, /fmt\(data\.lifetime_bytes_saved\)/)
|
|
1095
1122
|
assert.match(vaultPage, /repair_index:\s*"Repair index"/)
|
|
@@ -1201,6 +1228,18 @@ describe('vault dashboard backend (phase 4)', () => {
|
|
|
1201
1228
|
assert.ok(appTemplate.indexOf("id='save-space-tab'") < appTemplate.indexOf('class="app-autolaunch"'))
|
|
1202
1229
|
assert.match(globalTemplate, /include\('partials\/vault_workspace', \{ appMode: false \}\)/)
|
|
1203
1230
|
assert.match(embeddedTemplate, /include\('partials\/vault_workspace', \{ appMode: true \}\)/)
|
|
1231
|
+
const vaultStyles = await fs.promises.readFile(
|
|
1232
|
+
path.resolve(__dirname, '..', 'server', 'public', 'vault.css'), 'utf8')
|
|
1233
|
+
assert.match(vaultStyles, /\.vault-embed-main \.vault-shell \{[\s\S]*?height:\s*100%;[\s\S]*?\}/)
|
|
1234
|
+
const compactStart = vaultStyles.indexOf('@media (max-width: 820px)')
|
|
1235
|
+
const compactEnd = vaultStyles.indexOf('@media (pointer: coarse)', compactStart)
|
|
1236
|
+
assert.ok(compactStart >= 0 && compactEnd > compactStart)
|
|
1237
|
+
const compactStyles = vaultStyles.slice(compactStart, compactEnd)
|
|
1238
|
+
assert.match(compactStyles, /\.vault-embed-main \{ overflow-y: auto; \}/)
|
|
1239
|
+
assert.match(compactStyles, /\.vault-embed-main \.vault-shell \{ height: auto; \}/)
|
|
1240
|
+
const vaultScript = await fs.promises.readFile(
|
|
1241
|
+
path.resolve(__dirname, '..', 'server', 'public', 'vault.js'), 'utf8')
|
|
1242
|
+
assert.match(vaultScript, /el\("vault-explorer"\)\.style\.display = ""/)
|
|
1204
1243
|
|
|
1205
1244
|
const html = await ejs.renderFile(path.resolve(views, 'vault_app.ejs'), {
|
|
1206
1245
|
theme: 'light', platform: 'darwin', agent: 'electron', scope_id: 'app:appB'
|
|
@@ -1282,6 +1321,15 @@ describe('vault dashboard backend (phase 4)', () => {
|
|
|
1282
1321
|
|
|
1283
1322
|
assert.deepStrictEqual(requests, ['/info/dedup?scope_id=app%3AappB'])
|
|
1284
1323
|
assert.match(dom.window.document.getElementById('btn-scan').textContent, /Scan this app/)
|
|
1324
|
+
const candidateSize = dom.window.document.getElementById('vault-candidate-size')
|
|
1325
|
+
assert.deepStrictEqual([...candidateSize.options].map((option) => option.textContent),
|
|
1326
|
+
['10 MB+', '50 MB+', '100 MB+', '500 MB+', '1 GB+'])
|
|
1327
|
+
assert.strictEqual(candidateSize.value, '100000000')
|
|
1328
|
+
candidateSize.value = '50000000'
|
|
1329
|
+
candidateSize.dispatchEvent(new dom.window.Event('change', { bubbles: true }))
|
|
1330
|
+
assert.strictEqual(dom.window.localStorage.getItem('pinokio:vault:candidate-size'), '50000000')
|
|
1331
|
+
assert.match(dom.window.document.getElementById('vault-pane-footer').textContent,
|
|
1332
|
+
/Only files 50 MB and larger/)
|
|
1285
1333
|
assert.strictEqual(dom.window.document.querySelector('[data-source="app:appB"]').getAttribute('aria-current'), 'page')
|
|
1286
1334
|
assert.strictEqual(dom.window.document.getElementById('btn-add-source'), null)
|
|
1287
1335
|
assert.strictEqual(dom.window.document.getElementById('btn-repair'), null)
|