@dexto/storage 1.6.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.
Files changed (158) hide show
  1. package/LICENSE +44 -0
  2. package/README.md +80 -0
  3. package/dist/blob/factories/index.cjs +31 -0
  4. package/dist/blob/factories/index.d.cts +6 -0
  5. package/dist/blob/factories/index.d.ts +6 -0
  6. package/dist/blob/factories/index.d.ts.map +1 -0
  7. package/dist/blob/factories/index.js +6 -0
  8. package/dist/blob/factories/local.cjs +38 -0
  9. package/dist/blob/factories/local.d.cts +21 -0
  10. package/dist/blob/factories/local.d.ts +17 -0
  11. package/dist/blob/factories/local.d.ts.map +1 -0
  12. package/dist/blob/factories/local.js +14 -0
  13. package/dist/blob/factories/memory.cjs +44 -0
  14. package/dist/blob/factories/memory.d.cts +21 -0
  15. package/dist/blob/factories/memory.d.ts +17 -0
  16. package/dist/blob/factories/memory.d.ts.map +1 -0
  17. package/dist/blob/factories/memory.js +20 -0
  18. package/dist/blob/factory.cjs +16 -0
  19. package/dist/blob/factory.d.cts +36 -0
  20. package/dist/blob/factory.d.ts +35 -0
  21. package/dist/blob/factory.d.ts.map +1 -0
  22. package/dist/blob/factory.js +0 -0
  23. package/dist/blob/index.cjs +45 -0
  24. package/dist/blob/index.d.cts +8 -0
  25. package/dist/blob/index.d.ts +26 -0
  26. package/dist/blob/index.d.ts.map +1 -0
  27. package/dist/blob/index.js +19 -0
  28. package/dist/blob/local-blob-store.cjs +532 -0
  29. package/dist/blob/local-blob-store.d.cts +56 -0
  30. package/dist/blob/local-blob-store.d.ts +54 -0
  31. package/dist/blob/local-blob-store.d.ts.map +1 -0
  32. package/dist/blob/local-blob-store.js +498 -0
  33. package/dist/blob/memory-blob-store.cjs +327 -0
  34. package/dist/blob/memory-blob-store.d.cts +69 -0
  35. package/dist/blob/memory-blob-store.d.ts +67 -0
  36. package/dist/blob/memory-blob-store.d.ts.map +1 -0
  37. package/dist/blob/memory-blob-store.js +303 -0
  38. package/dist/blob/schemas.cjs +52 -0
  39. package/dist/blob/schemas.d.cts +87 -0
  40. package/dist/blob/schemas.d.ts +86 -0
  41. package/dist/blob/schemas.d.ts.map +1 -0
  42. package/dist/blob/schemas.js +25 -0
  43. package/dist/blob/types.cjs +16 -0
  44. package/dist/blob/types.d.cts +1 -0
  45. package/dist/blob/types.d.ts +2 -0
  46. package/dist/blob/types.d.ts.map +1 -0
  47. package/dist/blob/types.js +0 -0
  48. package/dist/cache/factories/index.cjs +31 -0
  49. package/dist/cache/factories/index.d.cts +6 -0
  50. package/dist/cache/factories/index.d.ts +6 -0
  51. package/dist/cache/factories/index.d.ts.map +1 -0
  52. package/dist/cache/factories/index.js +6 -0
  53. package/dist/cache/factories/memory.cjs +39 -0
  54. package/dist/cache/factories/memory.d.cts +21 -0
  55. package/dist/cache/factories/memory.d.ts +17 -0
  56. package/dist/cache/factories/memory.d.ts.map +1 -0
  57. package/dist/cache/factories/memory.js +15 -0
  58. package/dist/cache/factories/redis.cjs +65 -0
  59. package/dist/cache/factories/redis.d.cts +24 -0
  60. package/dist/cache/factories/redis.d.ts +20 -0
  61. package/dist/cache/factories/redis.d.ts.map +1 -0
  62. package/dist/cache/factories/redis.js +31 -0
  63. package/dist/cache/factory.cjs +16 -0
  64. package/dist/cache/factory.d.cts +42 -0
  65. package/dist/cache/factory.d.ts +41 -0
  66. package/dist/cache/factory.d.ts.map +1 -0
  67. package/dist/cache/factory.js +0 -0
  68. package/dist/cache/index.cjs +42 -0
  69. package/dist/cache/index.d.cts +7 -0
  70. package/dist/cache/index.d.ts +25 -0
  71. package/dist/cache/index.d.ts.map +1 -0
  72. package/dist/cache/index.js +17 -0
  73. package/dist/cache/memory-cache-store.cjs +106 -0
  74. package/dist/cache/memory-cache-store.d.cts +27 -0
  75. package/dist/cache/memory-cache-store.d.ts +25 -0
  76. package/dist/cache/memory-cache-store.d.ts.map +1 -0
  77. package/dist/cache/memory-cache-store.js +82 -0
  78. package/dist/cache/redis-store.cjs +176 -0
  79. package/dist/cache/redis-store.d.cts +34 -0
  80. package/dist/cache/redis-store.d.ts +32 -0
  81. package/dist/cache/redis-store.d.ts.map +1 -0
  82. package/dist/cache/redis-store.js +152 -0
  83. package/dist/cache/schemas.cjs +70 -0
  84. package/dist/cache/schemas.d.cts +93 -0
  85. package/dist/cache/schemas.d.ts +91 -0
  86. package/dist/cache/schemas.d.ts.map +1 -0
  87. package/dist/cache/schemas.js +43 -0
  88. package/dist/cache/types.cjs +16 -0
  89. package/dist/cache/types.d.cts +1 -0
  90. package/dist/cache/types.d.ts +2 -0
  91. package/dist/cache/types.d.ts.map +1 -0
  92. package/dist/cache/types.js +0 -0
  93. package/dist/database/factories/index.cjs +34 -0
  94. package/dist/database/factories/index.d.cts +7 -0
  95. package/dist/database/factories/index.d.ts +7 -0
  96. package/dist/database/factories/index.d.ts.map +1 -0
  97. package/dist/database/factories/index.js +8 -0
  98. package/dist/database/factories/memory.cjs +39 -0
  99. package/dist/database/factories/memory.d.cts +20 -0
  100. package/dist/database/factories/memory.d.ts +16 -0
  101. package/dist/database/factories/memory.d.ts.map +1 -0
  102. package/dist/database/factories/memory.js +15 -0
  103. package/dist/database/factories/postgres.cjs +61 -0
  104. package/dist/database/factories/postgres.d.cts +23 -0
  105. package/dist/database/factories/postgres.d.ts +19 -0
  106. package/dist/database/factories/postgres.d.ts.map +1 -0
  107. package/dist/database/factories/postgres.js +27 -0
  108. package/dist/database/factories/sqlite.cjs +65 -0
  109. package/dist/database/factories/sqlite.d.cts +24 -0
  110. package/dist/database/factories/sqlite.d.ts +20 -0
  111. package/dist/database/factories/sqlite.d.ts.map +1 -0
  112. package/dist/database/factories/sqlite.js +31 -0
  113. package/dist/database/factory.cjs +16 -0
  114. package/dist/database/factory.d.cts +42 -0
  115. package/dist/database/factory.d.ts +41 -0
  116. package/dist/database/factory.d.ts.map +1 -0
  117. package/dist/database/factory.js +0 -0
  118. package/dist/database/index.cjs +46 -0
  119. package/dist/database/index.d.cts +8 -0
  120. package/dist/database/index.d.ts +26 -0
  121. package/dist/database/index.d.ts.map +1 -0
  122. package/dist/database/index.js +24 -0
  123. package/dist/database/memory-database-store.cjs +121 -0
  124. package/dist/database/memory-database-store.d.cts +30 -0
  125. package/dist/database/memory-database-store.d.ts +28 -0
  126. package/dist/database/memory-database-store.d.ts.map +1 -0
  127. package/dist/database/memory-database-store.js +97 -0
  128. package/dist/database/postgres-store.cjs +342 -0
  129. package/dist/database/postgres-store.d.cts +57 -0
  130. package/dist/database/postgres-store.d.ts +55 -0
  131. package/dist/database/postgres-store.d.ts.map +1 -0
  132. package/dist/database/postgres-store.js +318 -0
  133. package/dist/database/schemas.cjs +82 -0
  134. package/dist/database/schemas.d.cts +127 -0
  135. package/dist/database/schemas.d.ts +125 -0
  136. package/dist/database/schemas.d.ts.map +1 -0
  137. package/dist/database/schemas.js +54 -0
  138. package/dist/database/sqlite-store.cjs +270 -0
  139. package/dist/database/sqlite-store.d.cts +35 -0
  140. package/dist/database/sqlite-store.d.ts +33 -0
  141. package/dist/database/sqlite-store.d.ts.map +1 -0
  142. package/dist/database/sqlite-store.js +236 -0
  143. package/dist/database/types.cjs +16 -0
  144. package/dist/database/types.d.cts +1 -0
  145. package/dist/database/types.d.ts +2 -0
  146. package/dist/database/types.d.ts.map +1 -0
  147. package/dist/database/types.js +0 -0
  148. package/dist/index.cjs +82 -0
  149. package/dist/index.d.cts +24 -0
  150. package/dist/index.d.ts +25 -0
  151. package/dist/index.d.ts.map +1 -0
  152. package/dist/index.js +50 -0
  153. package/dist/schemas.cjs +67 -0
  154. package/dist/schemas.d.cts +72 -0
  155. package/dist/schemas.d.ts +70 -0
  156. package/dist/schemas.d.ts.map +1 -0
  157. package/dist/schemas.js +46 -0
  158. package/package.json +55 -0
