zerobucket 0.6.0 → 0.7.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 +63 -8
- 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 +728 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +110 -3
- package/dist/index.d.ts +110 -3
- package/dist/index.js +725 -2
- package/dist/index.js.map +1 -1
- package/package.json +9 -4
package/README.md
CHANGED
|
@@ -10,20 +10,26 @@ 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
|
-
>
|
|
15
|
-
>
|
|
13
|
+
> **Status: early (0.7.x).** PostgreSQL **and SQLite**, each in classic and dedup modes, plus **MySQL/MariaDB** (classic
|
|
14
|
+
> mode; dedup and the CLI are not available for MySQL yet, see [What's not here yet](#whats-not-here-yet)), with
|
|
15
|
+
> S3-compatible object-storage tiering, HEIC (iPhone photo) support and a `zerobucket` command line tool.
|
|
16
16
|
|
|
17
17
|
## Install
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
20
|
npm install zerobucket pg sharp # PostgreSQL
|
|
21
21
|
npm install zerobucket better-sqlite3 sharp # or SQLite (see below): no database server at all
|
|
22
|
+
npm install zerobucket mysql2 sharp # or MySQL / MariaDB (see below)
|
|
22
23
|
npm install libheif-js # optional: accept HEIC (iPhone) photos, see below
|
|
23
24
|
```
|
|
24
25
|
|
|
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.
|
|
26
|
+
`pg` (the Postgres driver), `better-sqlite3` (SQLite), `mysql2` (MySQL/MariaDB) and `sharp` (image decoding/optimizing) are
|
|
27
|
+
peer dependencies, so you control their versions. If you only store PDFs through a custom validator, `sharp` is not needed.
|
|
28
|
+
`sharp` must be **0.35.5 or newer**: older versions bundle libvips/libheif/librsvg builds with published high-severity
|
|
29
|
+
advisories, and this package decodes untrusted uploads with it. A plain `npm install sharp` already gets a fixed version;
|
|
30
|
+
if you pinned an older one, upgrade with `npm install sharp@latest`.
|
|
31
|
+
`mysql2` must be **3.23.1 or newer** for the same reason: every earlier release has a published advisory (the oldest range
|
|
32
|
+
includes critical ones), and 3.23.1 is the first that `npm audit` reports clean.
|
|
27
33
|
For [object-storage tiering](#object-storage-tiering) also install `@aws-sdk/client-s3` (optional; loaded only when used).
|
|
28
34
|
Requires Node 20+ (SQLite needs Node 22+, see below). Ships ESM and CommonJS with full types.
|
|
29
35
|
|
|
@@ -214,7 +220,7 @@ const id2 = await images.put(iphonePhoto, {
|
|
|
214
220
|
|
|
215
221
|
The package installs a `zerobucket` command (`npx zerobucket ...`, or install globally with `npm i -g zerobucket`).
|
|
216
222
|
It has the same commands, flags, messages and exit codes as the Python package's CLI, and additionally works with
|
|
217
|
-
**SQLite** and **dedup mode**, and understands images that were tiered to object storage.
|
|
223
|
+
**SQLite** and **dedup mode**, and understands images that were tiered to object storage. It does not know MySQL/MariaDB yet.
|
|
218
224
|
|
|
219
225
|
```bash
|
|
220
226
|
export ZEROBUCKET_DATABASE_URL=postgresql://user:pass@localhost/mydb # or: --database-url URL
|
|
@@ -291,6 +297,55 @@ How it behaves in Node, and why:
|
|
|
291
297
|
(`better-sqlite3` 12 declares Node 20 support, but it is untested here.)
|
|
292
298
|
- Not available on SQLite: `migrateClassicToDedup()` (Python has it for Postgres only, too).
|
|
293
299
|
|
|
300
|
+
## MySQL / MariaDB
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
const images = new ZeroBucket({
|
|
304
|
+
mysql: "mysql://user:password@localhost:3306/mydb",
|
|
305
|
+
}); // npm install mysql2
|
|
306
|
+
// "mariadb://..." works too. Bring your own pool with ZeroBucket({ mysqlPool }) (never closed by close()).
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Same API as everywhere else: `put`, `get`, `getMany`, `putMany`, `delete`, `deleteMany`, `exists`, `metadata`, streaming
|
|
310
|
+
and ranges, access-control hooks, the optimize pipeline, custom validators, HEIC, `onOperation` metrics, retry with
|
|
311
|
+
backoff, pooling (`poolMaxSize`, `poolTimeoutMs`), **object-storage tiering** and `connection` (your own transaction).
|
|
312
|
+
It uses the Python package's table, byte for byte (`SHOW CREATE TABLE` is identical), so a Node app and a Python app can
|
|
313
|
+
share one MySQL/MariaDB database (the conformance harness verifies both directions, including tiered rows).
|
|
314
|
+
|
|
315
|
+
**Not available on MySQL yet:** dedup mode (`dedup: true` throws, rather than being ignored) and the `zerobucket` command
|
|
316
|
+
line tool (it knows PostgreSQL and SQLite). The Python package has both for MySQL.
|
|
317
|
+
|
|
318
|
+
What is different from Postgres, and why (each verified against a real MariaDB 10.11):
|
|
319
|
+
|
|
320
|
+
- **No `RETURNING`.** MySQL 8.0 does not have it (MariaDB 10.5+ does, but one code path serves both), so `delete()` and
|
|
321
|
+
`deleteMany()` run `SELECT ... FOR UPDATE` and then `DELETE` in one transaction. That is also how a tiered row's object
|
|
322
|
+
gets cleaned up.
|
|
323
|
+
- **Timestamps are written as UTC strings** (`DATETIME(6)`, naive UTC, like the Python package), so your server's or your
|
|
324
|
+
process's timezone setting cannot change what is stored.
|
|
325
|
+
- **Pooling.** `mysql2` opens connections on demand, so `poolMinSize` is accepted for parity but there is no minimum to hold.
|
|
326
|
+
`mysql2` has no acquire timeout, so `poolTimeoutMs` is implemented here: waiting longer than that for a free connection
|
|
327
|
+
throws a clear `StorageError`, which is **not** retried (retrying would only wait again).
|
|
328
|
+
- **Retry** covers connection loss, lock-wait timeout (1205), deadlock (1213) and "too many connections". Every retry
|
|
329
|
+
re-runs the **whole** operation on a fresh connection after a full rollback, never a single statement, so a multi-statement
|
|
330
|
+
operation can never be half-applied. A connection that fails is destroyed, never returned to the pool.
|
|
331
|
+
- **Migration takes a server-side lock** (`GET_LOCK`), so many processes starting at once do not race on the DDL, and it
|
|
332
|
+
issues **no DDL at all** when the schema is current. (Even a no-op `ALTER TABLE` needs an exclusive metadata lock that
|
|
333
|
+
waits behind every open transaction, and everything else on that table then queues behind _it_.)
|
|
334
|
+
- **Large images and `max_allowed_packet`.** An image bigger than the server's limit fails with an error that says so and
|
|
335
|
+
how to raise it (`SET GLOBAL max_allowed_packet = ...`). Images are sent as raw bytes, so the limit applies to the image
|
|
336
|
+
size, not twice that.
|
|
337
|
+
- **Tiering** locks only the image being moved (InnoDB row lock) for the duration of the upload: writes to _that_ image wait
|
|
338
|
+
(up to `innodb_lock_wait_timeout`, 50 s by default), every other row and every plain read is unaffected. Postgres behaves
|
|
339
|
+
the same way; SQLite has to lock the whole file.
|
|
340
|
+
- **`connection`** takes your own `mysql2/promise` connection. Multi-statement operations first check whether a transaction
|
|
341
|
+
is open: inside one they use a `SAVEPOINT` (a failure undoes only this call's work), in autocommit mode they open and
|
|
342
|
+
commit their own. The adapter never commits, rolls back or retries your transaction. Object cleanup after a `delete` is
|
|
343
|
+
skipped on your connection (it cannot know whether you will commit).
|
|
344
|
+
- **Streaming** uses ranged `SUBSTRING()` reads, so a delete mid-stream raises `StorageError` (like Postgres). Tiered images
|
|
345
|
+
stream with real S3 `Range` requests.
|
|
346
|
+
- **Tested on MariaDB 10.11.** Real MySQL 8.x is expected to behave the same (the SQL avoids MariaDB-only features) but has
|
|
347
|
+
not been run here. Use MariaDB 10.2.1+ or MySQL 8.0.16+: older servers accept the `CHECK` constraint syntax but ignore it.
|
|
348
|
+
|
|
294
349
|
## Deduplication
|
|
295
350
|
|
|
296
351
|
Store identical bytes once. In dedup mode every `put()` still returns its own id and keeps its own filename,
|
|
@@ -388,12 +443,12 @@ same corpus of valid and corrupt files identically.
|
|
|
388
443
|
|
|
389
444
|
## What's not here yet
|
|
390
445
|
|
|
391
|
-
Planned: **MySQL
|
|
446
|
+
Planned for MySQL/MariaDB: **dedup mode**, the **`zerobucket` CLI**, and a MySQL-specific operations guide.
|
|
392
447
|
Tiering and dedup cannot be combined (same rule as the Python package).
|
|
393
448
|
|
|
394
449
|
## Runtime notes
|
|
395
450
|
|
|
396
|
-
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
|
|
451
|
+
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
|
|
397
452
|
Postgres TCP connections through `pg` without a proxy such as Hyperdrive; use the Node runtime for these routes.
|
|
398
453
|
In serverless, keep `poolMaxSize` small (1-2) or inject a pool shared across invocations.
|
|
399
454
|
|
|
@@ -77,8 +77,30 @@ interface SqliteDatabaseLike {
|
|
|
77
77
|
};
|
|
78
78
|
exec(sql: string): unknown;
|
|
79
79
|
}
|
|
80
|
-
/**
|
|
81
|
-
|
|
80
|
+
/**
|
|
81
|
+
* A `mysql2/promise` connection (or pool connection): both `query()` and `execute()` resolve to
|
|
82
|
+
* `[rowsOrResultHeader, fields]`. Pass one as `connection` to make a call part of YOUR MySQL/MariaDB
|
|
83
|
+
* transaction. Declared structurally so consumers do not need `mysql2` installed to type-check.
|
|
84
|
+
*/
|
|
85
|
+
interface MysqlQueryable {
|
|
86
|
+
query(sql: string, values?: any): Promise<[any, any]>;
|
|
87
|
+
execute(sql: string, values?: any): Promise<[any, any]>;
|
|
88
|
+
}
|
|
89
|
+
/** The slice of a `mysql2/promise` pool connection that the MySQL backend uses. */
|
|
90
|
+
interface MysqlPoolConnectionLike extends MysqlQueryable {
|
|
91
|
+
beginTransaction(): Promise<void>;
|
|
92
|
+
commit(): Promise<void>;
|
|
93
|
+
rollback(): Promise<void>;
|
|
94
|
+
release(): void;
|
|
95
|
+
destroy(): void;
|
|
96
|
+
}
|
|
97
|
+
/** The slice of a `mysql2/promise` Pool that the MySQL backend uses (bring your own via `pool`). */
|
|
98
|
+
interface MysqlPoolLike {
|
|
99
|
+
getConnection(): Promise<MysqlPoolConnectionLike>;
|
|
100
|
+
end(): Promise<void>;
|
|
101
|
+
}
|
|
102
|
+
/** What `connection` accepts: a `pg` client for Postgres, a `better-sqlite3` Database for SQLite, a `mysql2/promise` connection for MySQL/MariaDB. */
|
|
103
|
+
type Connection = Queryable | SqliteDatabaseLike | MysqlQueryable;
|
|
82
104
|
/** The row shape a storage backend persists (after validation and checksumming). */
|
|
83
105
|
interface PreparedRow {
|
|
84
106
|
data: Buffer;
|
|
@@ -314,7 +336,17 @@ interface ZeroBucketOptions<C = unknown> {
|
|
|
314
336
|
* package's SQLiteBackend, so both can share one `.db` file.
|
|
315
337
|
*/
|
|
316
338
|
sqlite?: string;
|
|
317
|
-
/**
|
|
339
|
+
/**
|
|
340
|
+
* Use MySQL or MariaDB instead of PostgreSQL: `mysql://user:password@host:3306/database`
|
|
341
|
+
* (`mariadb://` works too). Needs `npm install mysql2`. Same schema as the Python package's
|
|
342
|
+
* MySQLBackend, so both can share one database. Classic mode only for now (`dedup` is not
|
|
343
|
+
* available for MySQL yet). `maxRetries`, `retryBaseDelayMs`, `poolMaxSize`, `poolTimeoutMs`,
|
|
344
|
+
* `autoMigrate`, `objectStorage` and `onOperation` apply to it too.
|
|
345
|
+
*/
|
|
346
|
+
mysql?: string;
|
|
347
|
+
/** Bring your own `mysql2/promise` Pool for MySQL/MariaDB. It is NOT closed by close(). */
|
|
348
|
+
mysqlPool?: MysqlPoolLike;
|
|
349
|
+
/** Advanced: inject a custom StorageBackend instead of building one from `connectionString` / `pool` / `sqlite` / `mysql`. */
|
|
318
350
|
backend?: StorageBackend;
|
|
319
351
|
/** Max accepted size in bytes. Default 8 MiB. */
|
|
320
352
|
maxBytes?: number;
|
|
@@ -470,4 +502,4 @@ declare function readInput(image: ImageInput, filename: string | undefined, maxB
|
|
|
470
502
|
filename: string | undefined;
|
|
471
503
|
}>;
|
|
472
504
|
|
|
473
|
-
export { type BatchDeleteResult as B, type CallOptions as C, DEFAULT_MAX_BYTES as D, type GetOptions as G, type ImageListOptions as I, type OperationEvent as O, type PreparedRow as P, type Queryable as Q, type StorageBackend as S, type ValidatedContent as V, type WritableLike as W, ZeroBucket as Z, type ObjectStorageLike as a, type StoredRecord as b, type StoredRecordMetadata as c, type StreamOptions$1 as d, type StorageInfo as e, type ImageListEntry as f, type ContentValidator as g, type BatchGetResult as h, type BatchPutResult as i, type BeforeGetHook as j, type BeforePutHook as k, type Connection as l, type Image as m, type ImageInput as n, type ImageMetadata as o,
|
|
505
|
+
export { optimizeImage as A, type BatchDeleteResult as B, type CallOptions as C, DEFAULT_MAX_BYTES as D, readInput as E, type GetOptions as G, type ImageListOptions as I, type MysqlPoolLike as M, type OperationEvent as O, type PreparedRow as P, type Queryable as Q, type StorageBackend as S, type ValidatedContent as V, type WritableLike as W, ZeroBucket as Z, type ObjectStorageLike as a, type StoredRecord as b, type StoredRecordMetadata as c, type StreamOptions$1 as d, type StorageInfo as e, type ImageListEntry as f, type ContentValidator as g, type BatchGetResult as h, type BatchPutResult as i, type BeforeGetHook as j, type BeforePutHook as k, type Connection as l, type Image as m, type ImageInput as n, type ImageMetadata as o, type MysqlPoolConnectionLike as p, type MysqlQueryable as q, ObjectStorage as r, type ObjectStorageOptions as s, type OptimizationResult as t, type OptimizeOptions as u, type PutManyOptions as v, type PutOptions as w, type SqliteDatabaseLike as x, type StreamOptions as y, type ZeroBucketOptions as z };
|
|
@@ -77,8 +77,30 @@ interface SqliteDatabaseLike {
|
|
|
77
77
|
};
|
|
78
78
|
exec(sql: string): unknown;
|
|
79
79
|
}
|
|
80
|
-
/**
|
|
81
|
-
|
|
80
|
+
/**
|
|
81
|
+
* A `mysql2/promise` connection (or pool connection): both `query()` and `execute()` resolve to
|
|
82
|
+
* `[rowsOrResultHeader, fields]`. Pass one as `connection` to make a call part of YOUR MySQL/MariaDB
|
|
83
|
+
* transaction. Declared structurally so consumers do not need `mysql2` installed to type-check.
|
|
84
|
+
*/
|
|
85
|
+
interface MysqlQueryable {
|
|
86
|
+
query(sql: string, values?: any): Promise<[any, any]>;
|
|
87
|
+
execute(sql: string, values?: any): Promise<[any, any]>;
|
|
88
|
+
}
|
|
89
|
+
/** The slice of a `mysql2/promise` pool connection that the MySQL backend uses. */
|
|
90
|
+
interface MysqlPoolConnectionLike extends MysqlQueryable {
|
|
91
|
+
beginTransaction(): Promise<void>;
|
|
92
|
+
commit(): Promise<void>;
|
|
93
|
+
rollback(): Promise<void>;
|
|
94
|
+
release(): void;
|
|
95
|
+
destroy(): void;
|
|
96
|
+
}
|
|
97
|
+
/** The slice of a `mysql2/promise` Pool that the MySQL backend uses (bring your own via `pool`). */
|
|
98
|
+
interface MysqlPoolLike {
|
|
99
|
+
getConnection(): Promise<MysqlPoolConnectionLike>;
|
|
100
|
+
end(): Promise<void>;
|
|
101
|
+
}
|
|
102
|
+
/** What `connection` accepts: a `pg` client for Postgres, a `better-sqlite3` Database for SQLite, a `mysql2/promise` connection for MySQL/MariaDB. */
|
|
103
|
+
type Connection = Queryable | SqliteDatabaseLike | MysqlQueryable;
|
|
82
104
|
/** The row shape a storage backend persists (after validation and checksumming). */
|
|
83
105
|
interface PreparedRow {
|
|
84
106
|
data: Buffer;
|
|
@@ -314,7 +336,17 @@ interface ZeroBucketOptions<C = unknown> {
|
|
|
314
336
|
* package's SQLiteBackend, so both can share one `.db` file.
|
|
315
337
|
*/
|
|
316
338
|
sqlite?: string;
|
|
317
|
-
/**
|
|
339
|
+
/**
|
|
340
|
+
* Use MySQL or MariaDB instead of PostgreSQL: `mysql://user:password@host:3306/database`
|
|
341
|
+
* (`mariadb://` works too). Needs `npm install mysql2`. Same schema as the Python package's
|
|
342
|
+
* MySQLBackend, so both can share one database. Classic mode only for now (`dedup` is not
|
|
343
|
+
* available for MySQL yet). `maxRetries`, `retryBaseDelayMs`, `poolMaxSize`, `poolTimeoutMs`,
|
|
344
|
+
* `autoMigrate`, `objectStorage` and `onOperation` apply to it too.
|
|
345
|
+
*/
|
|
346
|
+
mysql?: string;
|
|
347
|
+
/** Bring your own `mysql2/promise` Pool for MySQL/MariaDB. It is NOT closed by close(). */
|
|
348
|
+
mysqlPool?: MysqlPoolLike;
|
|
349
|
+
/** Advanced: inject a custom StorageBackend instead of building one from `connectionString` / `pool` / `sqlite` / `mysql`. */
|
|
318
350
|
backend?: StorageBackend;
|
|
319
351
|
/** Max accepted size in bytes. Default 8 MiB. */
|
|
320
352
|
maxBytes?: number;
|
|
@@ -470,4 +502,4 @@ declare function readInput(image: ImageInput, filename: string | undefined, maxB
|
|
|
470
502
|
filename: string | undefined;
|
|
471
503
|
}>;
|
|
472
504
|
|
|
473
|
-
export { type BatchDeleteResult as B, type CallOptions as C, DEFAULT_MAX_BYTES as D, type GetOptions as G, type ImageListOptions as I, type OperationEvent as O, type PreparedRow as P, type Queryable as Q, type StorageBackend as S, type ValidatedContent as V, type WritableLike as W, ZeroBucket as Z, type ObjectStorageLike as a, type StoredRecord as b, type StoredRecordMetadata as c, type StreamOptions$1 as d, type StorageInfo as e, type ImageListEntry as f, type ContentValidator as g, type BatchGetResult as h, type BatchPutResult as i, type BeforeGetHook as j, type BeforePutHook as k, type Connection as l, type Image as m, type ImageInput as n, type ImageMetadata as o,
|
|
505
|
+
export { optimizeImage as A, type BatchDeleteResult as B, type CallOptions as C, DEFAULT_MAX_BYTES as D, readInput as E, type GetOptions as G, type ImageListOptions as I, type MysqlPoolLike as M, type OperationEvent as O, type PreparedRow as P, type Queryable as Q, type StorageBackend as S, type ValidatedContent as V, type WritableLike as W, ZeroBucket as Z, type ObjectStorageLike as a, type StoredRecord as b, type StoredRecordMetadata as c, type StreamOptions$1 as d, type StorageInfo as e, type ImageListEntry as f, type ContentValidator as g, type BatchGetResult as h, type BatchPutResult as i, type BeforeGetHook as j, type BeforePutHook as k, type Connection as l, type Image as m, type ImageInput as n, type ImageMetadata as o, type MysqlPoolConnectionLike as p, type MysqlQueryable as q, ObjectStorage as r, type ObjectStorageOptions as s, type OptimizationResult as t, type OptimizeOptions as u, type PutManyOptions as v, type PutOptions as w, type SqliteDatabaseLike as x, type StreamOptions as y, type ZeroBucketOptions as z };
|
package/dist/http.d.cts
CHANGED
package/dist/http.d.ts
CHANGED