zerobucket 0.7.0 → 0.8.1
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 +81 -35
- package/dist/bin.js +1010 -17
- package/dist/{client-XQ22b1l3.d.cts → client-l4jPIFJI.d.cts} +21 -8
- package/dist/{client-XQ22b1l3.d.ts → client-l4jPIFJI.d.ts} +21 -8
- package/dist/http.d.cts +1 -1
- package/dist/http.d.ts +1 -1
- package/dist/index.cjs +294 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -14
- package/dist/index.d.ts +28 -14
- package/dist/index.js +294 -24
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,8 +10,7 @@ This is the Node.js/TypeScript sibling of the [Python package](https://pypi.org/
|
|
|
10
10
|
**It uses the exact same table**, so a Node service and a Python service can share one database
|
|
11
11
|
and read each other's images.
|
|
12
12
|
|
|
13
|
-
> **Status: early (0.
|
|
14
|
-
> mode; dedup and the CLI are not available for MySQL yet, see [What's not here yet](#whats-not-here-yet)), with
|
|
13
|
+
> **Status: early (0.8.x).** PostgreSQL, SQLite **and MySQL/MariaDB**, each in classic and dedup modes, with
|
|
15
14
|
> S3-compatible object-storage tiering, HEIC (iPhone photo) support and a `zerobucket` command line tool.
|
|
16
15
|
|
|
17
16
|
## Install
|
|
@@ -220,10 +219,13 @@ const id2 = await images.put(iphonePhoto, {
|
|
|
220
219
|
|
|
221
220
|
The package installs a `zerobucket` command (`npx zerobucket ...`, or install globally with `npm i -g zerobucket`).
|
|
222
221
|
It has the same commands, flags, messages and exit codes as the Python package's CLI, and additionally works with
|
|
223
|
-
**SQLite
|
|
222
|
+
**SQLite**, **MySQL/MariaDB** (a `mysql://` or `mariadb://` `--database-url`, or `ZEROBUCKET_DATABASE_URL`) and **dedup
|
|
223
|
+
mode**, and understands images that were tiered to object storage. On MySQL the on-disk size is read from
|
|
224
|
+
`information_schema` and is approximate (MySQL 8 caches those statistics for up to 24 hours).
|
|
224
225
|
|
|
225
226
|
```bash
|
|
226
227
|
export ZEROBUCKET_DATABASE_URL=postgresql://user:pass@localhost/mydb # or: --database-url URL
|
|
228
|
+
# MySQL/MariaDB: export ZEROBUCKET_DATABASE_URL=mysql://user:pass@localhost:3306/mydb # mariadb:// works too
|
|
227
229
|
# SQLite instead: export ZEROBUCKET_SQLITE_PATH=./images.db # or: --sqlite PATH
|
|
228
230
|
|
|
229
231
|
zerobucket init # create the schema (add --dedup for the dedup tables)
|
|
@@ -234,6 +236,7 @@ zerobucket tier IMAGE_ID --bucket my-images # move one i
|
|
|
234
236
|
zerobucket tier --older-than 90 --min-size 500000 --bucket my-images --dry-run # preview a bulk move
|
|
235
237
|
zerobucket tier --all --limit 1000 --bucket my-images --endpoint-url https://<acct>.r2.cloudflarestorage.com
|
|
236
238
|
zerobucket migrate --to-dedup # PostgreSQL: copy classic images into the dedup tables (non-destructive, re-runnable)
|
|
239
|
+
zerobucket migrate --to-dedup --skip-tiered # ...skipping images tiered to object storage instead of refusing
|
|
237
240
|
```
|
|
238
241
|
|
|
239
242
|
- **Exit codes:** `0` success, `1` problems found or an operation failed, `2` usage or connection error. That makes
|
|
@@ -308,12 +311,16 @@ const images = new ZeroBucket({
|
|
|
308
311
|
|
|
309
312
|
Same API as everywhere else: `put`, `get`, `getMany`, `putMany`, `delete`, `deleteMany`, `exists`, `metadata`, streaming
|
|
310
313
|
and ranges, access-control hooks, the optimize pipeline, custom validators, HEIC, `onOperation` metrics, retry with
|
|
311
|
-
backoff, pooling (`poolMaxSize`, `poolTimeoutMs`), **object-storage tiering** and
|
|
312
|
-
|
|
313
|
-
|
|
314
|
+
backoff, pooling (`poolMaxSize`, `poolTimeoutMs`), **dedup mode** (`dedup: true`), **object-storage tiering** and
|
|
315
|
+
`connection` (your own transaction). The [`zerobucket` CLI](#command-line) works on it too: pass a `mysql://` or
|
|
316
|
+
`mariadb://` `--database-url`.
|
|
314
317
|
|
|
315
|
-
|
|
316
|
-
|
|
318
|
+
It uses the Python package's tables, byte for byte (`SHOW CREATE TABLE` is identical for the classic table and for both
|
|
319
|
+
dedup tables), so a Node app and a Python app can share one MySQL/MariaDB database. The conformance harness verifies both
|
|
320
|
+
directions in both modes, including tiered rows and shared dedup blobs.
|
|
321
|
+
|
|
322
|
+
As everywhere, dedup and tiering cannot be combined (`dedup: true` with `objectStorage` throws). Copying an existing
|
|
323
|
+
classic table into the dedup tables (`migrate --to-dedup`) is PostgreSQL-only, as in the Python package.
|
|
317
324
|
|
|
318
325
|
What is different from Postgres, and why (each verified against a real MariaDB 10.11):
|
|
319
326
|
|
|
@@ -331,6 +338,18 @@ What is different from Postgres, and why (each verified against a real MariaDB 1
|
|
|
331
338
|
- **Migration takes a server-side lock** (`GET_LOCK`), so many processes starting at once do not race on the DDL, and it
|
|
332
339
|
issues **no DDL at all** when the schema is current. (Even a no-op `ALTER TABLE` needs an exclusive metadata lock that
|
|
333
340
|
waits behind every open transaction, and everything else on that table then queues behind _it_.)
|
|
341
|
+
- **Dedup** stores each distinct image once (`zerobucket_blobs`, with a `ref_count`) and one row per image
|
|
342
|
+
(`zerobucket_image_refs`). Without `RETURNING`, a delete decrements with `UPDATE ... ref_count = ref_count - n` (which
|
|
343
|
+
takes the row lock) and then removes the blob once the count reaches zero; the foreign key makes deleting a blob that is
|
|
344
|
+
still referenced impossible. A batch groups identical images, touches each distinct blob **once, in sorted checksum
|
|
345
|
+
order**, and decrements in the same order, so overlapping batches cannot deadlock.
|
|
346
|
+
- **Batch deletes use one `SELECT ... FOR UPDATE` and then point deletes, never `DELETE ... WHERE id IN (...)`.** On
|
|
347
|
+
MariaDB 10.11, with a small table, the `IN` delete is planned as a full scan (`EXPLAIN` says `type=ALL`), and a scan
|
|
348
|
+
under REPEATABLE READ locks every row it passes, including rows another batch already holds. Two overlapping
|
|
349
|
+
`deleteMany` batches deadlocked in 126 of 300 transactions with an `IN` delete and in 0 with point deletes (also 0 with
|
|
350
|
+
a concurrent writer on the same blobs); `READ COMMITTED` and `FORCE INDEX` alone did not fix it. MySQL 8.0.46 plans the
|
|
351
|
+
same statement as a range over just the listed ids and did **not** deadlock in the same test. Point deletes do not
|
|
352
|
+
depend on the plan, so they are used on both.
|
|
334
353
|
- **Large images and `max_allowed_packet`.** An image bigger than the server's limit fails with an error that says so and
|
|
335
354
|
how to raise it (`SET GLOBAL max_allowed_packet = ...`). Images are sent as raw bytes, so the limit applies to the image
|
|
336
355
|
size, not twice that.
|
|
@@ -343,8 +362,9 @@ What is different from Postgres, and why (each verified against a real MariaDB 1
|
|
|
343
362
|
skipped on your connection (it cannot know whether you will commit).
|
|
344
363
|
- **Streaming** uses ranged `SUBSTRING()` reads, so a delete mid-stream raises `StorageError` (like Postgres). Tiered images
|
|
345
364
|
stream with real S3 `Range` requests.
|
|
346
|
-
- **Tested on MariaDB 10.11
|
|
347
|
-
|
|
365
|
+
- **Tested on MariaDB 10.11 and MySQL 8.0.46** (the MySQL, dedup and CLI test suites, the Node/Python conformance harness
|
|
366
|
+
and the schema comparison all pass on both). Other versions are expected to behave the same but have not been run. Use
|
|
367
|
+
MariaDB 10.2.1+ or MySQL 8.0.16+: older servers accept the `CHECK` constraint syntax but ignore it.
|
|
348
368
|
|
|
349
369
|
## Deduplication
|
|
350
370
|
|
|
@@ -371,7 +391,11 @@ await images.delete(b); // last reference gone: blob removed
|
|
|
371
391
|
|
|
372
392
|
**Moving existing data in:** `await images.migrateClassicToDedup()` copies every classic image into the dedup tables,
|
|
373
393
|
keeping each id, and never modifies the classic table. It runs in small batches (bounded memory), is safe to re-run
|
|
374
|
-
(already-migrated ids are skipped and counted), and refuses up front if any classic image is tiered to object storage
|
|
394
|
+
(already-migrated ids are skipped and counted), and refuses up front if any classic image is tiered to object storage
|
|
395
|
+
(their bytes are not in the database to copy, and dedup mode has no tiering), changing nothing. If you would rather
|
|
396
|
+
migrate everything else, pass `{ skipTiered: true }` (CLI: `--skip-tiered`): tiered images are left untouched in the
|
|
397
|
+
classic table, counted in `tieredSkipped`, and **will not exist in the dedup tables**, so only use it if you can live
|
|
398
|
+
without them there.
|
|
375
399
|
|
|
376
400
|
## Object-storage tiering
|
|
377
401
|
|
|
@@ -421,30 +445,52 @@ same corpus of valid and corrupt files identically.
|
|
|
421
445
|
|
|
422
446
|
## Differences from the Python package
|
|
423
447
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
|
428
|
-
|
|
|
429
|
-
|
|
|
430
|
-
|
|
|
431
|
-
|
|
|
432
|
-
|
|
|
433
|
-
|
|
|
434
|
-
|
|
|
435
|
-
|
|
|
436
|
-
|
|
|
437
|
-
|
|
|
438
|
-
|
|
|
439
|
-
| `
|
|
440
|
-
|
|
|
441
|
-
|
|
|
442
|
-
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
+
Python column = zerobucket on PyPI **0.25.1 or later** (several of its behaviours below were fixed in 0.25.1; older versions
|
|
449
|
+
differ).
|
|
450
|
+
|
|
451
|
+
| | Python | npm |
|
|
452
|
+
| -------------------------------------------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
453
|
+
| API | sync + async clients | one async client |
|
|
454
|
+
| Malformed id (`"abc"`) | `StorageError` from Postgres | treated as not found (`ImageNotFoundError`, no DB round trip), friendlier for `/images/:id` routes |
|
|
455
|
+
| `optimize` | Pillow; EXIF stripped, not applied | sharp; EXIF **applied then stripped**; transparency → **white** for JPEG |
|
|
456
|
+
| `putMany` inserts | pipelined | one transaction, statements run sequentially |
|
|
457
|
+
| Formats | JPEG, PNG, WebP, HEIC (`[heic]` extra) | JPEG, PNG, WebP, HEIC (optional `libheif-js`) |
|
|
458
|
+
| Convert TO HEIC with `optimize` | yes (pillow-heif) | no (no HEVC encoder for Node); converting FROM HEIC works; `optimize: true` on a HEIC needs an explicit `format` |
|
|
459
|
+
| Event duration | `duration_seconds` | `durationMs` |
|
|
460
|
+
| `migrateClassicToDedup`, tiered classic rows | always **skips** them and reports `tiered_rows_skipped` | **refuses** by default (changes nothing); `skipTiered: true` / `--skip-tiered` skips and reports like Python |
|
|
461
|
+
| `migrateClassicToDedup`, other behaviour | batched, safe to re-run (server-side cursor) | batched, safe to re-run (keyset paging, advisory lock so concurrent runs cannot race) |
|
|
462
|
+
| SQLite API | synchronous `SQLiteBackend` + separate async backend | one async client; the sync driver runs on the event loop, lock waits are async (never block it) |
|
|
463
|
+
| SQLite `onOperation` / busy retry | none | `onOperation` events and async `SQLITE_BUSY` retry (`busyTimeoutMs`) |
|
|
464
|
+
| SQLite stream deleted mid-read | sync adapter keeps reading a WAL snapshot (async adapter raises) | raises `StorageError`, like Postgres; use `connection` in a transaction for a snapshot |
|
|
465
|
+
| MySQL `onOperation` / retry | sync `MySQLBackend` yes; `AsyncMySQLBackend` none | yes (same client for everything) |
|
|
466
|
+
| CLI | PostgreSQL, classic mode only; `verify` skips tiered rows with a note | PostgreSQL, SQLite **and** MySQL/MariaDB, classic **and** dedup; `verify --bucket` also checks tiered images; `migrate --to-dedup` |
|
|
467
|
+
| `connection=` without an open transaction | multi-statement ops rely on the driver's implicit transaction | always atomic (own transaction or savepoint) |
|
|
468
|
+
| S3 delete failure after `delete()` | raises after the row is gone | never fails the delete; reported via `onOperation` |
|
|
469
|
+
| Options | snake_case kwargs | camelCase options object |
|
|
470
|
+
|
|
471
|
+
Both packages now leave a tiered image's object in place on `delete(id, connection=...)` (a rolled-back transaction can
|
|
472
|
+
never lose data), take dedup batch locks in sorted checksum order (overlapping batches cannot deadlock), and skip the
|
|
473
|
+
MySQL `ALTER TABLE` at startup when the schema is current.
|
|
474
|
+
|
|
475
|
+
## Limitations (read before using in production)
|
|
476
|
+
|
|
477
|
+
- **Images live in your database.** That is the point (one backup, one transaction, no extra service), but it means
|
|
478
|
+
this is not built for very large files or high-volume media: every image goes through the database and counts toward
|
|
479
|
+
its size, its backups and its replication. The default size limit is 8 MiB (`maxBytes`). For large video or
|
|
480
|
+
millions of big files, use object storage directly.
|
|
481
|
+
- **Backups and space need attention.** A whole-database dump now includes every image, and deleting images does not
|
|
482
|
+
give disk space back on its own (PostgreSQL needs autovacuum, MySQL `OPTIMIZE TABLE`, SQLite `VACUUM`). SQLite
|
|
483
|
+
files must be backed up with `.backup`, not copied. See the
|
|
484
|
+
[Operations Guide](https://github.com/KedarGhadyalji/ZeroBucket/blob/main/docs/OPERATIONS.md).
|
|
485
|
+
- **Dedup and tiering cannot be combined** (a shared blob has many owners): `dedup: true` with `objectStorage` throws.
|
|
486
|
+
- **`migrate --to-dedup` is PostgreSQL-only**, as in the Python package. Tiered classic images cannot be migrated (see
|
|
487
|
+
Deduplication for `skipTiered`).
|
|
488
|
+
- **HEIC can be read and converted from, but not written.** There is no HEVC encoder for Node, so `optimize` cannot
|
|
489
|
+
produce HEIC.
|
|
490
|
+
- **SQLite allows one writer at a time**; lock waits are async and never block the event loop, but write-heavy
|
|
491
|
+
workloads belong on PostgreSQL or MySQL/MariaDB.
|
|
492
|
+
- **Tested versions:** CI runs PostgreSQL 16 and MariaDB 10.11; MySQL 8.0.46 was also run by hand. Other versions are
|
|
493
|
+
expected to behave the same but have not been run.
|
|
448
494
|
|
|
449
495
|
## Runtime notes
|
|
450
496
|
|