pinokiod 8.0.110 → 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.
@@ -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
@@ -1227,6 +1236,19 @@ class Vault {
1227
1236
  }
1228
1237
  }
1229
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
+
1230
1252
  sourceIsWithinScope(source, scopeId) {
1231
1253
  if (!scopeId) return true
1232
1254
  const seen = new Set()
@@ -2025,7 +2047,10 @@ class Vault {
2025
2047
  const selection = this.separateSelection(payload)
2026
2048
  if (selection.error) return { error: selection.error }
2027
2049
  const total = await this.registry.matchingFileSummary(
2028
- "linked", selection.sourceIds, selection.query)
2050
+ "linked",
2051
+ selection.sourceIds,
2052
+ selection.query,
2053
+ selection.nameOnly)
2029
2054
  if (!total.count) {
2030
2055
  return { error: "No matching deduplicated files remain." }
2031
2056
  }
@@ -2586,7 +2611,10 @@ class Vault {
2586
2611
  }
2587
2612
  return {
2588
2613
  sourceIds,
2589
- 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"
2590
2618
  }
2591
2619
  }
2592
2620
 
@@ -2954,7 +2982,8 @@ class Vault {
2954
2982
  selection.sourceIds,
2955
2983
  cursor,
2956
2984
  100,
2957
- selection.query
2985
+ selection.query,
2986
+ selection.nameOnly
2958
2987
  )
2959
2988
  if (!rows.length) break
2960
2989
  const affectedHashes = new Set()
@@ -3303,6 +3332,181 @@ class Vault {
3303
3332
  }, this.locationForPath(row.path, row.source_id)))
3304
3333
  }
3305
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
+
3306
3510
  async duplicateGroupChildren(scopeId, hash, options = {}) {
3307
3511
  if (typeof hash !== "string" || !SHA256_RE.test(hash)) {
3308
3512
  throw new TypeError("Invalid vault content identifier.")
@@ -3326,7 +3530,9 @@ class Vault {
3326
3530
  cursor,
3327
3531
  pageSize,
3328
3532
  unrestricted: !scopeId,
3329
- activeUnrestricted: !scopeId && !locationId
3533
+ activeUnrestricted: !scopeId && !locationId,
3534
+ // Content groups are only ever listed by the flat file view.
3535
+ nameOnly: true
3330
3536
  })
3331
3537
  return {
3332
3538
  hash,
@@ -3389,7 +3595,8 @@ class Vault {
3389
3595
  sourceIds: this.scopeSourceIds(scopeId, locationId),
3390
3596
  externalSourceIds: this.configuredExternalSourceIds(),
3391
3597
  query: String(options.query || "").slice(0, 500).trim(),
3392
- unrestricted: !scopeId && !locationId
3598
+ unrestricted: !scopeId && !locationId,
3599
+ nameOnly: true
3393
3600
  })
3394
3601
  return {
3395
3602
  hash,
@@ -3416,7 +3623,8 @@ class Vault {
3416
3623
  options.page_size, STATUS_PAGE_SIZE, 1, STATUS_PAGE_SIZE),
3417
3624
  sizeSort: options.size_sort || "desc",
3418
3625
  unrestricted: !scopeId && !locationId,
3419
- authorizedUnrestricted: !scopeId
3626
+ authorizedUnrestricted: !scopeId,
3627
+ nameOnly: true
3420
3628
  })
3421
3629
  return {
3422
3630
  items: result.items || [],
@@ -3429,6 +3637,9 @@ class Vault {
3429
3637
  const view = STATUS_VIEWS.has(options.view) ? options.view : "all"
3430
3638
  const groupDuplicates =
3431
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"
3432
3643
  const statusFilter = STATUS_FILTERS.has(options.status_filter)
3433
3644
  ? options.status_filter
3434
3645
  : "all"
@@ -3464,6 +3675,8 @@ class Vault {
3464
3675
  ? options.size_sort || "desc"
3465
3676
  : options.size_sort,
3466
3677
  groupDuplicates,
3678
+ nameOnly: filesMode,
3679
+ nameSort: options.name_sort === "desc" ? "desc" : "asc",
3467
3680
  cursor
3468
3681
  })
3469
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) {