@remit/storage-service 0.0.1

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 ADDED
@@ -0,0 +1,267 @@
1
+ # @remit/storage-service
2
+
3
+ Abstract storage service for email body parts and raw message content. Provides a unified interface for storing and retrieving binary content with configurable backends (S3 or local filesystem).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -w packages/storage-service
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```typescript
14
+ import { createStorageService } from "@remit/storage-service";
15
+ import { ContentEncoding } from "@remit/domain-enums";
16
+
17
+ const storage = createStorageService();
18
+
19
+ // Store content
20
+ const ref = await storage.store(Buffer.from("Hello, world!"), {
21
+ key: "accounts/abc123/messages/def456/body",
22
+ contentEncoding: ContentEncoding.Gzip,
23
+ contentType: "text/plain; charset=utf-8",
24
+ });
25
+
26
+ // Retrieve content
27
+ const content = await storage.retrieve(ref.uri);
28
+
29
+ // Check existence
30
+ if (await storage.exists(ref.uri)) {
31
+ await storage.delete(ref.uri);
32
+ }
33
+ ```
34
+
35
+ ## Environment Variables
36
+
37
+ | Variable | Required | Description |
38
+ | -------------------- | -------- | -------------------------------------------- |
39
+ | `S3_BUCKET_NAME` | No | S3 bucket name. If set, S3 backend is used |
40
+ | `S3_ENDPOINT` | No | Custom S3 endpoint (for LocalStack/MinIO) |
41
+ | `STORAGE_LOCAL_PATH` | No | Local filesystem path. Default: `.remit/storage` |
42
+
43
+ ### Backend Selection
44
+
45
+ The factory function automatically selects the backend:
46
+
47
+ ```
48
+ if (S3_BUCKET_NAME is defined) → S3 backend
49
+ else → Filesystem backend
50
+ ```
51
+
52
+ ## API
53
+
54
+ ### StorageService
55
+
56
+ ```typescript
57
+ interface StorageService {
58
+ store(content: Buffer, options: StoreOptions): Promise<StorageReference>;
59
+ retrieve(uri: string): Promise<Buffer>;
60
+ exists(uri: string): Promise<boolean>;
61
+ delete(uri: string): Promise<void>;
62
+ contentAddressableKey(content: Buffer, prefix?: string): string;
63
+ }
64
+ ```
65
+
66
+ ### StoreOptions
67
+
68
+ ```typescript
69
+ interface StoreOptions {
70
+ key: string; // Storage path
71
+ contentEncoding?: ContentEncoding; // Compression (default: none)
72
+ contentType?: string; // MIME type for S3 metadata
73
+ contentAddressable?: boolean; // Use content hash as key
74
+ }
75
+ ```
76
+
77
+ ### StorageReference
78
+
79
+ Returned by `store()`, contains all information needed to retrieve or reference the stored content:
80
+
81
+ ```typescript
82
+ interface StorageReference {
83
+ uri: string; // Full URI: s3://bucket/key or file:///path
84
+ storageType: StorageTypeValue; // "s3" | "filesystem"
85
+ storageLocation: string; // Bucket name or base path
86
+ storageKey: string; // Object key or relative path
87
+ sizeBytes: number; // Size after compression
88
+ checksumSha256: string; // Hex-encoded SHA-256 of original content
89
+ contentEncoding: ContentEncoding;
90
+ }
91
+ ```
92
+
93
+ ## URI Format
94
+
95
+ Storage references use URIs that encode all information needed to retrieve content:
96
+
97
+ ```
98
+ s3://bucket-name/path/to/object
99
+ file:///var/data/remit/path/to/file
100
+ ```
101
+
102
+ ### URI Utilities
103
+
104
+ ```typescript
105
+ import { parseStorageUri, buildStorageUri } from "@remit/storage-service";
106
+ import { StorageType } from "@remit/domain-enums";
107
+
108
+ // Parse URI into components
109
+ const parsed = parseStorageUri("s3://my-bucket/path/to/object");
110
+ // { storageType: "s3", storageLocation: "my-bucket", storageKey: "path/to/object" }
111
+
112
+ // Build URI from components
113
+ const uri = buildStorageUri(StorageType.S3, "my-bucket", "path/to/object");
114
+ // "s3://my-bucket/path/to/object"
115
+ ```
116
+
117
+ ## Content-Addressable Storage
118
+
119
+ For deduplication of attachments that may appear in multiple messages:
120
+
121
+ ```typescript
122
+ const storage = createStorageService();
123
+ const attachmentBuffer = Buffer.from(/* ... */);
124
+
125
+ // Generate content-addressable key
126
+ const key = storage.contentAddressableKey(attachmentBuffer);
127
+ // "dedup/a3/a3f2b8c9d0e1f2..."
128
+
129
+ // Check if already stored
130
+ const dedupUri = `s3://${bucketName}/${key}`;
131
+ if (await storage.exists(dedupUri)) {
132
+ // Content already stored, just reference it
133
+ return { uri: dedupUri, isDeduped: true };
134
+ }
135
+
136
+ // Store with content-addressable key
137
+ const ref = await storage.store(attachmentBuffer, {
138
+ key: "ignored-when-content-addressable",
139
+ contentAddressable: true,
140
+ });
141
+ ```
142
+
143
+ ## Compression
144
+
145
+ Gzip compression is automatically handled:
146
+
147
+ ```typescript
148
+ // Store with compression
149
+ const ref = await storage.store(content, {
150
+ key: "messages/123/body",
151
+ contentEncoding: ContentEncoding.Gzip,
152
+ });
153
+
154
+ // Retrieve automatically decompresses
155
+ const original = await storage.retrieve(ref.uri);
156
+ ```
157
+
158
+ ## Using Specific Backends
159
+
160
+ For cases where you need direct access to a specific backend:
161
+
162
+ ```typescript
163
+ import {
164
+ createS3StorageService,
165
+ createFilesystemStorageService,
166
+ } from "@remit/storage-service";
167
+ import { S3Client } from "@aws-sdk/client-s3";
168
+
169
+ // S3 backend
170
+ const s3Client = new S3Client({ endpoint: "http://localhost:4566" });
171
+ const s3Storage = createS3StorageService(s3Client, "my-bucket");
172
+
173
+ // Filesystem backend
174
+ const fsStorage = createFilesystemStorageService("/var/data/remit");
175
+ ```
176
+
177
+ ## Testing
178
+
179
+ ### Mock Service
180
+
181
+ For unit tests, use the mock implementation:
182
+
183
+ ```typescript
184
+ import { createMockStorageService } from "@remit/storage-service";
185
+
186
+ const storage = createMockStorageService();
187
+
188
+ // Works like the real service but stores in memory
189
+ const ref = await storage.store(Buffer.from("test"), { key: "test.txt" });
190
+ const content = await storage.retrieve(ref.uri);
191
+ ```
192
+
193
+ ### Running Tests
194
+
195
+ ```bash
196
+ # Run package tests
197
+ npm test -w packages/storage-service
198
+
199
+ # With LocalStack for S3 tests
200
+ S3_BUCKET_NAME=test-bucket S3_ENDPOINT=http://localhost:4566 npm test -w packages/storage-service
201
+ ```
202
+
203
+ ## Key Path Conventions
204
+
205
+ Consistent key paths enable efficient listing and cleanup:
206
+
207
+ ```
208
+ accounts/{accountId}/
209
+ messages/{messageId}/
210
+ raw # Raw RFC822 message
211
+ parts/
212
+ {partPath} # Body part content (1, 1.1, 2.1.3, etc.)
213
+
214
+ dedup/
215
+ {hash[0:2]}/
216
+ {hash} # Content-addressable storage for attachments
217
+ ```
218
+
219
+ ## Integration with ElectroDB Entities
220
+
221
+ ```typescript
222
+ import { BodyPartStorageService } from "@remit/remit-electrodb-service";
223
+ import { createStorageService } from "@remit/storage-service";
224
+
225
+ const storage = createStorageService();
226
+ const bodyPartStorageService = new BodyPartStorageService(config);
227
+
228
+ // Store content and create entity reference
229
+ const ref = await storage.store(content, {
230
+ key: `messages/${messageId}/parts/${partPath}`,
231
+ contentEncoding: ContentEncoding.Gzip,
232
+ });
233
+
234
+ await bodyPartStorageService.create({
235
+ messageId,
236
+ bodyPartId,
237
+ storageType: ref.storageType,
238
+ storageLocation: ref.storageLocation,
239
+ storageKey: ref.storageKey,
240
+ decodedSizeBytes: content.length,
241
+ checksumSha256: ref.checksumSha256,
242
+ contentEncoding: ref.contentEncoding,
243
+ isDeduped: false,
244
+ storedAt: Date.now(),
245
+ });
246
+ ```
247
+
248
+ ## Exports
249
+
250
+ ```typescript
251
+ // Factory functions
252
+ export { createStorageService, createMockStorageService } from "./storage.js";
253
+ export { createFilesystemStorageService } from "./backends/filesystem.js";
254
+ export { createS3StorageService } from "./backends/s3.js";
255
+
256
+ // URI utilities
257
+ export { parseStorageUri, buildStorageUri } from "./uri.js";
258
+
259
+ // Types
260
+ export type {
261
+ StorageService,
262
+ StorageReference,
263
+ StoreOptions,
264
+ } from "./storage.js";
265
+ export type { StorageTypeValue, ContentEncodingValue } from "./storage.js";
266
+ export type { ParsedStorageUri } from "./uri.js";
267
+ ```
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@remit/storage-service",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "main": "src/index.ts",
6
+ "types": "src/index.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./src/index.ts",
10
+ "default": "./src/index.ts"
11
+ },
12
+ "./filesystem": {
13
+ "types": "./src/filesystem.ts",
14
+ "default": "./src/filesystem.ts"
15
+ },
16
+ "./s3": {
17
+ "types": "./src/s3.ts",
18
+ "default": "./src/s3.ts"
19
+ }
20
+ },
21
+ "scripts": {
22
+ "test:typecheck": "tsgo --noEmit",
23
+ "test:run": "node --env-file=../../localhost-test-unit.env --import tsx --test 'src/**/*.test.ts'",
24
+ "test": "npm run test:typecheck && npm run test:run"
25
+ },
26
+ "dependencies": {
27
+ "@aws-sdk/client-s3": "*",
28
+ "@aws-sdk/lib-storage": "*",
29
+ "@aws-sdk/s3-request-presigner": "*"
30
+ },
31
+ "devDependencies": {
32
+ "@remit/domain-enums": "*",
33
+ "aws-sdk-client-mock": "*"
34
+ },
35
+ "license": "MIT",
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/remit-mail/remit.git",
42
+ "directory": "packages/storage-service"
43
+ }
44
+ }
@@ -0,0 +1,399 @@
1
+ import { createHash } from "node:crypto";
2
+ import { createWriteStream } from "node:fs";
3
+ import {
4
+ mkdir,
5
+ readdir,
6
+ readFile,
7
+ stat,
8
+ unlink,
9
+ writeFile,
10
+ } from "node:fs/promises";
11
+ import { dirname, join, resolve } from "node:path";
12
+ import { PassThrough, Readable } from "node:stream";
13
+ import { pipeline } from "node:stream/promises";
14
+ import { createGunzip, createGzip, gunzipSync, gzipSync } from "node:zlib";
15
+ import { ContentEncoding, StorageType } from "@remit/domain-enums";
16
+ import type {
17
+ ParsedBody,
18
+ StorageReference,
19
+ StorageService,
20
+ } from "../storage.js";
21
+ import {
22
+ buildBodyPartKey,
23
+ buildDeduplicatedKey,
24
+ buildExportArchiveKey,
25
+ buildExtractedPrefix,
26
+ buildExtractedSkippedKey,
27
+ buildExtractedTextKey,
28
+ buildMessageBodyKey,
29
+ buildParsedBodyKey,
30
+ computeChecksum,
31
+ isStorageNotFoundError,
32
+ } from "../storage.js";
33
+
34
+ interface StoreParams {
35
+ key: string;
36
+ content: Buffer;
37
+ compress?: boolean;
38
+ }
39
+
40
+ export const createFilesystemStorageService = (
41
+ basePath: string,
42
+ ): StorageService => {
43
+ const storeInternal = async (
44
+ params: StoreParams,
45
+ ): Promise<StorageReference> => {
46
+ const { key, content, compress = true } = params;
47
+ const checksumSha256 = computeChecksum(content);
48
+ const contentEncoding = compress
49
+ ? ContentEncoding.Gzip
50
+ : ContentEncoding.None;
51
+ const body = compress ? gzipSync(content) : content;
52
+
53
+ const fullPath = join(basePath, key);
54
+ await mkdir(dirname(fullPath), { recursive: true });
55
+ await writeFile(fullPath, body);
56
+
57
+ return {
58
+ uri: `file://${fullPath}`,
59
+ storageType: StorageType.Filesystem,
60
+ storageLocation: basePath,
61
+ storageKey: key,
62
+ sizeBytes: body.length,
63
+ checksumSha256,
64
+ contentEncoding,
65
+ };
66
+ };
67
+
68
+ const storeMessageBody: StorageService["storeMessageBody"] = (params) => {
69
+ const { accountConfigId, accountId, messageId, content } = params;
70
+ return storeInternal({
71
+ key: buildMessageBodyKey(accountConfigId, accountId, messageId),
72
+ content,
73
+ });
74
+ };
75
+
76
+ const storeMessageBodyStream: StorageService["storeMessageBodyStream"] =
77
+ async (params) => {
78
+ const { accountConfigId, accountId, messageId, content } = params;
79
+ const key = buildMessageBodyKey(accountConfigId, accountId, messageId);
80
+ const fullPath = join(basePath, key);
81
+ await mkdir(dirname(fullPath), { recursive: true });
82
+
83
+ const hash = createHash("sha256");
84
+ const hashTap = new PassThrough();
85
+ hashTap.on("data", (chunk: Buffer) => hash.update(chunk));
86
+
87
+ await pipeline(
88
+ content,
89
+ hashTap,
90
+ createGzip(),
91
+ createWriteStream(fullPath),
92
+ );
93
+
94
+ const { size } = await stat(fullPath);
95
+
96
+ return {
97
+ uri: `file://${fullPath}`,
98
+ storageType: StorageType.Filesystem,
99
+ storageLocation: basePath,
100
+ storageKey: key,
101
+ sizeBytes: size,
102
+ checksumSha256: hash.digest("hex"),
103
+ contentEncoding: ContentEncoding.Gzip,
104
+ };
105
+ };
106
+
107
+ const storeBodyPart: StorageService["storeBodyPart"] = (params) => {
108
+ const { accountConfigId, accountId, messageId, partPath, content } = params;
109
+ return storeInternal({
110
+ key: buildBodyPartKey(accountConfigId, accountId, messageId, partPath),
111
+ content,
112
+ });
113
+ };
114
+
115
+ const bodyPartExists: StorageService["bodyPartExists"] = (
116
+ accountConfigId,
117
+ accountId,
118
+ messageId,
119
+ partPath,
120
+ ) => {
121
+ const key = buildBodyPartKey(
122
+ accountConfigId,
123
+ accountId,
124
+ messageId,
125
+ partPath,
126
+ );
127
+ const fullPath = join(basePath, key);
128
+ return stat(fullPath)
129
+ .then(() => true)
130
+ .catch(() => false);
131
+ };
132
+
133
+ const retrieveBodyPart: StorageService["retrieveBodyPart"] = async (
134
+ accountConfigId,
135
+ accountId,
136
+ messageId,
137
+ partPath,
138
+ ) => {
139
+ const key = buildBodyPartKey(
140
+ accountConfigId,
141
+ accountId,
142
+ messageId,
143
+ partPath,
144
+ );
145
+ const fullPath = join(basePath, key);
146
+ const buffer = await readFile(fullPath).catch((error: unknown) => {
147
+ if (isStorageNotFoundError(error)) return null;
148
+ throw error;
149
+ });
150
+ if (!buffer) return null;
151
+ return buffer[0] === 0x1f && buffer[1] === 0x8b
152
+ ? gunzipSync(buffer)
153
+ : buffer;
154
+ };
155
+
156
+ const storeExtractedText: StorageService["storeExtractedText"] = (params) => {
157
+ const { accountConfigId, accountId, messageId, partPath, text } = params;
158
+ return storeInternal({
159
+ key: buildExtractedTextKey(
160
+ accountConfigId,
161
+ accountId,
162
+ messageId,
163
+ partPath,
164
+ ),
165
+ content: Buffer.from(text, "utf8"),
166
+ });
167
+ };
168
+
169
+ const storeExtractedSkipped: StorageService["storeExtractedSkipped"] = (
170
+ params,
171
+ ) => {
172
+ const { accountConfigId, accountId, messageId, partPath, marker } = params;
173
+ return storeInternal({
174
+ key: buildExtractedSkippedKey(
175
+ accountConfigId,
176
+ accountId,
177
+ messageId,
178
+ partPath,
179
+ ),
180
+ content: Buffer.from(JSON.stringify(marker), "utf8"),
181
+ compress: false,
182
+ });
183
+ };
184
+
185
+ const extractedResultExists: StorageService["extractedResultExists"] = async (
186
+ accountConfigId,
187
+ accountId,
188
+ messageId,
189
+ partPath,
190
+ ) => {
191
+ const textPath = join(
192
+ basePath,
193
+ buildExtractedTextKey(accountConfigId, accountId, messageId, partPath),
194
+ );
195
+ const skippedPath = join(
196
+ basePath,
197
+ buildExtractedSkippedKey(accountConfigId, accountId, messageId, partPath),
198
+ );
199
+ const exists = (fullPath: string): Promise<boolean> =>
200
+ stat(fullPath)
201
+ .then(() => true)
202
+ .catch(() => false);
203
+
204
+ const [textExists, skippedExists] = await Promise.all([
205
+ exists(textPath),
206
+ exists(skippedPath),
207
+ ]);
208
+ return textExists || skippedExists;
209
+ };
210
+
211
+ const retrieveExtractedText: StorageService["retrieveExtractedText"] = async (
212
+ accountConfigId,
213
+ accountId,
214
+ messageId,
215
+ partPath,
216
+ ) => {
217
+ const fullPath = join(
218
+ basePath,
219
+ buildExtractedTextKey(accountConfigId, accountId, messageId, partPath),
220
+ );
221
+ const buffer = await readFile(fullPath).catch((error: unknown) => {
222
+ if (isStorageNotFoundError(error)) return null;
223
+ throw error;
224
+ });
225
+ if (!buffer) return null;
226
+ const decoded =
227
+ buffer[0] === 0x1f && buffer[1] === 0x8b ? gunzipSync(buffer) : buffer;
228
+ return decoded.toString("utf8");
229
+ };
230
+
231
+ const listExtractedTexts: StorageService["listExtractedTexts"] = async (
232
+ accountConfigId,
233
+ accountId,
234
+ messageId,
235
+ ) => {
236
+ const prefix = buildExtractedPrefix(accountConfigId, accountId, messageId);
237
+ const suffix = ".txt.gz";
238
+ const dirPath = join(basePath, prefix);
239
+
240
+ const entries = await readdir(dirPath, { recursive: true }).catch(
241
+ (error: unknown) => {
242
+ if (isStorageNotFoundError(error)) return [];
243
+ throw error;
244
+ },
245
+ );
246
+
247
+ return entries
248
+ .filter((entry) => entry.endsWith(suffix))
249
+ .map((entry) => ({
250
+ partPath: entry.slice(0, -suffix.length),
251
+ key: `${prefix}${entry}`,
252
+ }));
253
+ };
254
+
255
+ const storeDeduplicated: StorageService["storeDeduplicated"] = (params) => {
256
+ const { accountConfigId, accountId, content } = params;
257
+ const checksumSha256 = computeChecksum(content);
258
+ return storeInternal({
259
+ key: buildDeduplicatedKey(accountConfigId, accountId, checksumSha256),
260
+ content,
261
+ });
262
+ };
263
+
264
+ const storeParsedBody: StorageService["storeParsedBody"] = (params) => {
265
+ const { accountConfigId, accountId, messageId, parsed } = params;
266
+ const content = Buffer.from(JSON.stringify(parsed), "utf8");
267
+ return storeInternal({
268
+ key: buildParsedBodyKey(accountConfigId, accountId, messageId),
269
+ content,
270
+ });
271
+ };
272
+
273
+ const retrieveParsedBody: StorageService["retrieveParsedBody"] = async (
274
+ accountConfigId,
275
+ accountId,
276
+ messageId,
277
+ ) => {
278
+ const key = buildParsedBodyKey(accountConfigId, accountId, messageId);
279
+ const fullPath = join(basePath, key);
280
+
281
+ const buffer = await readFile(fullPath).catch((error: unknown) => {
282
+ if (isStorageNotFoundError(error)) return null;
283
+ throw error;
284
+ });
285
+
286
+ if (!buffer) return null;
287
+
288
+ const decoded =
289
+ buffer[0] === 0x1f && buffer[1] === 0x8b ? gunzipSync(buffer) : buffer;
290
+ return JSON.parse(decoded.toString("utf8")) as ParsedBody;
291
+ };
292
+
293
+ // Resolve path from URI, handling both absolute and relative paths
294
+ // Note: URIs like file://.remit/storage/... parse incorrectly - '.remit' becomes hostname
295
+ // We reconstruct the original path by combining hostname + pathname
296
+ const resolvePathFromUri = (uri: string): string => {
297
+ const url = new URL(uri);
298
+ const hostname = url.hostname;
299
+ const pathname = url.pathname;
300
+
301
+ // If hostname exists, it was part of a relative path (e.g., file://.remit/... -> hostname='.remit')
302
+ if (hostname) {
303
+ const relativePath = hostname + pathname;
304
+ return resolve(relativePath);
305
+ }
306
+
307
+ // Absolute path (file:///absolute/path)
308
+ return pathname;
309
+ };
310
+
311
+ const retrieve = async (uri: string): Promise<Buffer> => {
312
+ const fullPath = resolvePathFromUri(uri);
313
+
314
+ const buffer = await readFile(fullPath);
315
+
316
+ if (buffer[0] === 0x1f && buffer[1] === 0x8b) {
317
+ return gunzipSync(buffer);
318
+ }
319
+
320
+ return buffer;
321
+ };
322
+
323
+ const exists = async (uri: string): Promise<boolean> => {
324
+ const fullPath = resolvePathFromUri(uri);
325
+ return stat(fullPath)
326
+ .then(() => true)
327
+ .catch(() => false);
328
+ };
329
+
330
+ const del = async (uri: string): Promise<void> => {
331
+ const fullPath = resolvePathFromUri(uri);
332
+ await unlink(fullPath);
333
+ };
334
+
335
+ return {
336
+ storeMessageBody,
337
+ storeMessageBodyStream,
338
+ storeBodyPart,
339
+ bodyPartExists,
340
+ retrieveBodyPart,
341
+ storeDeduplicated,
342
+ storeParsedBody,
343
+ retrieveParsedBody,
344
+ storeExtractedText,
345
+ storeExtractedSkipped,
346
+ extractedResultExists,
347
+ retrieveExtractedText,
348
+ listExtractedTexts,
349
+ retrieveMessageBody: async (accountConfigId, accountId, messageId) => {
350
+ const key = buildMessageBodyKey(accountConfigId, accountId, messageId);
351
+ const fullPath = join(basePath, key);
352
+ const buffer = await readFile(fullPath).catch((error: unknown) => {
353
+ if (isStorageNotFoundError(error)) return null;
354
+ throw error;
355
+ });
356
+ if (!buffer) return null;
357
+ if (buffer[0] === 0x1f && buffer[1] === 0x8b) {
358
+ return gunzipSync(buffer);
359
+ }
360
+ return buffer;
361
+ },
362
+ retrieveMessageBodyStream: async (
363
+ accountConfigId,
364
+ accountId,
365
+ messageId,
366
+ ) => {
367
+ const key = buildMessageBodyKey(accountConfigId, accountId, messageId);
368
+ const fullPath = join(basePath, key);
369
+ const buffer = await readFile(fullPath).catch((error: unknown) => {
370
+ if (isStorageNotFoundError(error)) return null;
371
+ throw error;
372
+ });
373
+ if (!buffer) return null;
374
+ if (buffer[0] === 0x1f && buffer[1] === 0x8b) {
375
+ return Readable.from(buffer).pipe(createGunzip());
376
+ }
377
+ return Readable.from(buffer);
378
+ },
379
+ retrieve,
380
+ exists,
381
+ delete: del,
382
+ storeExportArchiveStream: async (
383
+ accountConfigId,
384
+ exportRequestId,
385
+ body,
386
+ ) => {
387
+ const key = buildExportArchiveKey(accountConfigId, exportRequestId);
388
+ const fullPath = join(basePath, key);
389
+ await mkdir(dirname(fullPath), { recursive: true });
390
+ await pipeline(body, createWriteStream(fullPath));
391
+ return key;
392
+ },
393
+ getPresignedDownloadUrl: (_key: string, _expiresInSeconds: number) => {
394
+ throw new Error(
395
+ "getPresignedDownloadUrl is not supported on the filesystem backend",
396
+ );
397
+ },
398
+ };
399
+ };