zerobucket 0.7.0 → 0.8.0

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/dist/index.cjs CHANGED
@@ -2014,11 +2014,60 @@ var CHUNK2 = `SELECT SUBSTRING(data FROM ? FOR ?) AS chunk FROM zerobucket_image
2014
2014
  var EXISTS3 = `SELECT 1 FROM zerobucket_images WHERE id = ?`;
2015
2015
  var LOCK_ONE = `SELECT storage_backend, object_storage_key FROM zerobucket_images WHERE id = ? FOR UPDATE`;
2016
2016
  var DELETE_ONE2 = `DELETE FROM zerobucket_images WHERE id = ?`;
2017
- var LOCK_MANY = `SELECT id, storage_backend, object_storage_key FROM zerobucket_images WHERE id IN (?) ORDER BY id FOR UPDATE`;
2018
- var DELETE_MANY2 = `DELETE FROM zerobucket_images WHERE id IN (?)`;
2017
+ var LOCK_MANY = `SELECT id, storage_backend, object_storage_key FROM zerobucket_images FORCE INDEX (PRIMARY) WHERE id IN (?) ORDER BY id FOR UPDATE`;
2019
2018
  var SELECT_FOR_TIERING3 = `SELECT data, mime_type, size_bytes, storage_backend FROM zerobucket_images WHERE id = ? FOR UPDATE`;
2020
2019
  var UPDATE_AFTER_TIERING3 = `UPDATE zerobucket_images SET data = NULL, storage_backend = 'object_storage', object_storage_bucket = ?, object_storage_key = ?, updated_at = ? WHERE id = ?`;
2021
2020
  var LOCAL = "mysql";
2021
+ var DEDUP_BLOBS_SCHEMA = `
2022
+ CREATE TABLE IF NOT EXISTS zerobucket_blobs (
2023
+ checksum_sha256 CHAR(64) PRIMARY KEY,
2024
+ data LONGBLOB NOT NULL,
2025
+ mime_type VARCHAR(255) NOT NULL,
2026
+ size_bytes INT NOT NULL,
2027
+ width INT,
2028
+ height INT,
2029
+ ref_count INT NOT NULL DEFAULT 0,
2030
+ created_at DATETIME(6) NOT NULL
2031
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`;
2032
+ var DEDUP_REFS_SCHEMA = `
2033
+ CREATE TABLE IF NOT EXISTS zerobucket_image_refs (
2034
+ id CHAR(36) PRIMARY KEY,
2035
+ checksum_sha256 CHAR(64) NOT NULL,
2036
+ original_filename TEXT,
2037
+ created_at DATETIME(6) NOT NULL,
2038
+ updated_at DATETIME(6) NOT NULL,
2039
+ INDEX idx_zerobucket_image_refs_checksum (checksum_sha256),
2040
+ INDEX idx_zerobucket_image_refs_created_at (created_at),
2041
+ FOREIGN KEY (checksum_sha256) REFERENCES zerobucket_blobs(checksum_sha256)
2042
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`;
2043
+ function upsertBlobSql(n) {
2044
+ if (!Number.isInteger(n) || n < 1) throw new RangeError("invalid ref count");
2045
+ return `INSERT INTO zerobucket_blobs (checksum_sha256, data, mime_type, size_bytes, width, height, ref_count, created_at) VALUES (?, ?, ?, ?, ?, ?, ${n}, ?) ON DUPLICATE KEY UPDATE ref_count = ref_count + ${n}`;
2046
+ }
2047
+ var D_INSERT_REF3 = `INSERT INTO zerobucket_image_refs (id, checksum_sha256, original_filename, created_at, updated_at) VALUES (?, ?, ?, ?, ?)`;
2048
+ var D_COLS3 = `r.id, b.data, b.mime_type, r.original_filename, b.size_bytes, b.width, b.height, r.checksum_sha256, '${LOCAL}' AS storage_backend, NULL AS object_storage_bucket, NULL AS object_storage_key`;
2049
+ var D_FROM3 = `FROM zerobucket_image_refs r JOIN zerobucket_blobs b ON r.checksum_sha256 = b.checksum_sha256`;
2050
+ var D_LOCK_REF = `SELECT checksum_sha256 FROM zerobucket_image_refs WHERE id = ? FOR UPDATE`;
2051
+ var D_DELETE_REF3 = `DELETE FROM zerobucket_image_refs WHERE id = ?`;
2052
+ var D_LOCK_REFS = `SELECT id, checksum_sha256 FROM zerobucket_image_refs FORCE INDEX (PRIMARY) WHERE id IN (?) ORDER BY id FOR UPDATE`;
2053
+ var D_DECREMENT3 = `UPDATE zerobucket_blobs SET ref_count = ref_count - ? WHERE checksum_sha256 = ?`;
2054
+ var D_DELETE_EMPTY3 = `DELETE FROM zerobucket_blobs WHERE checksum_sha256 = ? AND ref_count <= 0`;
2055
+ var CLASSIC_SQL2 = {
2056
+ selectFull: SELECT_FULL3,
2057
+ selectMany: SELECT_MANY2,
2058
+ selectMetadata: SELECT_METADATA3,
2059
+ streamInfo: STREAM_INFO2,
2060
+ chunk: CHUNK2,
2061
+ exists: EXISTS3
2062
+ };
2063
+ var DEDUP_SQL2 = {
2064
+ selectFull: `SELECT ${D_COLS3} ${D_FROM3} WHERE r.id = ?`,
2065
+ selectMany: `SELECT ${D_COLS3} ${D_FROM3} WHERE r.id IN (?)`,
2066
+ selectMetadata: `SELECT r.id, b.mime_type, r.original_filename, b.size_bytes, b.width, b.height, r.checksum_sha256 ${D_FROM3} WHERE r.id = ?`,
2067
+ streamInfo: `SELECT b.size_bytes, '${LOCAL}' AS storage_backend, NULL AS object_storage_key ${D_FROM3} WHERE r.id = ?`,
2068
+ chunk: `SELECT SUBSTRING(b.data FROM ? FOR ?) AS chunk ${D_FROM3} WHERE r.id = ?`,
2069
+ exists: `SELECT 1 FROM zerobucket_image_refs WHERE id = ?`
2070
+ };
2022
2071
  var RETRYABLE_ERRNOS = /* @__PURE__ */ new Set([
2023
2072
  1205,
2024
2073
  // ER_LOCK_WAIT_TIMEOUT
@@ -2108,11 +2157,13 @@ var MySQLBackend = class {
2108
2157
  "MySQLBackend requires either `connectionString` or `pool`"
2109
2158
  );
2110
2159
  }
2111
- if (opts.dedup) {
2160
+ this.dedup = opts.dedup ?? false;
2161
+ if (this.dedup && opts.objectStorage) {
2112
2162
  throw new TypeError(
2113
- "dedup mode is not available for MySQL in the npm package yet (the Python package supports it). Use classic mode, or Postgres/SQLite for dedup."
2163
+ "objectStorage is not supported together with dedup: true (same restriction as the Python package). Tiering moves one image's bytes, but a deduplicated blob can be shared by many images."
2114
2164
  );
2115
2165
  }
2166
+ this.sql = this.dedup ? DEDUP_SQL2 : CLASSIC_SQL2;
2116
2167
  this.ownsPool = !opts.pool;
2117
2168
  this.pool = opts.pool;
2118
2169
  this.maxRetries = opts.maxRetries ?? 3;
@@ -2129,6 +2180,8 @@ var MySQLBackend = class {
2129
2180
  retryBaseDelayMs;
2130
2181
  poolTimeoutMs;
2131
2182
  autoMigrate;
2183
+ dedup;
2184
+ sql;
2132
2185
  // ---- lifecycle -------------------------------------------------------
2133
2186
  ready() {
2134
2187
  if (this.closed)
@@ -2194,6 +2247,10 @@ var MySQLBackend = class {
2194
2247
  );
2195
2248
  }
2196
2249
  try {
2250
+ if (this.dedup) {
2251
+ await this.migrateDedup(q);
2252
+ return;
2253
+ }
2197
2254
  const tableExists = (await q.query(
2198
2255
  "SELECT 1 FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'zerobucket_images'"
2199
2256
  ))[0].length > 0;
@@ -2235,6 +2292,16 @@ var MySQLBackend = class {
2235
2292
  });
2236
2293
  }
2237
2294
  }
2295
+ /** Dedup tables, created only if missing (same no-needless-DDL rule as the classic table). */
2296
+ async migrateDedup(q) {
2297
+ const have = new Set(
2298
+ (await q.query(
2299
+ "SELECT TABLE_NAME AS t FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN ('zerobucket_blobs', 'zerobucket_image_refs')"
2300
+ ))[0].map((r) => r.t)
2301
+ );
2302
+ if (!have.has("zerobucket_blobs")) await q.query(DEDUP_BLOBS_SCHEMA);
2303
+ if (!have.has("zerobucket_image_refs")) await q.query(DEDUP_REFS_SCHEMA);
2304
+ }
2238
2305
  async close() {
2239
2306
  this.closed = true;
2240
2307
  const pool = this.pool;
@@ -2400,6 +2467,19 @@ var MySQLBackend = class {
2400
2467
  async put(row, { connection } = {}) {
2401
2468
  try {
2402
2469
  const id = (0, import_node_crypto2.randomUUID)();
2470
+ if (this.dedup) {
2471
+ await this.run(
2472
+ "put",
2473
+ connection,
2474
+ async (q) => {
2475
+ const now = mysqlNow();
2476
+ await q.execute(upsertBlobSql(1), blobParams2(row, now));
2477
+ await q.execute(D_INSERT_REF3, refParams(id, row, now));
2478
+ },
2479
+ { tx: true }
2480
+ );
2481
+ return id;
2482
+ }
2403
2483
  await this.run("put", connection, async (q) => {
2404
2484
  await q.execute(INSERT3, rowParams2(id, row, mysqlNow()));
2405
2485
  });
@@ -2417,6 +2497,21 @@ var MySQLBackend = class {
2417
2497
  connection,
2418
2498
  async (q) => {
2419
2499
  const now = mysqlNow();
2500
+ if (this.dedup) {
2501
+ const groups = /* @__PURE__ */ new Map();
2502
+ for (const r of rows) {
2503
+ const g = groups.get(r.checksumSha256);
2504
+ if (g) g.n += 1;
2505
+ else groups.set(r.checksumSha256, { row: r, n: 1 });
2506
+ }
2507
+ for (const checksum of [...groups.keys()].sort()) {
2508
+ const g = groups.get(checksum);
2509
+ await q.execute(upsertBlobSql(g.n), blobParams2(g.row, now));
2510
+ }
2511
+ for (let i = 0; i < rows.length; i++)
2512
+ await q.execute(D_INSERT_REF3, refParams(ids[i], rows[i], now));
2513
+ return;
2514
+ }
2420
2515
  for (let i = 0; i < rows.length; i++)
2421
2516
  await q.execute(INSERT3, rowParams2(ids[i], rows[i], now));
2422
2517
  },
@@ -2434,7 +2529,7 @@ var MySQLBackend = class {
2434
2529
  row = await this.run(
2435
2530
  "get",
2436
2531
  connection,
2437
- async (q) => (await q.query(SELECT_FULL3, [id]))[0][0]
2532
+ async (q) => (await q.query(this.sql.selectFull, [id]))[0][0]
2438
2533
  );
2439
2534
  } catch (exc) {
2440
2535
  throw wrap("Failed to retrieve image", exc);
@@ -2449,7 +2544,7 @@ var MySQLBackend = class {
2449
2544
  rows = await this.run(
2450
2545
  "get_many",
2451
2546
  connection,
2452
- async (q) => (await q.query(SELECT_MANY2, [valid]))[0]
2547
+ async (q) => (await q.query(this.sql.selectMany, [valid]))[0]
2453
2548
  );
2454
2549
  } catch (exc) {
2455
2550
  throw wrap("Failed to retrieve image batch", exc);
@@ -2463,7 +2558,7 @@ var MySQLBackend = class {
2463
2558
  row = await this.run(
2464
2559
  "get_metadata",
2465
2560
  connection,
2466
- async (q) => (await q.query(SELECT_METADATA3, [id]))[0][0]
2561
+ async (q) => (await q.query(this.sql.selectMetadata, [id]))[0][0]
2467
2562
  );
2468
2563
  } catch (exc) {
2469
2564
  throw wrap("Failed to retrieve image metadata", exc);
@@ -2489,7 +2584,7 @@ var MySQLBackend = class {
2489
2584
  info = await this.run(
2490
2585
  "get_metadata",
2491
2586
  connection,
2492
- async (q) => (await q.query(STREAM_INFO2, [id]))[0][0]
2587
+ async (q) => (await q.query(this.sql.streamInfo, [id]))[0][0]
2493
2588
  );
2494
2589
  } catch (exc) {
2495
2590
  throw wrap("Failed to retrieve image metadata", exc);
@@ -2522,7 +2617,7 @@ var MySQLBackend = class {
2522
2617
  row = await self.run(
2523
2618
  "get_stream",
2524
2619
  connection,
2525
- async (q) => (await q.query(CHUNK2, [offset + 1, length, id]))[0][0]
2620
+ async (q) => (await q.query(self.sql.chunk, [offset + 1, length, id]))[0][0]
2526
2621
  );
2527
2622
  } catch (exc) {
2528
2623
  throw wrap("Failed to stream image", exc);
@@ -2547,6 +2642,7 @@ var MySQLBackend = class {
2547
2642
  }
2548
2643
  async delete(id, { connection } = {}) {
2549
2644
  if (!isValidId(id)) return false;
2645
+ if (this.dedup) return this.deleteDedup(id, connection);
2550
2646
  try {
2551
2647
  const row = await this.run(
2552
2648
  "delete",
@@ -2569,14 +2665,14 @@ var MySQLBackend = class {
2569
2665
  async deleteMany(ids, { connection } = {}) {
2570
2666
  const valid = [...new Set(ids.filter(isValidId))];
2571
2667
  if (valid.length === 0) return [];
2668
+ if (this.dedup) return this.deleteManyDedup(valid, connection);
2572
2669
  try {
2573
2670
  const rows = await this.run(
2574
2671
  "delete_many",
2575
2672
  connection,
2576
2673
  async (q) => {
2577
2674
  const found = (await q.query(LOCK_MANY, [valid]))[0];
2578
- if (found.length === 0) return [];
2579
- await q.query(DELETE_MANY2, [found.map((r) => r.id)]);
2675
+ for (const r of found) await q.query(DELETE_ONE2, [r.id]);
2580
2676
  return found;
2581
2677
  },
2582
2678
  { tx: true }
@@ -2596,12 +2692,163 @@ var MySQLBackend = class {
2596
2692
  return await this.run(
2597
2693
  "exists",
2598
2694
  connection,
2599
- async (q) => (await q.query(EXISTS3, [id]))[0].length > 0
2695
+ async (q) => (await q.query(this.sql.exists, [id]))[0].length > 0
2600
2696
  );
2601
2697
  } catch (exc) {
2602
2698
  throw wrap("Failed to check image existence", exc);
2603
2699
  }
2604
2700
  }
2701
+ // ---- maintenance (CLI) -----------------------------------------------
2702
+ /** Storage statistics, or null if the tables do not exist yet (nothing is created). */
2703
+ async getInfo() {
2704
+ try {
2705
+ return await this.run("info", void 0, async (q) => {
2706
+ const need = this.dedup ? ["zerobucket_image_refs", "zerobucket_blobs"] : ["zerobucket_images"];
2707
+ const present = new Set(
2708
+ (await q.query(
2709
+ "SELECT TABLE_NAME AS t FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN (?)",
2710
+ [need]
2711
+ ))[0].map((r) => r.t)
2712
+ );
2713
+ if (!need.every((t) => present.has(t))) return null;
2714
+ const j = this.dedup ? "FROM zerobucket_image_refs r JOIN zerobucket_blobs b ON r.checksum_sha256 = b.checksum_sha256" : "FROM zerobucket_images";
2715
+ const size = this.dedup ? "b.size_bytes" : "size_bytes";
2716
+ const created = this.dedup ? "r.created_at" : "created_at";
2717
+ const mime = this.dedup ? "b.mime_type" : "mime_type";
2718
+ const tot = (await q.query(
2719
+ `SELECT COUNT(*) AS n, COALESCE(SUM(${size}), 0) AS bytes, CAST(MIN(${created}) AS CHAR) AS oldest, CAST(MAX(${created}) AS CHAR) AS newest ${j}`
2720
+ ))[0][0];
2721
+ const disk = (await q.query(
2722
+ "SELECT COALESCE(SUM(DATA_LENGTH + INDEX_LENGTH), 0) AS s FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN (?)",
2723
+ [need]
2724
+ ))[0][0];
2725
+ const fmt = (await q.query(
2726
+ `SELECT ${mime} AS m, COUNT(*) AS n, COALESCE(SUM(${size}), 0) AS bytes ${j} GROUP BY ${mime} ORDER BY COUNT(*) DESC, ${mime}`
2727
+ ))[0];
2728
+ const utc = (v) => v ? `${v}+00:00` : null;
2729
+ const info = {
2730
+ mode: this.dedup ? "dedup" : "classic",
2731
+ count: Number(tot.n),
2732
+ totalBytes: Number(tot.bytes),
2733
+ oldest: utc(tot.oldest),
2734
+ newest: utc(tot.newest),
2735
+ onDisk: prettySize(Number(disk.s)),
2736
+ byFormat: fmt.map((r) => ({
2737
+ mimeType: r.m,
2738
+ count: Number(r.n),
2739
+ bytes: Number(r.bytes)
2740
+ }))
2741
+ };
2742
+ if (this.dedup) {
2743
+ const b = (await q.query(
2744
+ "SELECT COUNT(*) AS n, COALESCE(SUM(size_bytes), 0) AS bytes FROM zerobucket_blobs"
2745
+ ))[0][0];
2746
+ info.dedup = { blobs: Number(b.n), storedBytes: Number(b.bytes) };
2747
+ } else {
2748
+ const t = (await q.query(
2749
+ "SELECT COUNT(*) AS n, COALESCE(SUM(size_bytes), 0) AS bytes FROM zerobucket_images WHERE storage_backend = 'object_storage'"
2750
+ ))[0][0];
2751
+ info.tiered = { count: Number(t.n), bytes: Number(t.bytes) };
2752
+ }
2753
+ return info;
2754
+ });
2755
+ } catch (exc) {
2756
+ throw wrap("Failed to read storage info", exc);
2757
+ }
2758
+ }
2759
+ /** Ids matching the filters: oldest first, or a random sample. Used by the CLI's verify and tier. */
2760
+ async listImages(o = {}) {
2761
+ const where = [];
2762
+ const params = [];
2763
+ const created = this.dedup ? "r.created_at" : "created_at";
2764
+ if (o.minSize !== void 0) {
2765
+ where.push(`${this.dedup ? "b.size_bytes" : "size_bytes"} >= ?`);
2766
+ params.push(o.minSize);
2767
+ }
2768
+ if (o.olderThanDays !== void 0) {
2769
+ where.push(`${created} <= UTC_TIMESTAMP(6) - INTERVAL ? DAY`);
2770
+ params.push(o.olderThanDays);
2771
+ }
2772
+ if (o.onlyUntiered && !this.dedup)
2773
+ where.push(`storage_backend = '${LOCAL}'`);
2774
+ const from = this.dedup ? "FROM zerobucket_image_refs r JOIN zerobucket_blobs b ON r.checksum_sha256 = b.checksum_sha256" : "FROM zerobucket_images";
2775
+ const idCol = this.dedup ? "r.id" : "id";
2776
+ const tierCol = this.dedup ? "0 AS tiered" : "(storage_backend = 'object_storage') AS tiered";
2777
+ const order = o.sample !== void 0 ? "ORDER BY RAND()" : `ORDER BY ${created}, ${idCol}`;
2778
+ const lim = o.sample ?? o.limit;
2779
+ let sql = `SELECT ${idCol} AS id, ${tierCol} ${from}${where.length ? " WHERE " + where.join(" AND ") : ""} ${order}`;
2780
+ if (lim !== void 0) {
2781
+ if (!Number.isInteger(lim) || lim < 0)
2782
+ throw new RangeError("limit/sample must be a non-negative integer");
2783
+ sql += ` LIMIT ${lim}`;
2784
+ }
2785
+ try {
2786
+ const rows = await this.run(
2787
+ "list_images",
2788
+ void 0,
2789
+ async (q) => (await q.query(sql, params))[0]
2790
+ );
2791
+ return rows.map((r) => ({
2792
+ id: String(r.id),
2793
+ tiered: Boolean(Number(r.tiered))
2794
+ }));
2795
+ } catch (exc) {
2796
+ throw wrap("Failed to list images", exc);
2797
+ }
2798
+ }
2799
+ // ---- dedup deletes ---------------------------------------------------
2800
+ /**
2801
+ * Lock the ref row, delete it, then decrement its blob (the UPDATE takes the blob row lock, so two
2802
+ * deletes of different refs to one blob serialize instead of racing) and remove the blob once nothing
2803
+ * references it. All in one transaction: a failure can never leave a ref without its count or the reverse.
2804
+ */
2805
+ async deleteDedup(id, connection) {
2806
+ try {
2807
+ return await this.run(
2808
+ "delete",
2809
+ connection,
2810
+ async (q) => {
2811
+ const ref = (await q.query(D_LOCK_REF, [id]))[0][0];
2812
+ if (!ref) return false;
2813
+ const checksum = ref.checksum_sha256;
2814
+ await q.query(D_DELETE_REF3, [id]);
2815
+ await q.query(D_DECREMENT3, [1, checksum]);
2816
+ await q.query(D_DELETE_EMPTY3, [checksum]);
2817
+ return true;
2818
+ },
2819
+ { tx: true }
2820
+ );
2821
+ } catch (exc) {
2822
+ throw wrap("Failed to delete image", exc);
2823
+ }
2824
+ }
2825
+ async deleteManyDedup(ids, connection) {
2826
+ try {
2827
+ return await this.run(
2828
+ "delete_many",
2829
+ connection,
2830
+ async (q) => {
2831
+ const found = (await q.query(D_LOCK_REFS, [ids]))[0];
2832
+ if (found.length === 0) return [];
2833
+ for (const r of found) await q.query(D_DELETE_REF3, [r.id]);
2834
+ const counts = /* @__PURE__ */ new Map();
2835
+ for (const r of found)
2836
+ counts.set(
2837
+ r.checksum_sha256,
2838
+ (counts.get(r.checksum_sha256) ?? 0) + 1
2839
+ );
2840
+ for (const checksum of [...counts.keys()].sort()) {
2841
+ await q.query(D_DECREMENT3, [counts.get(checksum), checksum]);
2842
+ await q.query(D_DELETE_EMPTY3, [checksum]);
2843
+ }
2844
+ return found.map((r) => String(r.id));
2845
+ },
2846
+ { tx: true }
2847
+ );
2848
+ } catch (exc) {
2849
+ throw wrap("Failed to delete image batch", exc);
2850
+ }
2851
+ }
2605
2852
  // ---- helpers ---------------------------------------------------------
2606
2853
  hydrate(row, id) {
2607
2854
  return hydrateRow(row, id, this.opts.objectStorage);
@@ -2648,6 +2895,11 @@ var MySQLBackend = class {
2648
2895
  * id) makes the re-upload an idempotent overwrite.
2649
2896
  */
2650
2897
  async tierToObjectStorage(id, { connection } = {}) {
2898
+ if (this.dedup) {
2899
+ throw new StorageError(
2900
+ "tierToObjectStorage() is not supported in dedup mode (a blob may be shared by many images)."
2901
+ );
2902
+ }
2651
2903
  const os = this.opts.objectStorage;
2652
2904
  if (!os) {
2653
2905
  throw new StorageError(
@@ -2675,6 +2927,20 @@ var MySQLBackend = class {
2675
2927
  }
2676
2928
  }
2677
2929
  };
2930
+ function blobParams2(r, now) {
2931
+ return [
2932
+ r.checksumSha256,
2933
+ r.data,
2934
+ r.mimeType,
2935
+ r.sizeBytes,
2936
+ r.width,
2937
+ r.height,
2938
+ now
2939
+ ];
2940
+ }
2941
+ function refParams(id, r, now) {
2942
+ return [id, r.checksumSha256, r.originalFilename, now, now];
2943
+ }
2678
2944
  function rowParams2(id, r, now) {
2679
2945
  return [
2680
2946
  id,