zerobucket 0.6.1 → 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 +80 -9
- package/dist/bin.js +984 -5
- package/dist/{client-C7xs1dlr.d.cts → client-XQ22b1l3.d.cts} +36 -4
- package/dist/{client-C7xs1dlr.d.ts → client-XQ22b1l3.d.ts} +36 -4
- package/dist/http.d.cts +1 -1
- package/dist/http.d.ts +1 -1
- package/dist/index.cjs +1211 -221
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +129 -3
- package/dist/index.d.ts +129 -3
- package/dist/index.js +991 -2
- package/dist/index.js.map +1 -1
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -10,23 +10,25 @@ 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
|
-
> object-storage tiering, HEIC (iPhone photo) support and a `zerobucket` command line tool.
|
|
15
|
-
> roadmap (see [What's not here yet](#whats-not-here-yet)).
|
|
13
|
+
> **Status: early (0.8.x).** PostgreSQL, SQLite **and MySQL/MariaDB**, each in classic and dedup modes, with
|
|
14
|
+
> S3-compatible object-storage tiering, HEIC (iPhone photo) support and a `zerobucket` command line tool.
|
|
16
15
|
|
|
17
16
|
## Install
|
|
18
17
|
|
|
19
18
|
```bash
|
|
20
19
|
npm install zerobucket pg sharp # PostgreSQL
|
|
21
20
|
npm install zerobucket better-sqlite3 sharp # or SQLite (see below): no database server at all
|
|
21
|
+
npm install zerobucket mysql2 sharp # or MySQL / MariaDB (see below)
|
|
22
22
|
npm install libheif-js # optional: accept HEIC (iPhone) photos, see below
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
`pg` (the Postgres driver), `better-sqlite3` (SQLite) and `sharp` (image decoding/optimizing) are
|
|
26
|
-
control their versions. If you only store PDFs through a custom validator, `sharp` is not needed.
|
|
25
|
+
`pg` (the Postgres driver), `better-sqlite3` (SQLite), `mysql2` (MySQL/MariaDB) and `sharp` (image decoding/optimizing) are
|
|
26
|
+
peer dependencies, so you control their versions. If you only store PDFs through a custom validator, `sharp` is not needed.
|
|
27
27
|
`sharp` must be **0.35.5 or newer**: older versions bundle libvips/libheif/librsvg builds with published high-severity
|
|
28
28
|
advisories, and this package decodes untrusted uploads with it. A plain `npm install sharp` already gets a fixed version;
|
|
29
29
|
if you pinned an older one, upgrade with `npm install sharp@latest`.
|
|
30
|
+
`mysql2` must be **3.23.1 or newer** for the same reason: every earlier release has a published advisory (the oldest range
|
|
31
|
+
includes critical ones), and 3.23.1 is the first that `npm audit` reports clean.
|
|
30
32
|
For [object-storage tiering](#object-storage-tiering) also install `@aws-sdk/client-s3` (optional; loaded only when used).
|
|
31
33
|
Requires Node 20+ (SQLite needs Node 22+, see below). Ships ESM and CommonJS with full types.
|
|
32
34
|
|
|
@@ -217,10 +219,13 @@ const id2 = await images.put(iphonePhoto, {
|
|
|
217
219
|
|
|
218
220
|
The package installs a `zerobucket` command (`npx zerobucket ...`, or install globally with `npm i -g zerobucket`).
|
|
219
221
|
It has the same commands, flags, messages and exit codes as the Python package's CLI, and additionally works with
|
|
220
|
-
**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).
|
|
221
225
|
|
|
222
226
|
```bash
|
|
223
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
|
|
224
229
|
# SQLite instead: export ZEROBUCKET_SQLITE_PATH=./images.db # or: --sqlite PATH
|
|
225
230
|
|
|
226
231
|
zerobucket init # create the schema (add --dedup for the dedup tables)
|
|
@@ -294,6 +299,72 @@ How it behaves in Node, and why:
|
|
|
294
299
|
(`better-sqlite3` 12 declares Node 20 support, but it is untested here.)
|
|
295
300
|
- Not available on SQLite: `migrateClassicToDedup()` (Python has it for Postgres only, too).
|
|
296
301
|
|
|
302
|
+
## MySQL / MariaDB
|
|
303
|
+
|
|
304
|
+
```ts
|
|
305
|
+
const images = new ZeroBucket({
|
|
306
|
+
mysql: "mysql://user:password@localhost:3306/mydb",
|
|
307
|
+
}); // npm install mysql2
|
|
308
|
+
// "mariadb://..." works too. Bring your own pool with ZeroBucket({ mysqlPool }) (never closed by close()).
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Same API as everywhere else: `put`, `get`, `getMany`, `putMany`, `delete`, `deleteMany`, `exists`, `metadata`, streaming
|
|
312
|
+
and ranges, access-control hooks, the optimize pipeline, custom validators, HEIC, `onOperation` metrics, retry with
|
|
313
|
+
backoff, pooling (`poolMaxSize`, `poolTimeoutMs`), **dedup mode** (`dedup: true`), **object-storage tiering** and
|
|
314
|
+
`connection` (your own transaction). The [`zerobucket` CLI](#command-line) works on it too: pass a `mysql://` or
|
|
315
|
+
`mariadb://` `--database-url`.
|
|
316
|
+
|
|
317
|
+
It uses the Python package's tables, byte for byte (`SHOW CREATE TABLE` is identical for the classic table and for both
|
|
318
|
+
dedup tables), so a Node app and a Python app can share one MySQL/MariaDB database. The conformance harness verifies both
|
|
319
|
+
directions in both modes, including tiered rows and shared dedup blobs.
|
|
320
|
+
|
|
321
|
+
As everywhere, dedup and tiering cannot be combined (`dedup: true` with `objectStorage` throws). Copying an existing
|
|
322
|
+
classic table into the dedup tables (`migrate --to-dedup`) is PostgreSQL-only, as in the Python package.
|
|
323
|
+
|
|
324
|
+
What is different from Postgres, and why (each verified against a real MariaDB 10.11):
|
|
325
|
+
|
|
326
|
+
- **No `RETURNING`.** MySQL 8.0 does not have it (MariaDB 10.5+ does, but one code path serves both), so `delete()` and
|
|
327
|
+
`deleteMany()` run `SELECT ... FOR UPDATE` and then `DELETE` in one transaction. That is also how a tiered row's object
|
|
328
|
+
gets cleaned up.
|
|
329
|
+
- **Timestamps are written as UTC strings** (`DATETIME(6)`, naive UTC, like the Python package), so your server's or your
|
|
330
|
+
process's timezone setting cannot change what is stored.
|
|
331
|
+
- **Pooling.** `mysql2` opens connections on demand, so `poolMinSize` is accepted for parity but there is no minimum to hold.
|
|
332
|
+
`mysql2` has no acquire timeout, so `poolTimeoutMs` is implemented here: waiting longer than that for a free connection
|
|
333
|
+
throws a clear `StorageError`, which is **not** retried (retrying would only wait again).
|
|
334
|
+
- **Retry** covers connection loss, lock-wait timeout (1205), deadlock (1213) and "too many connections". Every retry
|
|
335
|
+
re-runs the **whole** operation on a fresh connection after a full rollback, never a single statement, so a multi-statement
|
|
336
|
+
operation can never be half-applied. A connection that fails is destroyed, never returned to the pool.
|
|
337
|
+
- **Migration takes a server-side lock** (`GET_LOCK`), so many processes starting at once do not race on the DDL, and it
|
|
338
|
+
issues **no DDL at all** when the schema is current. (Even a no-op `ALTER TABLE` needs an exclusive metadata lock that
|
|
339
|
+
waits behind every open transaction, and everything else on that table then queues behind _it_.)
|
|
340
|
+
- **Dedup** stores each distinct image once (`zerobucket_blobs`, with a `ref_count`) and one row per image
|
|
341
|
+
(`zerobucket_image_refs`). Without `RETURNING`, a delete decrements with `UPDATE ... ref_count = ref_count - n` (which
|
|
342
|
+
takes the row lock) and then removes the blob once the count reaches zero; the foreign key makes deleting a blob that is
|
|
343
|
+
still referenced impossible. A batch groups identical images, touches each distinct blob **once, in sorted checksum
|
|
344
|
+
order**, and decrements in the same order, so overlapping batches cannot deadlock.
|
|
345
|
+
- **Batch deletes use one `SELECT ... FOR UPDATE` and then point deletes, never `DELETE ... WHERE id IN (...)`.** On
|
|
346
|
+
MariaDB 10.11, with a small table, the `IN` delete is planned as a full scan (`EXPLAIN` says `type=ALL`), and a scan
|
|
347
|
+
under REPEATABLE READ locks every row it passes, including rows another batch already holds. Two overlapping
|
|
348
|
+
`deleteMany` batches deadlocked in 126 of 300 transactions with an `IN` delete and in 0 with point deletes (also 0 with
|
|
349
|
+
a concurrent writer on the same blobs); `READ COMMITTED` and `FORCE INDEX` alone did not fix it. MySQL 8.0.46 plans the
|
|
350
|
+
same statement as a range over just the listed ids and did **not** deadlock in the same test. Point deletes do not
|
|
351
|
+
depend on the plan, so they are used on both.
|
|
352
|
+
- **Large images and `max_allowed_packet`.** An image bigger than the server's limit fails with an error that says so and
|
|
353
|
+
how to raise it (`SET GLOBAL max_allowed_packet = ...`). Images are sent as raw bytes, so the limit applies to the image
|
|
354
|
+
size, not twice that.
|
|
355
|
+
- **Tiering** locks only the image being moved (InnoDB row lock) for the duration of the upload: writes to _that_ image wait
|
|
356
|
+
(up to `innodb_lock_wait_timeout`, 50 s by default), every other row and every plain read is unaffected. Postgres behaves
|
|
357
|
+
the same way; SQLite has to lock the whole file.
|
|
358
|
+
- **`connection`** takes your own `mysql2/promise` connection. Multi-statement operations first check whether a transaction
|
|
359
|
+
is open: inside one they use a `SAVEPOINT` (a failure undoes only this call's work), in autocommit mode they open and
|
|
360
|
+
commit their own. The adapter never commits, rolls back or retries your transaction. Object cleanup after a `delete` is
|
|
361
|
+
skipped on your connection (it cannot know whether you will commit).
|
|
362
|
+
- **Streaming** uses ranged `SUBSTRING()` reads, so a delete mid-stream raises `StorageError` (like Postgres). Tiered images
|
|
363
|
+
stream with real S3 `Range` requests.
|
|
364
|
+
- **Tested on MariaDB 10.11 and MySQL 8.0.46** (the MySQL, dedup and CLI test suites, the Node/Python conformance harness
|
|
365
|
+
and the schema comparison all pass on both). Other versions are expected to behave the same but have not been run. Use
|
|
366
|
+
MariaDB 10.2.1+ or MySQL 8.0.16+: older servers accept the `CHECK` constraint syntax but ignore it.
|
|
367
|
+
|
|
297
368
|
## Deduplication
|
|
298
369
|
|
|
299
370
|
Store identical bytes once. In dedup mode every `put()` still returns its own id and keeps its own filename,
|
|
@@ -391,12 +462,12 @@ same corpus of valid and corrupt files identically.
|
|
|
391
462
|
|
|
392
463
|
## What's not here yet
|
|
393
464
|
|
|
394
|
-
Planned
|
|
395
|
-
Tiering and dedup cannot be combined (same rule as the Python package).
|
|
465
|
+
Planned for MySQL/MariaDB: a MySQL-specific operations guide, and `migrate --to-dedup` (PostgreSQL-only today, as in the
|
|
466
|
+
Python package). Tiering and dedup cannot be combined (same rule as the Python package).
|
|
396
467
|
|
|
397
468
|
## Runtime notes
|
|
398
469
|
|
|
399
|
-
Node 20+ for Postgres, Node 22+ for SQLite. Bun/Deno: expected to work, not yet tested. Edge runtimes (Cloudflare Workers, Vercel Edge) cannot open raw
|
|
470
|
+
Node 20+ for Postgres and MySQL/MariaDB, Node 22+ for SQLite. Bun/Deno: expected to work, not yet tested. Edge runtimes (Cloudflare Workers, Vercel Edge) cannot open raw
|
|
400
471
|
Postgres TCP connections through `pg` without a proxy such as Hyperdrive; use the Node runtime for these routes.
|
|
401
472
|
In serverless, keep `poolMaxSize` small (1-2) or inject a pool shared across invocations.
|
|
402
473
|
|