pinokiod 8.0.108 → 8.0.111

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,8 +1,8 @@
1
1
  const fs = require("fs")
2
2
  const path = require("path")
3
3
  const crypto = require("crypto")
4
- const { execFile } = require("child_process")
5
4
  const { Worker } = require("worker_threads")
5
+ const Util = require("../util")
6
6
  const Registry = require("./registry")
7
7
  const Scanner = require("./scanner")
8
8
  const Sweeper = require("./sweeper")
@@ -150,6 +150,15 @@ const sameFileMetadata = (left, right) => {
150
150
  }
151
151
 
152
152
  const sourceId = (kind, name) => `${kind}:${encodeURIComponent(name)}`
153
+ const commonDirectory = (left, right) => {
154
+ let index = 0
155
+ while (index < left.length &&
156
+ index < right.length &&
157
+ left[index] === right[index]) index += 1
158
+ const shared = left.slice(0, index)
159
+ const cut = shared.lastIndexOf("/")
160
+ return cut < 0 ? "" : shared.slice(0, cut)
161
+ }
153
162
  const boundedInteger = (value, fallback, minimum, maximum) => {
154
163
  const parsed = Number(value)
155
164
  if (!Number.isSafeInteger(parsed)) return fallback
@@ -188,41 +197,11 @@ class FileActionChanges {
188
197
  }
189
198
  }
190
199
 
