zerobucket 0.4.0 → 0.5.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/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { S as StorageBackend, O as OperationEvent, a as ObjectStorageLike, P as PreparedRow, C as CallOptions, b as StoredRecord, c as StoredRecordMetadata, d as StreamOptions, e as ContentValidator, V as ValidatedContent } from './client-WZ4VRCPi.cjs';
2
- export { B as BatchDeleteResult, f as BatchGetResult, g as BatchPutResult, h as BeforeGetHook, i as BeforePutHook, j as Connection, D as DEFAULT_MAX_BYTES, G as GetOptions, I as Image, k as ImageInput, l as ImageMetadata, m as ObjectStorage, n as ObjectStorageOptions, o as OptimizationResult, p as OptimizeOptions, q as PutManyOptions, r as PutOptions, Q as Queryable, s as SqliteDatabaseLike, t as StreamOptions, W as WritableLike, Z as ZeroBucket, u as ZeroBucketOptions, v as optimizeImage, w as readInput } from './client-WZ4VRCPi.cjs';
1
+ import { S as StorageBackend, O as OperationEvent, a as ObjectStorageLike, P as PreparedRow, C as CallOptions, b as StoredRecord, c as StoredRecordMetadata, d as StreamOptions, e as StorageInfo, I as ImageListOptions, f as ImageListEntry, g as ContentValidator, V as ValidatedContent } from './client-C7xs1dlr.cjs';
2
+ export { B as BatchDeleteResult, h as BatchGetResult, i as BatchPutResult, j as BeforeGetHook, k as BeforePutHook, l as Connection, D as DEFAULT_MAX_BYTES, G as GetOptions, m as Image, n as ImageInput, o as ImageMetadata, p as ObjectStorage, q as ObjectStorageOptions, r as OptimizationResult, s as OptimizeOptions, t as PutManyOptions, u as PutOptions, Q as Queryable, v as SqliteDatabaseLike, w as StreamOptions, W as WritableLike, Z as ZeroBucket, x as ZeroBucketOptions, y as optimizeImage, z as readInput } from './client-C7xs1dlr.cjs';
3
3
  import { Pool } from 'pg';
4
4
 
