zerobucket 0.9.1 → 0.10.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 CHANGED
@@ -456,25 +456,26 @@ same corpus of valid and corrupt files identically.
456
456
  Python column = zerobucket on PyPI **0.26.0 or later** (several of its behaviours below were fixed in 0.25.1 and 0.26.0; older
457
457
  versions differ).
458
458
 
459
- | | Python | npm |
460
- | -------------------------------------------- | --------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
461
- | API | sync + async clients | one async client |
462
- | `optimize` | Pillow; EXIF stripped, not applied | sharp; EXIF **applied then stripped**; transparency → **white** for JPEG |
463
- | JPEG encoder in `optimize` | Pillow, progressive + optimized | sharp with `mozjpeg`, progressive: on smooth photos roughly **half the size** at about the same SSIM (measured); WebP and PNG match |
464
- | `putMany` inserts | pipelined | one transaction, statements run sequentially |
465
- | Formats | JPEG, PNG, WebP, HEIC (`[heic]` extra) | JPEG, PNG, WebP, HEIC (optional `libheif-js`) |
466
- | 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` |
467
- | Event duration | `duration_seconds` | `durationMs` |
468
- | `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 |
469
- | `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) |
470
- | 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) |
471
- | SQLite `onOperation` / busy retry | none | `onOperation` events and async `SQLITE_BUSY` retry (`busyTimeoutMs`) |
472
- | 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 |
473
- | MySQL `onOperation` / retry | sync `MySQLBackend` yes; `AsyncMySQLBackend` none | yes (same client for everything) |
474
- | 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` |
475
- | `connection=` without an open transaction | multi-statement ops rely on the driver's implicit transaction | always atomic (own transaction or savepoint) |
476
- | S3 delete failure after `delete()` | raises after the row is gone | never fails the delete; reported via `onOperation` |
477
- | Options | snake_case kwargs | camelCase options object |
459
+ | | Python | npm |
460
+ | -------------------------------------------------------------- | --------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
461
+ | API | sync + async clients | one async client |
462
+ | `optimize` | Pillow; EXIF stripped, not applied | sharp; EXIF **applied then stripped**; transparency → **white** for JPEG |
463
+ | JPEG encoder in `optimize` | Pillow, progressive + optimized | sharp with `mozjpeg`, progressive: on smooth photos roughly **half the size** at about the same SSIM (measured); WebP and PNG match |
464
+ | JPEG missing only its last 1-2 bytes (the end-of-image marker) | accepted (Pillow reads every pixel) | **rejected** (libvips treats a premature end as an error); truncated interlaced PNGs are rejected by both since npm 0.10.0 |
465
+ | `putMany` inserts | pipelined | one transaction, statements run sequentially |
466
+ | Formats | JPEG, PNG, WebP, HEIC (`[heic]` extra) | JPEG, PNG, WebP, HEIC (optional `libheif-js`) |
467
+ | 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` |
468
+ | Event duration | `duration_seconds` | `durationMs` |
469
+ | `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 |
470
+ | `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) |
471
+ | 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) |
472
+ | SQLite `onOperation` / busy retry | none | `onOperation` events and async `SQLITE_BUSY` retry (`busyTimeoutMs`) |
473
+ | 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 |
474
+ | MySQL `onOperation` / retry | sync `MySQLBackend` yes; `AsyncMySQLBackend` none | yes (same client for everything) |
475
+ | 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` |
476
+ | `connection=` without an open transaction | multi-statement ops rely on the driver's implicit transaction | always atomic (own transaction or savepoint) |
477
+ | S3 delete failure after `delete()` | raises after the row is gone | never fails the delete; reported via `onOperation` |
478
+ | Options | snake_case kwargs | camelCase options object |
478
479
 
479
480
  Both packages treat a malformed id as "not found" on every backend (Python 0.26.0+, npm 0.9.0+) and never pass one to a
480
481
  `before_get`/`beforeGet` hook. Both packages now leave a tiered image's object in place on `delete(id, connection=...)` (a rolled-back transaction can
@@ -484,19 +485,22 @@ MySQL `ALTER TABLE` at startup when the schema is current.
484
485
  ## Performance
485
486
 