@@ -0,0 +1,532 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var local_blob_store_exports = {};
30
+ __export(local_blob_store_exports, {
31
+ LocalBlobStore: () => LocalBlobStore
32
+ });
33
+ module.exports = __toCommonJS(local_blob_store_exports);
34
+ var import_fs = require("fs");
35
+ var import_path = __toESM(require("path"), 1);
36
+ var import_crypto = require("crypto");
37
+ var import_url = require("url");
38
+ var import_core = require("@dexto/core");
39
+ function isPlainObject(value) {
40
+ return typeof value === "object" && value !== null && !Array.isArray(value);
41
+ }
42
+ function parseStoredBlobMetadata(value) {
43
+ if (!isPlainObject(value)) {
44
+ throw new Error("Invalid blob metadata: expected object");
45
+ }
46
+ const id = value["id"];
47
+ if (typeof id !== "string") {
48
+ throw new Error("Invalid blob metadata: id");
49
+ }
50
+ const mimeType = value["mimeType"];
51
+ if (typeof mimeType !== "string") {
52
+ throw new Error("Invalid blob metadata: mimeType");
53
+ }
54
+ const originalName = value["originalName"];
55
+ if (originalName !== void 0 && typeof originalName !== "string") {
56
+ throw new Error("Invalid blob metadata: originalName");
57
+ }
58
+ const createdAtRaw = value["createdAt"];
59
+ const createdAt = createdAtRaw instanceof Date ? createdAtRaw : typeof createdAtRaw === "string" || typeof createdAtRaw === "number" ? new Date(createdAtRaw) : null;
60
+ if (!createdAt || Number.isNaN(createdAt.getTime())) {
61
+ throw new Error("Invalid blob metadata: createdAt");
62
+ }
63
+ const size = value["size"];
64
+ if (typeof size !== "number") {
65
+ throw new Error("Invalid blob metadata: size");
66
+ }
67
+ const hash = value["hash"];
68
+ if (typeof hash !== "string") {
69
+ throw new Error("Invalid blob metadata: hash");
70
+ }
71
+ const source = value["source"];
72
+ if (source !== void 0 && source !== "tool" && source !== "user" && source !== "system") {
73
+ throw new Error("Invalid blob metadata: source");
74
+ }
75
+ return {
76
+ id,
77
+ mimeType,
78
+ ...originalName !== void 0 && { originalName },
79
+ createdAt,
80
+ size,
81
+ hash,
82
+ ...source !== void 0 && { source }
83
+ };
84
+ }
85
+ class LocalBlobStore {
86
+ config;
87
+ storePath;
88
+ connected = false;
89
+ statsCache = null;
90
+ statsCachePromise = null;
91
+ lastStatsRefresh = 0;
92
+ logger;
93
+ static STATS_REFRESH_INTERVAL_MS = 6e4;
94
+ // 1 minute
95
+ constructor(config, logger) {
96
+ this.config = config;
97
+ this.storePath = config.storePath;
98
+ this.logger = logger.createChild(import_core.DextoLogComponent.STORAGE);
99
+ }
100
+ async connect() {
101
+ if (this.connected) return;
102
+ try {
103
+ await import_fs.promises.mkdir(this.storePath, { recursive: true });
104
+ await this.refreshStatsCache();
105
+ this.lastStatsRefresh = Date.now();
106
+ this.connected = true;
107
+ this.logger.debug(`LocalBlobStore connected at: ${this.storePath}`);
108
+ } catch (error) {
109
+ throw import_core.StorageError.blobOperationFailed("connect", "local", error);
110
+ }
111
+ }
112
+ async disconnect() {
113
+ this.connected = false;
114
+ this.logger.debug("LocalBlobStore disconnected");
115
+ }
116
+ isConnected() {
117
+ return this.connected;
118
+ }
119
+ getStoreType() {
120
+ return "local";
121
+ }
122
+ async store(input, metadata = {}) {
123
+ if (!this.connected) {
124
+ throw import_core.StorageError.blobBackendNotConnected("local");
125
+ }
126
+ const buffer = await this.inputToBuffer(input);
127
+ if (buffer.length > this.config.maxBlobSize) {
128
+ throw import_core.StorageError.blobSizeExceeded(buffer.length, this.config.maxBlobSize);
129
+ }
130
+ const hash = (0, import_crypto.createHash)("sha256").update(buffer).digest("hex").substring(0, 16);
131
+ const id = hash;
132
+ const blobPath = import_path.default.join(this.storePath, `${id}.dat`);
133
+ const metaPath = import_path.default.join(this.storePath, `${id}.meta.json`);
134
+ try {
135
+ const existingMeta = await import_fs.promises.readFile(metaPath, "utf-8");
136
+ const parsed = JSON.parse(existingMeta);
137
+ const existingMetadata = parseStoredBlobMetadata(parsed);
138
+ this.logger.debug(
139
+ `Blob ${id} already exists, returning existing reference (deduplication)`
140
+ );
141
+ return {
142
+ id,
143
+ uri: `blob:${id}`,
144
+ metadata: existingMetadata
145
+ };
146
+ } catch {
147
+ }
148
+ const maxTotalSize = this.config.maxTotalSize;
149
+ if (maxTotalSize) {
150
+ const stats = await this.ensureStatsCache();
151
+ if (stats.totalSize + buffer.length > maxTotalSize) {
152
+ throw import_core.StorageError.blobTotalSizeExceeded(
153
+ stats.totalSize + buffer.length,
154
+ maxTotalSize
155
+ );
156
+ }
157
+ }
158
+ const storedMetadata = {
159
+ id,
160
+ mimeType: metadata.mimeType || this.detectMimeType(buffer, metadata.originalName),
161
+ originalName: metadata.originalName,
162
+ createdAt: metadata.createdAt || /* @__PURE__ */ new Date(),
163
+ source: metadata.source || "system",
164
+ size: buffer.length,
165
+ hash
166
+ };
167
+ try {
168
+ await Promise.all([
169
+ import_fs.promises.writeFile(blobPath, buffer),
170
+ import_fs.promises.writeFile(metaPath, JSON.stringify(storedMetadata, null, 2))
171
+ ]);
172
+ this.logger.debug(
173
+ `Stored blob ${id} (${buffer.length} bytes, ${storedMetadata.mimeType})`
174
+ );
175
+ this.updateStatsCacheAfterStore(buffer.length);
176
+ return {
177
+ id,
178
+ uri: `blob:${id}`,
179
+ metadata: storedMetadata
180
+ };
181
+ } catch (error) {
182
+ await Promise.allSettled([
183
+ import_fs.promises.unlink(blobPath).catch(() => {
184
+ }),
185
+ import_fs.promises.unlink(metaPath).catch(() => {
186
+ })
187
+ ]);
188
+ throw import_core.StorageError.blobOperationFailed("store", "local", error);
189
+ }
190
+ }
191
+ async retrieve(reference, format = "buffer") {
192
+ if (!this.connected) {
193
+ throw import_core.StorageError.blobBackendNotConnected("local");
194
+ }
195
+ const id = this.parseReference(reference);
196
+ const blobPath = import_path.default.join(this.storePath, `${id}.dat`);
197
+ const metaPath = import_path.default.join(this.storePath, `${id}.meta.json`);
198
+ try {
199
+ const metaContent = await import_fs.promises.readFile(metaPath, "utf-8");
200
+ const parsed = JSON.parse(metaContent);
201
+ const metadata = parseStoredBlobMetadata(parsed);
202
+ switch (format) {
203
+ case "base64": {
204
+ const buffer = await import_fs.promises.readFile(blobPath);
205
+ return { format: "base64", data: buffer.toString("base64"), metadata };
206
+ }
207
+ case "buffer": {
208
+ const buffer = await import_fs.promises.readFile(blobPath);
209
+ return { format: "buffer", data: buffer, metadata };
210
+ }
211
+ case "path": {
212
+ await import_fs.promises.access(blobPath);
213
+ return { format: "path", data: blobPath, metadata };
214
+ }
215
+ case "stream": {
216
+ const stream = (0, import_fs.createReadStream)(blobPath);
217
+ return { format: "stream", data: stream, metadata };
218
+ }
219
+ case "url": {
220
+ const absolutePath = import_path.default.resolve(blobPath);
221
+ return {
222
+ format: "url",
223
+ data: (0, import_url.pathToFileURL)(absolutePath).toString(),
224
+ metadata
225
+ };
226
+ }
227
+ default:
228
+ throw import_core.StorageError.blobInvalidInput(format, `Unsupported format: ${format}`);
229
+ }
230
+ } catch (error) {
231
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
232
+ throw import_core.StorageError.blobNotFound(reference);
233
+ }
234
+ throw import_core.StorageError.blobOperationFailed("retrieve", "local", error);
235
+ }
236
+ }
237
+ async exists(reference) {
238
+ if (!this.connected) {
239
+ throw import_core.StorageError.blobBackendNotConnected("local");
240
+ }
241
+ const id = this.parseReference(reference);
242
+ const blobPath = import_path.default.join(this.storePath, `${id}.dat`);
243
+ const metaPath = import_path.default.join(this.storePath, `${id}.meta.json`);
244
+ try {
245
+ await Promise.all([import_fs.promises.access(blobPath), import_fs.promises.access(metaPath)]);
246
+ return true;
247
+ } catch {
248
+ return false;
249
+ }
250
+ }
251
+ async delete(reference) {
252
+ if (!this.connected) {
253
+ throw import_core.StorageError.blobBackendNotConnected("local");
254
+ }
255
+ const id = this.parseReference(reference);
256
+ const blobPath = import_path.default.join(this.storePath, `${id}.dat`);
257
+ const metaPath = import_path.default.join(this.storePath, `${id}.meta.json`);
258
+ try {
259
+ const metaContent = await import_fs.promises.readFile(metaPath, "utf-8");
260
+ const parsed = JSON.parse(metaContent);
261
+ const metadata = parseStoredBlobMetadata(parsed);
262
+ await Promise.all([import_fs.promises.unlink(blobPath), import_fs.promises.unlink(metaPath)]);
263
+ this.logger.debug(`Deleted blob: ${id}`);
264
+ this.updateStatsCacheAfterDelete(metadata.size);
265
+ } catch (error) {
266
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
267
+ throw import_core.StorageError.blobNotFound(reference);
268
+ }
269
+ throw import_core.StorageError.blobOperationFailed("delete", "local", error);
270
+ }
271
+ }
272
+ async cleanup(olderThan) {
273
+ if (!this.connected) {
274
+ throw import_core.StorageError.blobBackendNotConnected("local");
275
+ }
276
+ const cleanupDays = this.config.cleanupAfterDays;
277
+ const cutoffDate = olderThan || new Date(Date.now() - cleanupDays * 24 * 60 * 60 * 1e3);
278
+ let deletedCount = 0;
279
+ try {
280
+ const files = await import_fs.promises.readdir(this.storePath);
281
+ const metaFiles = files.filter((f) => f.endsWith(".meta.json"));
282
+ for (const metaFile of metaFiles) {
283
+ const metaPath = import_path.default.join(this.storePath, metaFile);
284
+ const id = metaFile.replace(".meta.json", "");
285
+ const blobPath = import_path.default.join(this.storePath, `${id}.dat`);
286
+ try {
287
+ const metaContent = await import_fs.promises.readFile(metaPath, "utf-8");
288
+ const parsed = JSON.parse(metaContent);
289
+ const metadata = parseStoredBlobMetadata(parsed);
290
+ if (metadata.createdAt < cutoffDate) {
291
+ await Promise.all([
292
+ import_fs.promises.unlink(blobPath).catch(() => {
293
+ }),
294
+ import_fs.promises.unlink(metaPath).catch(() => {
295
+ })
296
+ ]);
297
+ deletedCount++;
298
+ this.updateStatsCacheAfterDelete(metadata.size);
299
+ this.logger.debug(`Cleaned up old blob: ${id}`);
300
+ }
301
+ } catch (error) {
302
+ this.logger.warn(
303
+ `Failed to process blob metadata ${metaFile}: ${String(error)}`
304
+ );
305
+ }
306
+ }
307
+ if (deletedCount > 0) {
308
+ this.logger.info(`Blob cleanup: removed ${deletedCount} old blobs`);
309
+ }
310
+ return deletedCount;
311
+ } catch (error) {
312
+ throw import_core.StorageError.blobCleanupFailed("local", error);
313
+ }
314
+ }
315
+ async getStats() {
316
+ if (!this.connected) {
317
+ throw import_core.StorageError.blobBackendNotConnected("local");
318
+ }
319
+ const stats = await this.ensureStatsCache();
320
+ return {
321
+ count: stats.count,
322
+ totalSize: stats.totalSize,
323
+ backendType: "local",
324
+ storePath: this.storePath
325
+ };
326
+ }
327
+ getStoragePath() {
328
+ return this.storePath;
329
+ }
330
+ async listBlobs() {
331
+ if (!this.connected) {
332
+ throw import_core.StorageError.blobBackendNotConnected("local");
333
+ }
334
+ try {
335
+ const files = await import_fs.promises.readdir(this.storePath);
336
+ const metaFiles = files.filter((f) => f.endsWith(".meta.json"));
337
+ const blobs = [];
338
+ for (const metaFile of metaFiles) {
339
+ try {
340
+ const metaPath = import_path.default.join(this.storePath, metaFile);
341
+ const metaContent = await import_fs.promises.readFile(metaPath, "utf-8");
342
+ const parsed = JSON.parse(metaContent);
343
+ const metadata = parseStoredBlobMetadata(parsed);
344
+ const blobId = metaFile.replace(".meta.json", "");
345
+ blobs.push({
346
+ id: blobId,
347
+ uri: `blob:${blobId}`,
348
+ metadata
349
+ });
350
+ } catch (error) {
351
+ this.logger.warn(
352
+ `Failed to process blob metadata ${metaFile}: ${String(error)}`
353
+ );
354
+ }
355
+ }
356
+ return blobs;
357
+ } catch (error) {
358
+ this.logger.warn(`Failed to list blobs: ${String(error)}`);
359
+ return [];
360
+ }
361
+ }
362
+ async ensureStatsCache() {
363
+ const now = Date.now();
364
+ const cacheAge = now - this.lastStatsRefresh;
365
+ const isStale = cacheAge > LocalBlobStore.STATS_REFRESH_INTERVAL_MS;
366
+ if (!this.statsCache || isStale) {
367
+ if (!this.statsCachePromise) {
368
+ this.statsCachePromise = this.refreshStatsCache();
369
+ }
370
+ try {
371
+ await this.statsCachePromise;
372
+ this.lastStatsRefresh = now;
373
+ } finally {
374
+ this.statsCachePromise = null;
375
+ }
376
+ }
377
+ return this.statsCache ?? { count: 0, totalSize: 0 };
378
+ }
379
+ async refreshStatsCache() {
380
+ const stats = { count: 0, totalSize: 0 };
381
+ try {
382
+ const files = await import_fs.promises.readdir(this.storePath);
383
+ const datFiles = files.filter((f) => f.endsWith(".dat"));
384
+ stats.count = datFiles.length;
385
+ for (const datFile of datFiles) {
386
+ try {
387
+ const stat = await import_fs.promises.stat(import_path.default.join(this.storePath, datFile));
388
+ stats.totalSize += stat.size;
389
+ } catch (error) {
390
+ this.logger.debug(
391
+ `Skipping size calculation for ${datFile}: ${error instanceof Error ? error.message : String(error)}`
392
+ );
393
+ }
394
+ }
395
+ } catch (error) {
396
+ this.logger.warn(`Failed to refresh blob stats cache: ${String(error)}`);
397
+ }
398
+ this.statsCache = stats;
399
+ }
400
+ updateStatsCacheAfterStore(size) {
401
+ if (!this.statsCache) {
402
+ this.statsCache = { count: 1, totalSize: size };
403
+ return;
404
+ }
405
+ this.statsCache.count += 1;
406
+ this.statsCache.totalSize += size;
407
+ }
408
+ updateStatsCacheAfterDelete(size) {
409
+ if (!this.statsCache) {
410
+ this.statsCache = { count: 0, totalSize: 0 };
411
+ return;
412
+ }
413
+ this.statsCache.count = Math.max(0, this.statsCache.count - 1);
414
+ this.statsCache.totalSize = Math.max(0, this.statsCache.totalSize - size);
415
+ }
416
+ /**
417
+ * Convert various input types to Buffer
418
+ */
419
+ async inputToBuffer(input) {
420
+ if (Buffer.isBuffer(input)) {
421
+ return input;
422
+ }
423
+ if (input instanceof Uint8Array) {
424
+ return Buffer.from(input);
425
+ }
426
+ if (input instanceof ArrayBuffer) {
427
+ return Buffer.from(new Uint8Array(input));
428
+ }
429
+ if (typeof input === "string") {
430
+ if (input.startsWith("data:")) {
431
+ const commaIndex = input.indexOf(",");
432
+ if (commaIndex !== -1 && input.includes(";base64,")) {
433
+ const base64Data = input.substring(commaIndex + 1);
434
+ return Buffer.from(base64Data, "base64");
435
+ }
436
+ throw import_core.StorageError.blobEncodingError(
437
+ "inputToBuffer",
438
+ "Unsupported data URI format"
439
+ );
440
+ }
441
+ if ((input.includes("/") || input.includes("\\")) && input.length > 1) {
442
+ try {
443
+ await import_fs.promises.access(input);
444
+ return await import_fs.promises.readFile(input);
445
+ } catch {
446
+ }
447
+ }
448
+ try {
449
+ return Buffer.from(input, "base64");
450
+ } catch {
451
+ throw import_core.StorageError.blobEncodingError("inputToBuffer", "Invalid base64 string");
452
+ }
453
+ }
454
+ throw import_core.StorageError.blobInvalidInput(input, `Unsupported input type: ${typeof input}`);
455
+ }
456
+ /**
457
+ * Parse blob reference to extract ID
458
+ */
459
+ parseReference(reference) {
460
+ if (!reference) {
461
+ throw import_core.StorageError.blobInvalidReference(reference, "Empty reference");
462
+ }
463
+ if (reference.startsWith("blob:")) {
464
+ const id = reference.substring(5);
465
+ if (!id) {
466
+ throw import_core.StorageError.blobInvalidReference(reference, "Empty blob ID after prefix");
467
+ }
468
+ return id;
469
+ }
470
+ return reference;
471
+ }
472
+ /**
473
+ * Detect MIME type from buffer content and/or filename
474
+ */
475
+ detectMimeType(buffer, filename) {
476
+ const header = buffer.subarray(0, 16);
477
+ if (header.length >= 3) {
478
+ const jpegSignature = header.subarray(0, 3);
479
+ if (jpegSignature.equals(Buffer.from([255, 216, 255]))) {
480
+ return "image/jpeg";
481
+ }
482
+ }
483
+ if (header.length >= 4) {
484
+ const signature = header.subarray(0, 4);
485
+ if (signature.equals(Buffer.from([137, 80, 78, 71]))) return "image/png";
486
+ if (signature.equals(Buffer.from([71, 73, 70, 56]))) return "image/gif";
487
+ if (signature.equals(Buffer.from([37, 80, 68, 70]))) return "application/pdf";
488
+ }
489
+ if (filename) {
490
+ const ext = import_path.default.extname(filename).toLowerCase();
491
+ const mimeTypes = {
492
+ ".jpg": "image/jpeg",
493
+ ".jpeg": "image/jpeg",
494
+ ".png": "image/png",
495
+ ".gif": "image/gif",
496
+ ".pdf": "application/pdf",
497
+ ".txt": "text/plain",
498
+ ".json": "application/json",
499
+ ".xml": "text/xml",
500
+ ".html": "text/html",
501
+ ".css": "text/css",
502
+ ".js": "text/javascript",
503
+ ".mp3": "audio/mpeg",
504
+ ".mp4": "video/mp4",
505
+ ".wav": "audio/wav"
506
+ };
507
+ if (mimeTypes[ext]) return mimeTypes[ext];
508
+ }
509
+ if (this.isTextBuffer(buffer)) {
510
+ return "text/plain";
511
+ }
512
+ return "application/octet-stream";
513
+ }
514
+ /**
515
+ * Check if buffer contains text content
516
+ */
517
+ isTextBuffer(buffer) {
518
+ let printableCount = 0;
519
+ const sampleSize = Math.min(512, buffer.length);
520
+ for (let i = 0; i < sampleSize; i++) {
521
+ const byte = buffer[i];
522
+ if (byte !== void 0 && (byte >= 32 && byte <= 126 || byte === 9 || byte === 10 || byte === 13)) {
523
+ printableCount++;
524
+ }
525
+ }
526
+ return printableCount / sampleSize > 0.7;
527
+ }
528
+ }
529
+ // Annotate the CommonJS export names for ESM import in node:
530
+ 0 && (module.exports = {
531
+ LocalBlobStore
532
+ });
@@ -0,0 +1,56 @@
1
+ import { BlobStore, Logger, BlobInput, BlobMetadata, BlobReference, BlobData, BlobStats } from '@dexto/core';
2
+ import { LocalBlobStoreConfig } from './schemas.cjs';
3
+ import 'zod';
4
+
5
+ /**
6
+ * Local filesystem blob store implementation.
7
+ *
8
+ * Stores blobs on the local filesystem with content-based deduplication
9
+ * and metadata tracking. This is the default store for development
10
+ * and single-machine deployments.
11
+ */
12
+ declare class LocalBlobStore implements BlobStore {
13
+ private config;
14
+ private storePath;
15
+ private connected;
16
+ private statsCache;
17
+ private statsCachePromise;
18
+ private lastStatsRefresh;
19
+ private logger;
20
+ private static readonly STATS_REFRESH_INTERVAL_MS;
21
+ constructor(config: LocalBlobStoreConfig, logger: Logger);
22
+ connect(): Promise<void>;
23
+ disconnect(): Promise<void>;
24
+ isConnected(): boolean;
25
+ getStoreType(): string;
26
+ store(input: BlobInput, metadata?: BlobMetadata): Promise<BlobReference>;
27
+ retrieve(reference: string, format?: 'base64' | 'buffer' | 'path' | 'stream' | 'url'): Promise<BlobData>;
28
+ exists(reference: string): Promise<boolean>;
29
+ delete(reference: string): Promise<void>;
30
+ cleanup(olderThan?: Date): Promise<number>;
31
+ getStats(): Promise<BlobStats>;
32
+ getStoragePath(): string | undefined;
33
+ listBlobs(): Promise<BlobReference[]>;
34
+ private ensureStatsCache;
35
+ private refreshStatsCache;
36
+ private updateStatsCacheAfterStore;
37
+ private updateStatsCacheAfterDelete;
38
+ /**
39
+ * Convert various input types to Buffer
40
+ */
41
+ private inputToBuffer;
42
+ /**
43
+ * Parse blob reference to extract ID
44
+ */
45
+ private parseReference;
46
+ /**
47
+ * Detect MIME type from buffer content and/or filename
48
+ */
49
+ private detectMimeType;
50
+ /**
51
+ * Check if buffer contains text content
52
+ */
53
+ private isTextBuffer;
54
+ }
55
+
56
+ export { LocalBlobStore };
@@ -0,0 +1,54 @@
1
+ import type { Logger } from '@dexto/core';
2
+ import type { BlobStore, BlobInput, BlobMetadata, BlobReference, BlobData, BlobStats } from './types.js';
3
+ import type { LocalBlobStoreConfig } from './schemas.js';
4
+ /**
5
+ * Local filesystem blob store implementation.
6
+ *
7
+ * Stores blobs on the local filesystem with content-based deduplication
8
+ * and metadata tracking. This is the default store for development
9
+ * and single-machine deployments.
10
+ */
11
+ export declare class LocalBlobStore implements BlobStore {
12
+ private config;
13
+ private storePath;
14
+ private connected;
15
+ private statsCache;
16
+ private statsCachePromise;
17
+ private lastStatsRefresh;
18
+ private logger;
19
+ private static readonly STATS_REFRESH_INTERVAL_MS;
20
+ constructor(config: LocalBlobStoreConfig, logger: Logger);
21
+ connect(): Promise<void>;
22
+ disconnect(): Promise<void>;
23
+ isConnected(): boolean;
24
+ getStoreType(): string;
25
+ store(input: BlobInput, metadata?: BlobMetadata): Promise<BlobReference>;
26
+ retrieve(reference: string, format?: 'base64' | 'buffer' | 'path' | 'stream' | 'url'): Promise<BlobData>;
27
+ exists(reference: string): Promise<boolean>;
28
+ delete(reference: string): Promise<void>;
29
+ cleanup(olderThan?: Date): Promise<number>;
30
+ getStats(): Promise<BlobStats>;
31
+ getStoragePath(): string | undefined;
32
+ listBlobs(): Promise<BlobReference[]>;
33
+ private ensureStatsCache;
34
+ private refreshStatsCache;
35
+ private updateStatsCacheAfterStore;
36
+ private updateStatsCacheAfterDelete;
37
+ /**
38
+ * Convert various input types to Buffer
39
+ */
40
+ private inputToBuffer;
41
+ /**
42
+ * Parse blob reference to extract ID
43
+ */
44
+ private parseReference;
45
+ /**
46
+ * Detect MIME type from buffer content and/or filename
47
+ */
48
+ private detectMimeType;
49
+ /**
50
+ * Check if buffer contains text content
51
+ */
52
+ private isTextBuffer;
53
+ }
54
+ //# sourceMappingURL=local-blob-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local-blob-store.d.ts","sourceRoot":"","sources":["../../src/blob/local-blob-store.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,OAAO,KAAK,EACR,SAAS,EACT,SAAS,EACT,YAAY,EACZ,aAAa,EACb,QAAQ,EACR,SAAS,EAEZ,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAkEzD;;;;;;GAMG;AACH,qBAAa,cAAe,YAAW,SAAS;IAC5C,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,UAAU,CAAqD;IACvE,OAAO,CAAC,iBAAiB,CAA8B;IACvD,OAAO,CAAC,gBAAgB,CAAa;IACrC,OAAO,CAAC,MAAM,CAAS;IAEvB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,yBAAyB,CAAS;gBAE9C,MAAM,EAAE,oBAAoB,EAAE,MAAM,EAAE,MAAM;IAOlD,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAoBxB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAKjC,WAAW,IAAI,OAAO;IAItB,YAAY,IAAI,MAAM;IAIhB,KAAK,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,GAAE,YAAiB,GAAG,OAAO,CAAC,aAAa,CAAC;IAyF5E,QAAQ,CACV,SAAS,EAAE,MAAM,EACjB,MAAM,GAAE,QAAQ,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAgB,GACnE,OAAO,CAAC,QAAQ,CAAC;IAsDd,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAiB3C,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAwBxC,OAAO,CAAC,SAAS,CAAC,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC;IAiD1C,QAAQ,IAAI,OAAO,CAAC,SAAS,CAAC;IAepC,cAAc,IAAI,MAAM,GAAG,SAAS;IAI9B,SAAS,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC;YAqC7B,gBAAgB;YAsBhB,iBAAiB;IAwB/B,OAAO,CAAC,0BAA0B;IASlC,OAAO,CAAC,2BAA2B;IASnC;;OAEG;YACW,aAAa;IAgD3B;;OAEG;IACH,OAAO,CAAC,cAAc;IAgBtB;;OAEG;IACH,OAAO,CAAC,cAAc;IAiDtB;;OAEG;IACH,OAAO,CAAC,YAAY;CAiBvB"}