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: 100 * 1024 * 1024,
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,
@@ -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, DIR_CONCURRENCY, STAT_CONCURRENCY,
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
- return this.startScan(payload.scope_id || null)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "8.0.46",
3
+ "version": "8.0.47",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -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 { width: 100%; }
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; }
@@ -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 100 MB and larger appear here. Files keep their current locations.",
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 = "grid"
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" ? COPY.reclaimable_note : COPY.tracked_note
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({ action: "scan", scope_id: SCOPE_ID })
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'>
@@ -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)