486
487
  Measured on one single-core container, so treat it as directional; the full tables (PostgreSQL, SQLite, MariaDB, per size,
487
- memory, event-loop impact, throughput) are in the
488
+ memory, event-loop impact, throughput, and the 0.9.0 to 0.10.0 before and after) are in the
488
489
  [benchmark results](https://github.com/KedarGhadyalji/ZeroBucket/blob/main/benchmarks/node/RESULTS.md).
489
490
 
490
- | Image size | `put()` (PostgreSQL) | `get()` | Notes |
491
- | ------------ | -------------------- | ----------- | ------------------------------------------------------------------- |
492
- | up to ~500KB | under 60ms | under 5ms | Feels like any other write. |
493
- | 0.5-1.5MB | about 60-160ms | under 10ms | Fine for uploads. |
494
- | about 5MB | about 0.6s | about 45ms | About 58MB of transient memory per upload. |
495
- | about 10MB | about 1.2-1.4s | about 120ms | About 130MB of transient memory per upload. Default limit is 8 MiB. |
496
-
497
- - `put()` time is mostly image validation (a full decode), so it follows the image's pixel count, not the database you pick.
498
- - The decode runs off the main thread: the event loop stalled at most about 16ms during a 10MB `put()`.
499
- - Memory is the decoded image, roughly 11x the file size for a 45-megapixel JPEG. Cap `maxBytes` and `maxPixels` on public endpoints.
491
+ | Image size | `put()` (PostgreSQL) | `get()` | Notes |
492
+ | ------------ | -------------------- | ----------- | ----------------------------------------------------------------- |
493
+ | up to ~500KB | under 20ms | under 5ms | Feels like any other write. |
494
+ | 0.5-1.5MB | about 20-45ms | under 10ms | Fine for uploads, including in a loop. |
495
+ | about 5MB | about 170ms | about 50ms | A few MB of transient memory for baseline JPEG and PNG. |
496
+ | about 10MB | about 350ms | about 135ms | Default limit is 8 MiB. Memory depends on the format (see below). |
497
+
498
+ - Since 0.10.0, `put()` is 2.5-4x faster than 0.9.x and about as fast as the Python package for large images. Validation is now
499
+ roughly half of `put()` on PostgreSQL.
500
+ - The decode runs off the main thread: the event loop stalled at most about 14ms during a 10MB `put()`.
501
+ - Transient memory depends on the format. A baseline JPEG or non-interlaced PNG validates in streaming fashion (about 11MB for a
502
+ 45-megapixel JPEG, down from about 148MB in 0.9.x). WebP, progressive JPEG and interlaced PNG still need roughly the decoded
503
+ image in memory, so cap `maxBytes` and `maxPixels` on public endpoints.
500
504
  - Stored size equals the file size: JPEG, PNG and WebP are already compressed, so the database cannot shrink them further.
501
505
  - Compression quality of `optimize` is measured in the
502
506
  [compression results](https://github.com/KedarGhadyalji/ZeroBucket/blob/main/benchmarks/node/COMPRESSION_RESULTS.md).
package/dist/index.cjs CHANGED
@@ -3197,14 +3197,16 @@ async function validateImage(data, opts) {
3197
3197
  if (!width || !height)
3198
3198
  throw new CorruptedImageError("Image has no readable dimensions");
3199
3199
  try {
3200
- await make().stats();
3200
+ await sharp(data, {
3201
+ limitInputPixels: maxPixels,
3202
+ failOn: "error",
3203
+ sequentialRead: true
3204
+ }).extract({ left: 0, top: 0, width: 1, height }).raw().toBuffer();
3201
3205
  } catch (exc) {
3202
3206
  if (isPixelLimit(exc)) throw new ImageTooLargeError(sizeBytes, maxBytes);
3203
3207
  throw new CorruptedImageError(
3204
- `Image data is truncated or corrupted: ${errMessage(exc)}`,
3205
- {
3206
- cause: exc
3207
- }
3208
+ `Image data is truncated or corrupted: ${decodeDetail(exc)}`,
3209
+ { cause: exc }
3208
3210
  );
3209
3211
  }
3210
3212
  return { mimeType: FORMAT_TO_MIME[format], width, height, sizeBytes };
@@ -3248,6 +3250,10 @@ async function validateHeic(data, {
3248
3250
  }
3249
3251
  return { mimeType: FORMAT_TO_MIME.heic, width, height, sizeBytes };
3250
3252
  }
3253
+ function decodeDetail(exc) {
3254
+ const msg3 = errMessage(exc).trim();
3255
+ return /^unknown error$/i.test(msg3) ? "the decoder stopped before the end of the image" : msg3;
3256
+ }
3251
3257
  function isPixelLimit(exc) {
3252
3258
  return /pixel limit/i.test(errMessage(exc));
3253
3259
  }