@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,498 @@
1
+ import { promises as fs, createReadStream } from "fs";
2
+ import path from "path";
3
+ import { createHash } from "crypto";
4
+ import { pathToFileURL } from "url";
5
+ import { DextoLogComponent, StorageError } from "@dexto/core";
6
+ function isPlainObject(value) {
7
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8
+ }
9
+ function parseStoredBlobMetadata(value) {
10
+ if (!isPlainObject(value)) {
11
+ throw new Error("Invalid blob metadata: expected object");
12
+ }
13
+ const id = value["id"];
14
+ if (typeof id !== "string") {
15
+ throw new Error("Invalid blob metadata: id");
16
+ }
17
+ const mimeType = value["mimeType"];
18
+ if (typeof mimeType !== "string") {
19
+ throw new Error("Invalid blob metadata: mimeType");
20
+ }
21
+ const originalName = value["originalName"];
22
+ if (originalName !== void 0 && typeof originalName !== "string") {
23
+ throw new Error("Invalid blob metadata: originalName");
24
+ }
25
+ const createdAtRaw = value["createdAt"];
26
+ const createdAt = createdAtRaw instanceof Date ? createdAtRaw : typeof createdAtRaw === "string" || typeof createdAtRaw === "number" ? new Date(createdAtRaw) : null;
27
+ if (!createdAt || Number.isNaN(createdAt.getTime())) {
28
+ throw new Error("Invalid blob metadata: createdAt");
29
+ }
30
+ const size = value["size"];
31
+ if (typeof size !== "number") {
32
+ throw new Error("Invalid blob metadata: size");
33
+ }
34
+ const hash = value["hash"];
35
+ if (typeof hash !== "string") {
36
+ throw new Error("Invalid blob metadata: hash");
37
+ }
38
+ const source = value["source"];
39
+ if (source !== void 0 && source !== "tool" && source !== "user" && source !== "system") {
40
+ throw new Error("Invalid blob metadata: source");
41
+ }
42
+ return {
43
+ id,
44
+ mimeType,
45
+ ...originalName !== void 0 && { originalName },
46
+ createdAt,
47
+ size,
48
+ hash,
49
+ ...source !== void 0 && { source }
50
+ };
51
+ }
52
+ class LocalBlobStore {
53
+ config;
54
+ storePath;
55
+ connected = false;
56
+ statsCache = null;
57
+ statsCachePromise = null;
58
+ lastStatsRefresh = 0;
59
+ logger;
60
+ static STATS_REFRESH_INTERVAL_MS = 6e4;
61
+ // 1 minute
62
+ constructor(config, logger) {
63
+ this.config = config;
64
+ this.storePath = config.storePath;
65
+ this.logger = logger.createChild(DextoLogComponent.STORAGE);
66
+ }
67
+ async connect() {
68
+ if (this.connected) return;
69
+ try {
70
+ await fs.mkdir(this.storePath, { recursive: true });
71
+ await this.refreshStatsCache();
72
+ this.lastStatsRefresh = Date.now();
73
+ this.connected = true;
74
+ this.logger.debug(`LocalBlobStore connected at: ${this.storePath}`);
75
+ } catch (error) {
76
+ throw StorageError.blobOperationFailed("connect", "local", error);
77
+ }
78
+ }
79
+ async disconnect() {
80
+ this.connected = false;
81
+ this.logger.debug("LocalBlobStore disconnected");
82
+ }
83
+ isConnected() {
84
+ return this.connected;
85
+ }
86
+ getStoreType() {
87
+ return "local";
88
+ }
89
+ async store(input, metadata = {}) {
90
+ if (!this.connected) {
91
+ throw StorageError.blobBackendNotConnected("local");
92
+ }
93
+ const buffer = await this.inputToBuffer(input);
94
+ if (buffer.length > this.config.maxBlobSize) {
95
+ throw StorageError.blobSizeExceeded(buffer.length, this.config.maxBlobSize);
96
+ }
97
+ const hash = createHash("sha256").update(buffer).digest("hex").substring(0, 16);
98
+ const id = hash;
99
+ const blobPath = path.join(this.storePath, `${id}.dat`);
100
+ const metaPath = path.join(this.storePath, `${id}.meta.json`);
101
+ try {
102
+ const existingMeta = await fs.readFile(metaPath, "utf-8");
103
+ const parsed = JSON.parse(existingMeta);
104
+ const existingMetadata = parseStoredBlobMetadata(parsed);
105
+ this.logger.debug(
106
+ `Blob ${id} already exists, returning existing reference (deduplication)`
107
+ );
108
+ return {
109
+ id,
110
+ uri: `blob:${id}`,
111
+ metadata: existingMetadata
112
+ };
113
+ } catch {
114
+ }
115
+ const maxTotalSize = this.config.maxTotalSize;
116
+ if (maxTotalSize) {
117
+ const stats = await this.ensureStatsCache();
118
+ if (stats.totalSize + buffer.length > maxTotalSize) {
119
+ throw StorageError.blobTotalSizeExceeded(
120
+ stats.totalSize + buffer.length,
121
+ maxTotalSize
122
+ );
123
+ }
124
+ }
125
+ const storedMetadata = {
126
+ id,
127
+ mimeType: metadata.mimeType || this.detectMimeType(buffer, metadata.originalName),
128
+ originalName: metadata.originalName,
129
+ createdAt: metadata.createdAt || /* @__PURE__ */ new Date(),
130
+ source: metadata.source || "system",
131
+ size: buffer.length,
132
+ hash
133
+ };
134
+ try {
135
+ await Promise.all([
136
+ fs.writeFile(blobPath, buffer),
137
+ fs.writeFile(metaPath, JSON.stringify(storedMetadata, null, 2))
138
+ ]);
139
+ this.logger.debug(
140
+ `Stored blob ${id} (${buffer.length} bytes, ${storedMetadata.mimeType})`
141
+ );
142
+ this.updateStatsCacheAfterStore(buffer.length);
143
+ return {
144
+ id,
145
+ uri: `blob:${id}`,
146
+ metadata: storedMetadata
147
+ };
148
+ } catch (error) {
149
+ await Promise.allSettled([
150
+ fs.unlink(blobPath).catch(() => {
151
+ }),
152
+ fs.unlink(metaPath).catch(() => {
153
+ })
154
+ ]);
155
+ throw StorageError.blobOperationFailed("store", "local", error);
156
+ }
157
+ }
158
+ async retrieve(reference, format = "buffer") {
159
+ if (!this.connected) {
160
+ throw StorageError.blobBackendNotConnected("local");
161
+ }
162
+ const id = this.parseReference(reference);
163
+ const blobPath = path.join(this.storePath, `${id}.dat`);
164
+ const metaPath = path.join(this.storePath, `${id}.meta.json`);
165
+ try {
166
+ const metaContent = await fs.readFile(metaPath, "utf-8");
167
+ const parsed = JSON.parse(metaContent);
168
+ const metadata = parseStoredBlobMetadata(parsed);
169
+ switch (format) {
170
+ case "base64": {
171
+ const buffer = await fs.readFile(blobPath);
172
+ return { format: "base64", data: buffer.toString("base64"), metadata };
173
+ }
174
+ case "buffer": {
175
+ const buffer = await fs.readFile(blobPath);
176
+ return { format: "buffer", data: buffer, metadata };
177
+ }
178
+ case "path": {
179
+ await fs.access(blobPath);
180
+ return { format: "path", data: blobPath, metadata };
181
+ }
182
+ case "stream": {
183
+ const stream = createReadStream(blobPath);
184
+ return { format: "stream", data: stream, metadata };
185
+ }
186
+ case "url": {
187
+ const absolutePath = path.resolve(blobPath);
188
+ return {
189
+ format: "url",
190
+ data: pathToFileURL(absolutePath).toString(),
191
+ metadata
192
+ };
193
+ }
194
+ default:
195
+ throw StorageError.blobInvalidInput(format, `Unsupported format: ${format}`);
196
+ }
197
+ } catch (error) {
198
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
199
+ throw StorageError.blobNotFound(reference);
200
+ }
201
+ throw StorageError.blobOperationFailed("retrieve", "local", error);
202
+ }
203
+ }
204
+ async exists(reference) {
205
+ if (!this.connected) {
206
+ throw StorageError.blobBackendNotConnected("local");
207
+ }
208
+ const id = this.parseReference(reference);
209
+ const blobPath = path.join(this.storePath, `${id}.dat`);
210
+ const metaPath = path.join(this.storePath, `${id}.meta.json`);
211
+ try {
212
+ await Promise.all([fs.access(blobPath), fs.access(metaPath)]);
213
+ return true;
214
+ } catch {
215
+ return false;
216
+ }
217
+ }
218
+ async delete(reference) {
219
+ if (!this.connected) {
220
+ throw StorageError.blobBackendNotConnected("local");
221
+ }
222
+ const id = this.parseReference(reference);
223
+ const blobPath = path.join(this.storePath, `${id}.dat`);
224
+ const metaPath = path.join(this.storePath, `${id}.meta.json`);
225
+ try {
226
+ const metaContent = await fs.readFile(metaPath, "utf-8");
227
+ const parsed = JSON.parse(metaContent);
228
+ const metadata = parseStoredBlobMetadata(parsed);
229
+ await Promise.all([fs.unlink(blobPath), fs.unlink(metaPath)]);
230
+ this.logger.debug(`Deleted blob: ${id}`);
231
+ this.updateStatsCacheAfterDelete(metadata.size);
232
+ } catch (error) {
233
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
234
+ throw StorageError.blobNotFound(reference);
235
+ }
236
+ throw StorageError.blobOperationFailed("delete", "local", error);
237
+ }
238
+ }
239
+ async cleanup(olderThan) {
240
+ if (!this.connected) {
241
+ throw StorageError.blobBackendNotConnected("local");
242
+ }
243
+ const cleanupDays = this.config.cleanupAfterDays;
244
+ const cutoffDate = olderThan || new Date(Date.now() - cleanupDays * 24 * 60 * 60 * 1e3);
245
+ let deletedCount = 0;
246
+ try {
247
+ const files = await fs.readdir(this.storePath);
248
+ const metaFiles = files.filter((f) => f.endsWith(".meta.json"));
249
+ for (const metaFile of metaFiles) {
250
+ const metaPath = path.join(this.storePath, metaFile);
251
+ const id = metaFile.replace(".meta.json", "");
252
+ const blobPath = path.join(this.storePath, `${id}.dat`);
253
+ try {
254
+ const metaContent = await fs.readFile(metaPath, "utf-8");
255
+ const parsed = JSON.parse(metaContent);
256
+ const metadata = parseStoredBlobMetadata(parsed);
257
+ if (metadata.createdAt < cutoffDate) {
258
+ await Promise.all([
259
+ fs.unlink(blobPath).catch(() => {
260
+ }),
261
+ fs.unlink(metaPath).catch(() => {
262
+ })
263
+ ]);
264
+ deletedCount++;
265
+ this.updateStatsCacheAfterDelete(metadata.size);
266
+ this.logger.debug(`Cleaned up old blob: ${id}`);
267
+ }
268
+ } catch (error) {
269
+ this.logger.warn(
270
+ `Failed to process blob metadata ${metaFile}: ${String(error)}`
271
+ );
272
+ }
273
+ }
274
+ if (deletedCount > 0) {
275
+ this.logger.info(`Blob cleanup: removed ${deletedCount} old blobs`);
276
+ }
277
+ return deletedCount;
278
+ } catch (error) {
279
+ throw StorageError.blobCleanupFailed("local", error);
280
+ }
281
+ }
282
+ async getStats() {
283
+ if (!this.connected) {
284
+ throw StorageError.blobBackendNotConnected("local");
285
+ }
286
+ const stats = await this.ensureStatsCache();
287
+ return {
288
+ count: stats.count,
289
+ totalSize: stats.totalSize,
290
+ backendType: "local",
291
+ storePath: this.storePath
292
+ };
293
+ }
294
+ getStoragePath() {
295
+ return this.storePath;
296
+ }
297
+ async listBlobs() {
298
+ if (!this.connected) {
299
+ throw StorageError.blobBackendNotConnected("local");
300
+ }
301
+ try {
302
+ const files = await fs.readdir(this.storePath);
303
+ const metaFiles = files.filter((f) => f.endsWith(".meta.json"));
304
+ const blobs = [];
305
+ for (const metaFile of metaFiles) {
306
+ try {
307
+ const metaPath = path.join(this.storePath, metaFile);
308
+ const metaContent = await fs.readFile(metaPath, "utf-8");
309
+ const parsed = JSON.parse(metaContent);
310
+ const metadata = parseStoredBlobMetadata(parsed);
311
+ const blobId = metaFile.replace(".meta.json", "");
312
+ blobs.push({
313
+ id: blobId,
314
+ uri: `blob:${blobId}`,
315
+ metadata
316
+ });
317
+ } catch (error) {
318
+ this.logger.warn(
319
+ `Failed to process blob metadata ${metaFile}: ${String(error)}`
320
+ );
321
+ }
322
+ }
323
+ return blobs;
324
+ } catch (error) {
325
+ this.logger.warn(`Failed to list blobs: ${String(error)}`);
326
+ return [];
327
+ }
328
+ }
329
+ async ensureStatsCache() {
330
+ const now = Date.now();
331
+ const cacheAge = now - this.lastStatsRefresh;
332
+ const isStale = cacheAge > LocalBlobStore.STATS_REFRESH_INTERVAL_MS;
333
+ if (!this.statsCache || isStale) {
334
+ if (!this.statsCachePromise) {
335
+ this.statsCachePromise = this.refreshStatsCache();
336
+ }
337
+ try {
338
+ await this.statsCachePromise;
339
+ this.lastStatsRefresh = now;
340
+ } finally {
341
+ this.statsCachePromise = null;
342
+ }
343
+ }
344
+ return this.statsCache ?? { count: 0, totalSize: 0 };
345
+ }
346
+ async refreshStatsCache() {
347
+ const stats = { count: 0, totalSize: 0 };
348
+ try {
349
+ const files = await fs.readdir(this.storePath);
350
+ const datFiles = files.filter((f) => f.endsWith(".dat"));
351
+ stats.count = datFiles.length;
352
+ for (const datFile of datFiles) {
353
+ try {
354
+ const stat = await fs.stat(path.join(this.storePath, datFile));
355
+ stats.totalSize += stat.size;
356
+ } catch (error) {
357
+ this.logger.debug(
358
+ `Skipping size calculation for ${datFile}: ${error instanceof Error ? error.message : String(error)}`
359
+ );
360
+ }
361
+ }
362
+ } catch (error) {
363
+ this.logger.warn(`Failed to refresh blob stats cache: ${String(error)}`);
364
+ }
365
+ this.statsCache = stats;
366
+ }
367
+ updateStatsCacheAfterStore(size) {
368
+ if (!this.statsCache) {
369
+ this.statsCache = { count: 1, totalSize: size };
370
+ return;
371
+ }
372
+ this.statsCache.count += 1;
373
+ this.statsCache.totalSize += size;
374
+ }
375
+ updateStatsCacheAfterDelete(size) {
376
+ if (!this.statsCache) {
377
+ this.statsCache = { count: 0, totalSize: 0 };
378
+ return;
379
+ }
380
+ this.statsCache.count = Math.max(0, this.statsCache.count - 1);
381
+ this.statsCache.totalSize = Math.max(0, this.statsCache.totalSize - size);
382
+ }
383
+ /**
384
+ * Convert various input types to Buffer
385
+ */
386
+ async inputToBuffer(input) {
387
+ if (Buffer.isBuffer(input)) {
388
+ return input;
389
+ }
390
+ if (input instanceof Uint8Array) {
391
+ return Buffer.from(input);
392
+ }
393
+ if (input instanceof ArrayBuffer) {
394
+ return Buffer.from(new Uint8Array(input));
395
+ }
396
+ if (typeof input === "string") {
397
+ if (input.startsWith("data:")) {
398
+ const commaIndex = input.indexOf(",");
399
+ if (commaIndex !== -1 && input.includes(";base64,")) {
400
+ const base64Data = input.substring(commaIndex + 1);
401
+ return Buffer.from(base64Data, "base64");
402
+ }
403
+ throw StorageError.blobEncodingError(
404
+ "inputToBuffer",
405
+ "Unsupported data URI format"
406
+ );
407
+ }
408
+ if ((input.includes("/") || input.includes("\\")) && input.length > 1) {
409
+ try {
410
+ await fs.access(input);
411
+ return await fs.readFile(input);
412
+ } catch {
413
+ }
414
+ }
415
+ try {
416
+ return Buffer.from(input, "base64");
417
+ } catch {
418
+ throw StorageError.blobEncodingError("inputToBuffer", "Invalid base64 string");
419
+ }
420
+ }
421
+ throw StorageError.blobInvalidInput(input, `Unsupported input type: ${typeof input}`);
422
+ }
423
+ /**
424
+ * Parse blob reference to extract ID
425
+ */
426
+ parseReference(reference) {
427
+ if (!reference) {
428
+ throw StorageError.blobInvalidReference(reference, "Empty reference");
429
+ }
430
+ if (reference.startsWith("blob:")) {
431
+ const id = reference.substring(5);
432
+ if (!id) {
433
+ throw StorageError.blobInvalidReference(reference, "Empty blob ID after prefix");
434
+ }
435
+ return id;
436
+ }
437
+ return reference;
438
+ }
439
+ /**
440
+ * Detect MIME type from buffer content and/or filename
441
+ */
442
+ detectMimeType(buffer, filename) {
443
+ const header = buffer.subarray(0, 16);
444
+ if (header.length >= 3) {
445
+ const jpegSignature = header.subarray(0, 3);
446
+ if (jpegSignature.equals(Buffer.from([255, 216, 255]))) {
447
+ return "image/jpeg";
448
+ }
449
+ }
450
+ if (header.length >= 4) {
451
+ const signature = header.subarray(0, 4);
452
+ if (signature.equals(Buffer.from([137, 80, 78, 71]))) return "image/png";
453
+ if (signature.equals(Buffer.from([71, 73, 70, 56]))) return "image/gif";
454
+ if (signature.equals(Buffer.from([37, 80, 68, 70]))) return "application/pdf";
455
+ }
456
+ if (filename) {
457
+ const ext = path.extname(filename).toLowerCase();
458
+ const mimeTypes = {
459
+ ".jpg": "image/jpeg",
460
+ ".jpeg": "image/jpeg",
461
+ ".png": "image/png",
462
+ ".gif": "image/gif",
463
+ ".pdf": "application/pdf",
464
+ ".txt": "text/plain",
465
+ ".json": "application/json",
466
+ ".xml": "text/xml",
467
+ ".html": "text/html",
468
+ ".css": "text/css",
469
+ ".js": "text/javascript",
470
+ ".mp3": "audio/mpeg",
471
+ ".mp4": "video/mp4",
472
+ ".wav": "audio/wav"
473
+ };
474
+ if (mimeTypes[ext]) return mimeTypes[ext];
475
+ }
476
+ if (this.isTextBuffer(buffer)) {
477
+ return "text/plain";
478
+ }
479
+ return "application/octet-stream";
480
+ }
481
+ /**
482
+ * Check if buffer contains text content
483
+ */
484
+ isTextBuffer(buffer) {
485
+ let printableCount = 0;
486
+ const sampleSize = Math.min(512, buffer.length);
487
+ for (let i = 0; i < sampleSize; i++) {
488
+ const byte = buffer[i];
489
+ if (byte !== void 0 && (byte >= 32 && byte <= 126 || byte === 9 || byte === 10 || byte === 13)) {
490
+ printableCount++;
491
+ }
492
+ }
493
+ return printableCount / sampleSize > 0.7;
494
+ }
495
+ }
496
+ export {
497
+ LocalBlobStore
498
+ };