191
- const revealInFileManager = (filePath, platform = process.platform) =>
192
- new Promise((resolve, reject) => {
193
- const command = platform === "darwin"
194
- ? "open"
195
- : platform === "win32"
196
- ? "explorer.exe"
197
- : "xdg-open"
198
- const args = platform === "darwin"
199
- ? ["-R", filePath]
200
- : platform === "win32"
201
- ? [`/select,${filePath}`]
202
- : [path.dirname(filePath)]
203
- execFile(command, args, {
204
- timeout: 10000,
205
- windowsHide: true
206
- }, (error) => {
207
- // Explorer reports a non-zero exit code even when it opens the folder,
208
- // so an exit status alone is not a failure on Windows. A missing binary
209
- // or a refused spawn still is.
210
- if (error && platform === "win32" && Number.isInteger(error.code)) {
211
- resolve()
212
- return
213
- }
214
- error ? reject(error) : resolve()
215
- })
216
- })
217
-
218
200
  class Vault {
219
201
  constructor(kernel) {
220
202
  this.kernel = kernel
221
203
  this.fileManagerLauncher = (filePath) =>
222
- revealInFileManager(
223
- filePath,
224
- this.kernel.platform || process.platform
225
- )
204
+ Util.openfs(filePath, { action: "view" }, this.kernel)
226
205
  this.enabled = false
227
206
  this.initialized = false
228
207
  this.mode = null
@@ -1257,6 +1236,19 @@ class Vault {
1257
1236
  }
1258
1237
  }
1259
1238
 
1239
+ sourceLabelChain(source) {
1240
+ if (!source) return ""
1241
+ const parts = [source.label]
1242
+ const seen = new Set([source.id])
1243
+ let current = source
1244
+ while (current && current.parent_id && !seen.has(current.parent_id)) {
1245
+ seen.add(current.parent_id)
1246
+ current = this._sourcesById.get(current.parent_id)
1247
+ if (current) parts.unshift(current.label)
1248
+ }
1249
+ return parts.join(" / ")
1250
+ }
1251
+
1260
1252
  sourceIsWithinScope(source, scopeId) {
1261
1253
  if (!scopeId) return true
1262
1254
  const seen = new Set()
@@ -2055,7 +2047,10 @@ class Vault {
2055
2047
  const selection = this.separateSelection(payload)
2056
2048
  if (selection.error) return { error: selection.error }
2057
2049
  const total = await this.registry.matchingFileSummary(
2058
- "linked", selection.sourceIds, selection.query)
2050
+ "linked",
2051
+ selection.sourceIds,
2052
+ selection.query,
2053
+ selection.nameOnly)
2059
2054
  if (!total.count) {
2060
2055
  return { error: "No matching deduplicated files remain." }
2061
2056
  }
@@ -2616,7 +2611,10 @@ class Vault {
2616
2611
  }
2617
2612
  return {
2618
2613
  sourceIds,
2619
- query: String(payload.query || "").slice(0, 500).trim()
2614
+ query: String(payload.query || "").slice(0, 500).trim(),
2615
+ // Separating "all matching" has to mean the rows the search listed, so
2616
+ // it repeats the filter the view was using.
2617
+ nameOnly: payload.display_mode === "files"
2620
2618
  }
2621
2619
  }
2622
2620
 
@@ -2984,7 +2982,8 @@ class Vault {
2984
2982
  selection.sourceIds,
2985
2983
  cursor,
2986
2984
  100,
2987
- selection.query
2985
+ selection.query,
2986
+ selection.nameOnly
2988
2987
  )
2989
2988
  if (!rows.length) break
2990
2989
  const affectedHashes = new Set()
@@ -3320,24 +3319,194 @@ class Vault {
3320
3319
  }
3321
3320
 
3322
3321
  publicDuplicateChildItems(rows) {
3323
- const publicStatus = {
3324
- reference: "tracked",
3325
- duplicate: "duplicate",
3326
- linked: "shared"
3327
- }
3328
3322
  return (rows || []).map((row) => Object.assign({
3329
3323
  kind: "duplicate_path",
3330
3324
  path: row.path,
3331
3325
  hash: row.hash,
3332
3326
  size: Number(row.size) || 0,
3333
3327
  app: row.app || null,
3334
- status: publicStatus[row.status],
3328
+ status: PUBLIC_FILE_STATUS[row.status],
3335
3329
  registry_status: row.status,
3336
3330
  shareable: row.status === "duplicate",
3337
3331
  selectable: !!row.selectable
3338
3332
  }, this.locationForPath(row.path, row.source_id)))
3339
3333
  }
3340
3334
 
3335
+ // One level of the folder tree. Subfolders come back with the totals for
3336
+ // everything inside them, which is what makes sorting a level by size mean
3337
+ // anything; files sitting directly in the folder come back as normal rows.
3338
+ async treeEntries(scopeId, options = {}) {
3339
+ if (!this.enabled || !this.registry) return { enabled: false }
3340
+ const locationId = typeof options.location_id === "string" &&
3341
+ options.location_id
3342
+ ? options.location_id
3343
+ : null
3344
+ const view = STATUS_VIEWS.has(options.view) ? options.view : "all"
3345
+ const statusFilter = STATUS_FILTERS.has(options.status_filter)
3346
+ ? options.status_filter
3347
+ : "all"
3348
+ const query = String(options.query || "").slice(0, 500).trim()
3349
+ const sizeSort = options.size_sort === "asc" || options.size_sort === "desc"
3350
+ ? options.size_sort
3351
+ : null
3352
+ const nameSort = options.name_sort === "desc" ? "desc" : "asc"
3353
+ // The authorised set, not the requested id: a scoped page must not be able
3354
+ // to read another location by naming it, and a rail entry standing for a
3355
+ // group of locations -- Pinokio, Apps -- owns no files of its own, so it
3356
+ // resolves to its descendants rather than to an empty tree.
3357
+ const sourceIds = this.scopeSourceIds(scopeId, locationId)
3358
+ const source = sourceIds.length === 1
3359
+ ? this._sourcesById.get(sourceIds[0])
3360
+ : null
3361
+ // A group of locations, or none chosen at all, is a tree of locations.
3362
+ if (!source || !source.root) {
3363
+ const rows = await this.registry.treeLocationRows({
3364
+ sourceIds,
3365
+ unrestricted: !scopeId && !locationId,
3366
+ externalSourceIds: this.configuredExternalSourceIds(),
3367
+ view,
3368
+ statusFilter,
3369
+ query,
3370
+ nameOnly: false
3371
+ })
3372
+ const items = rows
3373
+ .map((row) => {
3374
+ const entry = this._sourcesById.get(row.source_id)
3375
+ if (!entry) return null
3376
+ return {
3377
+ kind: "location",
3378
+ name: entry.label,
3379
+ label: this.sourceLabelChain(entry),
3380
+ relative_path: "",
3381
+ location_id: entry.id,
3382
+ source_id: entry.id,
3383
+ size: Number(row.bytes) || 0,
3384
+ file_count: Number(row.file_count) || 0,
3385
+ duplicate_count: Number(row.duplicate_count) || 0,
3386
+ unavailable_count: Number(row.unavailable_count) || 0,
3387
+ linked_count: Number(row.linked_count) || 0
3388
+ }
3389
+ })
3390
+ .filter(Boolean)
3391
+ items.sort((left, right) => {
3392
+ if (sizeSort === "asc" && left.size !== right.size) return left.size - right.size
3393
+ if (sizeSort === "desc" && left.size !== right.size) return right.size - left.size
3394
+ const order = left.label.localeCompare(right.label)
3395
+ return nameSort === "desc" ? -order : order
3396
+ })
3397
+ return {
3398
+ enabled: true,
3399
+ parent: "",
3400
+ source_id: null,
3401
+ items,
3402
+ has_next: false,
3403
+ next_cursor: null
3404
+ }
3405
+ }
3406
+ const parent = String(options.parent || "")
3407
+ .replace(/^[\\/]+/, "")
3408
+ .replace(/[\\/]+$/, "")
3409
+ .slice(0, 4096)
3410
+ const root = source.root.replace(/[\\/]+$/, "")
3411
+ const separator = root.includes("\\") && !root.includes("/") ? "\\" : "/"
3412
+ const prefix = parent
3413
+ ? `${root}${separator}${parent.split("/").join(separator)}${separator}`
3414
+ : `${root}${separator}`
3415
+ const pageSize = boundedInteger(
3416
+ options.page_size, STATUS_PAGE_SIZE, 1, STATUS_PAGE_SIZE)
3417
+ const directoryOffset = boundedInteger(
3418
+ options.directory_offset, 0, 0, Number.MAX_SAFE_INTEGER)
3419
+ const level = await this.registry.treeLevel({
3420
+ sourceId: source.id,
3421
+ prefix,
3422
+ view,
3423
+ statusFilter,
3424
+ query,
3425
+ nameOnly: false,
3426
+ sizeSort,
3427
+ nameSort,
3428
+ cursor: typeof options.cursor === "string"
3429
+ ? options.cursor.slice(0, 2048)
3430
+ : "",
3431
+ pageSize,
3432
+ // One extra of each so the merge can tell whether a side has more.
3433
+ directoryLimit: pageSize + 1,
3434
+ directoryOffset,
3435
+ externalSourceIds: this.configuredExternalSourceIds()
3436
+ })
3437
+ const directories = (level.directories || []).map((row) => {
3438
+ // A run of folders holding nothing but one another reads as one row.
3439
+ // The shared prefix of the first and last path underneath is exactly
3440
+ // that run, because the two are the extremes of a sorted list.
3441
+ const shared = commonDirectory(row.lo || "", row.hi || "")
3442
+ const label = shared && shared.startsWith(`${row.name}/`)
3443
+ ? shared
3444
+ : row.name
3445
+ return {
3446
+ kind: "directory",
3447
+ name: row.name,
3448
+ label,
3449
+ relative_path: parent ? `${parent}/${label}` : label,
3450
+ source_id: source.id,
3451
+ size: Number(row.bytes) || 0,
3452
+ file_count: Number(row.file_count) || 0,
3453
+ duplicate_count: Number(row.duplicate_count) || 0,
3454
+ unavailable_count: Number(row.unavailable_count) || 0,
3455
+ linked_count: Number(row.linked_count) || 0
3456
+ }
3457
+ })
3458
+ const files = this.publicFileItems(level.files)
3459
+ .map((item) => Object.assign({ kind: "file" }, item))
3460
+ const fileCursors = (level.files && level.files.rowCursors) || []
3461
+ // Both sides arrive sorted the same way, so walking them together gives the
3462
+ // page its true order. Sorting by size interleaves folders and files: the
3463
+ // question on screen is "what is biggest", and a large loose file buried
3464
+ // under small folders is the wrong answer to it. Sorting by name keeps the
3465
+ // familiar folders-first grouping.
3466
+ const ahead = (left, right) => {
3467
+ if (sizeSort) {
3468
+ if (left.size !== right.size) {
3469
+ return sizeSort === "asc"
3470
+ ? left.size < right.size
3471
+ : left.size > right.size
3472
+ }
3473
+ } else if ((left.kind === "directory") !== (right.kind === "directory")) {
3474
+ return left.kind === "directory"
3475
+ }
3476
+ const order = String(left.label || left.relative_path || "")
3477
+ .localeCompare(String(right.label || right.relative_path || ""))
3478
+ return nameSort === "desc" ? order > 0 : order < 0
3479
+ }
3480
+ const items = []
3481
+ let directoryIndex = 0
3482
+ let fileIndex = 0
3483
+ while (items.length < pageSize &&
3484
+ (directoryIndex < directories.length || fileIndex < files.length)) {
3485
+ const directory = directories[directoryIndex]
3486
+ const file = files[fileIndex]
3487
+ if (directory && (!file || ahead(directory, file))) {
3488
+ items.push(directory)
3489
+ directoryIndex += 1
3490
+ } else if (file) {
3491
+ items.push(file)
3492
+ fileIndex += 1
3493
+ } else break
3494
+ }
3495
+ return {
3496
+ enabled: true,
3497
+ parent,
3498
+ source_id: source.id,
3499
+ items,
3500
+ has_next: directoryIndex < directories.length ||
3501
+ fileIndex < files.length ||
3502
+ !!(level.files && level.files.nextCursor),
3503
+ next_directory_offset: directoryOffset + directoryIndex,
3504
+ next_cursor: fileIndex > 0
3505
+ ? fileCursors[fileIndex - 1] || null
3506
+ : (typeof options.cursor === "string" ? options.cursor : null)
3507
+ }
3508
+ }
3509
+
3341
3510
  async duplicateGroupChildren(scopeId, hash, options = {}) {
3342
3511
  if (typeof hash !== "string" || !SHA256_RE.test(hash)) {
3343
3512
  throw new TypeError("Invalid vault content identifier.")
@@ -3361,7 +3530,9 @@ class Vault {
3361
3530
  cursor,
3362
3531
  pageSize,
3363
3532
  unrestricted: !scopeId,
3364
- activeUnrestricted: !scopeId && !locationId
3533
+ activeUnrestricted: !scopeId && !locationId,
3534
+ // Content groups are only ever listed by the flat file view.
3535
+ nameOnly: true
3365
3536
  })
3366
3537
  return {
3367
3538
  hash,
@@ -3424,7 +3595,8 @@ class Vault {
3424
3595
  sourceIds: this.scopeSourceIds(scopeId, locationId),
3425
3596
  externalSourceIds: this.configuredExternalSourceIds(),
3426
3597
  query: String(options.query || "").slice(0, 500).trim(),
3427
- unrestricted: !scopeId && !locationId
3598
+ unrestricted: !scopeId && !locationId,
3599
+ nameOnly: true
3428
3600
  })
3429
3601
  return {
3430
3602
  hash,
@@ -3451,7 +3623,8 @@ class Vault {
3451
3623
  options.page_size, STATUS_PAGE_SIZE, 1, STATUS_PAGE_SIZE),
3452
3624
  sizeSort: options.size_sort || "desc",
3453
3625
  unrestricted: !scopeId && !locationId,
3454
- authorizedUnrestricted: !scopeId
3626
+ authorizedUnrestricted: !scopeId,
3627
+ nameOnly: true
3455
3628
  })
3456
3629
  return {
3457
3630
  items: result.items || [],
@@ -3464,6 +3637,9 @@ class Vault {
3464
3637
  const view = STATUS_VIEWS.has(options.view) ? options.view : "all"
3465
3638
  const groupDuplicates =
3466
3639
  view === "duplicates" && options.display_mode === "files"
3640
+ // The flat list shows a file name where the tree shows a folder path, so
3641
+ // the search has to match what is on screen. See queryClause().
3642
+ const filesMode = options.display_mode === "files"
3467
3643
  const statusFilter = STATUS_FILTERS.has(options.status_filter)
3468
3644
  ? options.status_filter
3469
3645
  : "all"
@@ -3499,6 +3675,8 @@ class Vault {
3499
3675
  ? options.size_sort || "desc"
3500
3676
  : options.size_sort,
3501
3677
  groupDuplicates,
3678
+ nameOnly: filesMode,
3679
+ nameSort: options.name_sort === "desc" ? "desc" : "asc",
3502
3680
  cursor
3503
3681
  })
3504
3682
  let items
@@ -210,6 +210,8 @@ for (const method of [
210
210
  "duplicateGroupSelection",
211
211
  "duplicateGroupPageSelection",
212
212
  "statusSnapshot",
213
+ "treeLevel",
214
+ "treeLocationRows",
213
215
  "clearFiles"
214
216
  ]) {
215
217
  Registry.prototype[method] = function (...args) {