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/README.md +31 -12
- package/dist/bin.js +984 -5
- package/dist/index.cjs +278 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -1
- package/dist/index.d.ts +20 -1
- package/dist/index.js +278 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
2160
|
+
this.dedup = opts.dedup ?? false;
|
|
2161
|
+
if (this.dedup && opts.objectStorage) {
|
|
2112
2162
|
throw new TypeError(
|
|
2113
|
-
"
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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,
|