5
5
  /**
@@ -149,6 +149,8 @@ declare class PostgresBackend implements StorageBackend {
149
149
  delete(id: string, { connection }?: CallOptions): Promise<boolean>;
150
150
  deleteMany(ids: string[], { connection }?: CallOptions): Promise<string[]>;
151
151
  exists(id: string, { connection }?: CallOptions): Promise<boolean>;
152
+ getInfo(): Promise<StorageInfo | null>;
153
+ listImages(o?: ImageListOptions): Promise<ImageListEntry[]>;
152
154
  private deleteDedup;
153
155
  private deleteManyDedup;
154
156
  /**
@@ -253,6 +255,8 @@ declare class SQLiteBackend implements StorageBackend {
253
255
  delete(id: string, { connection }?: CallOptions): Promise<boolean>;
254
256
  deleteMany(ids: string[], { connection }?: CallOptions): Promise<string[]>;
255
257
  exists(id: string, { connection }?: CallOptions): Promise<boolean>;
258
+ getInfo(): Promise<StorageInfo | null>;
259
+ listImages(o?: ImageListOptions): Promise<ImageListEntry[]>;
256
260
  /**
257
261
  * Remove tiered objects AFTER their rows are gone (a failed object delete leaves a harmless orphan; the
258
262
  * reverse order could leave a row pointing at nothing). Skipped inside a caller-owned transaction, since the
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { S as StorageBackend, O as OperationEvent, a as ObjectStorageLike, P as PreparedRow, C as CallOptions, b as StoredRecord, c as StoredRecordMetadata, d as StreamOptions, e as ContentValidator, V as ValidatedContent } from './client-WZ4VRCPi.js';
2
- export { B as BatchDeleteResult, f as BatchGetResult, g as BatchPutResult, h as BeforeGetHook, i as BeforePutHook, j as Connection, D as DEFAULT_MAX_BYTES, G as GetOptions, I as Image, k as ImageInput, l as ImageMetadata, m as ObjectStorage, n as ObjectStorageOptions, o as OptimizationResult, p as OptimizeOptions, q as PutManyOptions, r as PutOptions, Q as Queryable, s as SqliteDatabaseLike, t as StreamOptions, W as WritableLike, Z as ZeroBucket, u as ZeroBucketOptions, v as optimizeImage, w as readInput } from './client-WZ4VRCPi.js';
1
+ import { S as StorageBackend, O as OperationEvent, a as ObjectStorageLike, P as PreparedRow, C as CallOptions, b as StoredRecord, c as StoredRecordMetadata, d as StreamOptions, e as StorageInfo, I as ImageListOptions, f as ImageListEntry, g as ContentValidator, V as ValidatedContent } from './client-C7xs1dlr.js';
2
+ export { B as BatchDeleteResult, h as BatchGetResult, i as BatchPutResult, j as BeforeGetHook, k as BeforePutHook, l as Connection, D as DEFAULT_MAX_BYTES, G as GetOptions, m as Image, n as ImageInput, o as ImageMetadata, p as ObjectStorage, q as ObjectStorageOptions, r as OptimizationResult, s as OptimizeOptions, t as PutManyOptions, u as PutOptions, Q as Queryable, v as SqliteDatabaseLike, w as StreamOptions, W as WritableLike, Z as ZeroBucket, x as ZeroBucketOptions, y as optimizeImage, z as readInput } from './client-C7xs1dlr.js';
3
3
  import { Pool } from 'pg';
4
4
 
5
5
  /**
@@ -149,6 +149,8 @@ declare class PostgresBackend implements StorageBackend {
149
149
  delete(id: string, { connection }?: CallOptions): Promise<boolean>;
150
150
  deleteMany(ids: string[], { connection }?: CallOptions): Promise<string[]>;
151
151
  exists(id: string, { connection }?: CallOptions): Promise<boolean>;
152
+ getInfo(): Promise<StorageInfo | null>;
153
+ listImages(o?: ImageListOptions): Promise<ImageListEntry[]>;
152
154
  private deleteDedup;
153
155
  private deleteManyDedup;
154
156
  /**
@@ -253,6 +255,8 @@ declare class SQLiteBackend implements StorageBackend {
253
255
  delete(id: string, { connection }?: CallOptions): Promise<boolean>;
254
256
  deleteMany(ids: string[], { connection }?: CallOptions): Promise<string[]>;
255
257
  exists(id: string, { connection }?: CallOptions): Promise<boolean>;
258
+ getInfo(): Promise<StorageInfo | null>;
259
+ listImages(o?: ImageListOptions): Promise<ImageListEntry[]>;
256
260
  /**
257
261
  * Remove tiered objects AFTER their rows are gone (a failed object delete leaves a harmless orphan; the
258
262
  * reverse order could leave a row pointing at nothing). Skipped inside a caller-owned transaction, since the
package/dist/index.js CHANGED
@@ -87,6 +87,17 @@ async function* streamFromObjectStorage(os, id, key, start, end, chunkSize) {
87
87
  offset = last + 1;
88
88
  }
89
89
  }
90
+ function prettySize(bytes) {
91
+ if (bytes < 1024) return `${bytes} bytes`;
92
+ const units = ["kB", "MB", "GB", "TB"];
93
+ let v = bytes / 1024;
94
+ let i = 0;
95
+ while (v >= 1024 && i < units.length - 1) {
96
+ v /= 1024;
97
+ i += 1;
98
+ }
99
+ return `${v.toFixed(1)} ${units[i]}`;
100
+ }
90
101
 
91
102
  // src/adapters/postgres.ts
92
103
  var SCHEMA = `
@@ -721,6 +732,95 @@ var PostgresBackend = class {
721
732
  throw wrap("Failed to check image existence", exc);
722
733
  }
723
734
  }
735
+ // ---- maintenance (CLI) -----------------------------------------------
736
+ async getInfo() {
737
+ try {
738
+ return await this.run("info", void 0, async (q) => {
739
+ const imgTable = this.dedup ? "zerobucket_image_refs" : "zerobucket_images";
740
+ const present = await q.query(
741
+ "SELECT to_regclass($1) AS a, to_regclass('zerobucket_blobs') AS b",
742
+ [imgTable]
743
+ );
744
+ if (!present.rows[0].a || this.dedup && !present.rows[0].b)
745
+ return null;
746
+ const j = this.dedup ? "FROM zerobucket_image_refs r JOIN zerobucket_blobs b ON r.checksum_sha256 = b.checksum_sha256" : "FROM zerobucket_images";
747
+ const size = this.dedup ? "b.size_bytes" : "size_bytes";
748
+ const created = this.dedup ? "r.created_at" : "created_at";
749
+ const mime = this.dedup ? "b.mime_type" : "mime_type";
750
+ const tot = (await q.query(
751
+ `SELECT count(*)::int AS n, coalesce(sum(${size}), 0)::text AS bytes, min(${created})::text AS oldest, max(${created})::text AS newest ${j}`
752
+ )).rows[0];
753
+ const disk = (await q.query(
754
+ this.dedup ? "SELECT pg_size_pretty(pg_total_relation_size('zerobucket_blobs') + pg_total_relation_size('zerobucket_image_refs')) AS s" : "SELECT pg_size_pretty(pg_total_relation_size('zerobucket_images')) AS s"
755
+ )).rows[0].s;
756
+ const fmt = (await q.query(
757
+ `SELECT ${mime} AS m, count(*)::int AS n, coalesce(sum(${size}), 0)::text AS bytes ${j} GROUP BY ${mime} ORDER BY count(*) DESC, ${mime}`
758
+ )).rows;
759
+ const info = {
760
+ mode: this.dedup ? "dedup" : "classic",
761
+ count: tot.n,
762
+ totalBytes: Number(tot.bytes),
763
+ // Postgres prints the offset as "+00"; Python's CLI (and ISO-8601) print "+00:00". Normalise so output is identical.
764
+ oldest: tot.oldest && tot.oldest.replace(/([+-]\d\d)$/, "$1:00"),
765
+ newest: tot.newest && tot.newest.replace(/([+-]\d\d)$/, "$1:00"),
766
+ onDisk: disk,
767
+ byFormat: fmt.map((r) => ({
768
+ mimeType: r.m,
769
+ count: r.n,
770
+ bytes: Number(r.bytes)
771
+ }))
772
+ };
773
+ if (this.dedup) {
774
+ const b = (await q.query(
775
+ "SELECT count(*)::int AS n, coalesce(sum(size_bytes), 0)::text AS bytes FROM zerobucket_blobs"
776
+ )).rows[0];
777
+ info.dedup = { blobs: b.n, storedBytes: Number(b.bytes) };
778
+ } else {
779
+ const t = (await q.query(
780
+ "SELECT count(*)::int AS n, coalesce(sum(size_bytes), 0)::text AS bytes FROM zerobucket_images WHERE storage_backend = 'object_storage'"
781
+ )).rows[0];
782
+ info.tiered = { count: t.n, bytes: Number(t.bytes) };
783
+ }
784
+ return info;
785
+ });
786
+ } catch (exc) {
787
+ throw wrap("Failed to read storage info", exc);
788
+ }
789
+ }
790
+ async listImages(o = {}) {
791
+ const where = [];
792
+ const params = [];
793
+ const add = (sql2, v) => (params.push(v), where.push(sql2.replace("$?", `$${params.length}`)));
794
+ if (o.minSize !== void 0)
795
+ add(`${this.dedup ? "b.size_bytes" : "size_bytes"} >= $?`, o.minSize);
796
+ if (o.olderThanDays !== void 0)
797
+ add(
798
+ `${this.dedup ? "r.created_at" : "created_at"} <= now() - make_interval(days => $?::int)`,
799
+ o.olderThanDays
800
+ );
801
+ if (o.onlyUntiered && !this.dedup)
802
+ where.push("storage_backend = 'postgres'");
803
+ const from = this.dedup ? "FROM zerobucket_image_refs r JOIN zerobucket_blobs b ON r.checksum_sha256 = b.checksum_sha256" : "FROM zerobucket_images";
804
+ const idCol = this.dedup ? "r.id" : "id";
805
+ const tierCol = this.dedup ? "false AS tiered" : "(storage_backend = 'object_storage') AS tiered";
806
+ const order = o.sample !== void 0 ? "ORDER BY random()" : `ORDER BY ${this.dedup ? "r.created_at" : "created_at"}, ${idCol}`;
807
+ const lim = o.sample ?? o.limit;
808
+ let sql = `SELECT ${idCol} AS id, ${tierCol} ${from}${where.length ? " WHERE " + where.join(" AND ") : ""} ${order}`;
809
+ if (lim !== void 0) {
810
+ params.push(lim);
811
+ sql += ` LIMIT $${params.length}`;
812
+ }
813
+ try {
814
+ const rows = await this.run(
815
+ "list_images",
816
+ void 0,
817
+ async (q) => (await q.query(sql, params)).rows
818
+ );
819
+ return rows.map((r) => ({ id: String(r.id), tiered: Boolean(r.tiered) }));
820
+ } catch (exc) {
821
+ throw wrap("Failed to list images", exc);
822
+ }
823
+ }
724
824
  // ---- dedup deletes / migration ---------------------------------------
725
825
  async deleteDedup(id, connection) {
726
826
  try {
@@ -1002,6 +1102,7 @@ function isQueryable(c) {
1002
1102
 
1003
1103
  // src/adapters/sqlite.ts
1004
1104
  import { randomUUID } from "crypto";
1105
+ import { existsSync, statSync } from "fs";
1005
1106
  var SCHEMA2 = `
1006
1107
  CREATE TABLE IF NOT EXISTS zerobucket_images (
1007
1108
  id TEXT PRIMARY KEY,
@@ -1588,6 +1689,96 @@ var SQLiteBackend = class {
1588
1689
  throw wrap("Failed to check image existence", exc);
1589
1690
  }
1590
1691
  }
1692
+ // ---- maintenance (CLI) -----------------------------------------------
1693
+ async getInfo() {
1694
+ try {
1695
+ return await this.execute("info", void 0, "read", (db) => {
1696
+ const has = (t) => db.prepare(
1697
+ "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?"
1698
+ ).get(t) !== void 0;
1699
+ if (!has(this.dedup ? "zerobucket_image_refs" : "zerobucket_images") || this.dedup && !has("zerobucket_blobs"))
1700
+ return null;
1701
+ const j = this.dedup ? D_FROM2 : "FROM zerobucket_images";
1702
+ const size = this.dedup ? "b.size_bytes" : "size_bytes";
1703
+ const created = this.dedup ? "r.created_at" : "created_at";
1704
+ const mime = this.dedup ? "b.mime_type" : "mime_type";
1705
+ const tot = db.prepare(
1706
+ `SELECT count(*) AS n, coalesce(sum(${size}), 0) AS bytes, min(${created}) AS oldest, max(${created}) AS newest ${j}`
1707
+ ).get();
1708
+ const fmt = db.prepare(
1709
+ `SELECT ${mime} AS m, count(*) AS n, coalesce(sum(${size}), 0) AS bytes ${j} GROUP BY ${mime} ORDER BY count(*) DESC, ${mime}`
1710
+ ).all();
1711
+ let onDisk = null;
1712
+ if (this.opts.path !== ":memory:") {
1713
+ let total = statSync(this.opts.path).size;
1714
+ for (const suffix of ["-wal", "-shm"])
1715
+ if (existsSync(this.opts.path + suffix))
1716
+ total += statSync(this.opts.path + suffix).size;
1717
+ onDisk = prettySize(total);
1718
+ }
1719
+ const info = {
1720
+ mode: this.dedup ? "dedup" : "classic",
1721
+ count: tot.n,
1722
+ totalBytes: Number(tot.bytes),
1723
+ oldest: tot.oldest,
1724
+ newest: tot.newest,
1725
+ onDisk,
1726
+ byFormat: fmt.map((r) => ({
1727
+ mimeType: r.m,
1728
+ count: r.n,
1729
+ bytes: Number(r.bytes)
1730
+ }))
1731
+ };
1732
+ if (this.dedup) {
1733
+ const b = db.prepare(
1734
+ "SELECT count(*) AS n, coalesce(sum(size_bytes), 0) AS bytes FROM zerobucket_blobs"
1735
+ ).get();
1736
+ info.dedup = { blobs: b.n, storedBytes: Number(b.bytes) };
1737
+ } else {
1738
+ const t = db.prepare(
1739
+ "SELECT count(*) AS n, coalesce(sum(size_bytes), 0) AS bytes FROM zerobucket_images WHERE storage_backend = 'object_storage'"
1740
+ ).get();
1741
+ info.tiered = { count: t.n, bytes: Number(t.bytes) };
1742
+ }
1743
+ return info;
1744
+ });
1745
+ } catch (exc) {
1746
+ throw wrap("Failed to read storage info", exc);
1747
+ }
1748
+ }
1749
+ async listImages(o = {}) {
1750
+ const where = [];
1751
+ const params = [];
1752
+ if (o.minSize !== void 0)
1753
+ where.push(`${this.dedup ? "b.size_bytes" : "size_bytes"} >= ?`), params.push(o.minSize);
1754
+ if (o.olderThanDays !== void 0) {
1755
+ const cutoff = new Date(Date.now() - o.olderThanDays * 864e5).toISOString().replace("Z", "000+00:00");
1756
+ where.push(`${this.dedup ? "r.created_at" : "created_at"} <= ?`);
1757
+ params.push(cutoff);
1758
+ }
1759
+ if (o.onlyUntiered && !this.dedup) where.push("storage_backend = 'sqlite'");
1760
+ const from = this.dedup ? D_FROM2 : "FROM zerobucket_images";
1761
+ const idCol = this.dedup ? "r.id" : "id";
1762
+ const tierCol = this.dedup ? "0 AS tiered" : "(storage_backend = 'object_storage') AS tiered";
1763
+ const order = o.sample !== void 0 ? "ORDER BY random()" : `ORDER BY ${this.dedup ? "r.created_at" : "created_at"}, ${idCol}`;
1764
+ const lim = o.sample ?? o.limit;
1765
+ let sql = `SELECT ${idCol} AS id, ${tierCol} ${from}${where.length ? " WHERE " + where.join(" AND ") : ""} ${order}`;
1766
+ if (lim !== void 0) sql += " LIMIT ?", params.push(lim);
1767
+ try {
1768
+ const rows = await this.execute(
1769
+ "list_images",
1770
+ void 0,
1771
+ "read",
1772
+ (db) => db.prepare(sql).all(...params)
1773
+ );
1774
+ return rows.map((r) => ({
1775
+ id: String(r.id),
1776
+ tiered: Boolean(r.tiered)
1777
+ }));
1778
+ } catch (exc) {
1779
+ throw wrap("Failed to list images", exc);
1780
+ }
1781
+ }
1591
1782
  /**
1592
1783
  * Remove tiered objects AFTER their rows are gone (a failed object delete leaves a harmless orphan; the
1593
1784
  * reverse order could leave a row pointing at nothing). Skipped inside a caller-owned transaction, since the