@remit/storage-service 0.0.13 → 0.0.15

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.
@@ -1,14 +1,15 @@
1
- import { createHash } from "node:crypto";
2
1
  import { createWriteStream } from "node:fs";
3
2
  import { mkdir, readdir, readFile, rm, stat, unlink, writeFile, } from "node:fs/promises";
4
3
  import { dirname, join, resolve } from "node:path";
5
- import { PassThrough, Readable } from "node:stream";
4
+ import { Readable } from "node:stream";
6
5
  import { pipeline } from "node:stream/promises";
7
- import { createGunzip, createGzip, gunzipSync, gzipSync } from "node:zlib";
6
+ import { promisify } from "node:util";
7
+ import { createGunzip, gunzipSync, gzip } from "node:zlib";
8
8
  import { ContentEncoding, StorageType } from "@remit/domain-enums";
9
9
  import { UPLOAD_ROUTE_PREFIX, UPLOAD_URL_SIGNING_LABEL, } from "../outbox-upload-url.js";
10
10
  import { signStoragePath } from "../signed-path.js";
11
11
  import { buildBodyPartKey, buildDeduplicatedKey, buildExportArchiveKey, buildMessageBodyKey, buildOutboxAttachmentKey, buildOutboxAttachmentPrefix, buildParsedBodyKey, computeChecksum, isStorageNotFoundError, } from "../storage.js";
12
+ const gzipAsync = promisify(gzip);
12
13
  export const createFilesystemStorageService = (basePath, uploadUrls) => {
13
14
  const storeInternal = async (params) => {
14
15
  const { key, content, compress = true } = params;
@@ -16,7 +17,7 @@ export const createFilesystemStorageService = (basePath, uploadUrls) => {
16
17
  const contentEncoding = compress
17
18
  ? ContentEncoding.Gzip
18
19
  : ContentEncoding.None;
19
- const body = compress ? gzipSync(content) : content;
20
+ const body = compress ? await gzipAsync(content) : content;
20
21
  const fullPath = join(basePath, key);
21
22
  await mkdir(dirname(fullPath), { recursive: true });
22
23
  await writeFile(fullPath, body);
@@ -37,26 +38,6 @@ export const createFilesystemStorageService = (basePath, uploadUrls) => {
37
38
  content,
38
39
  });
39
40
  };
40
- const storeMessageBodyStream = async (params) => {
41
- const { accountConfigId, accountId, messageId, content } = params;
42
- const key = buildMessageBodyKey(accountConfigId, accountId, messageId);
43
- const fullPath = join(basePath, key);
44
- await mkdir(dirname(fullPath), { recursive: true });
45
- const hash = createHash("sha256");
46
- const hashTap = new PassThrough();
47
- hashTap.on("data", (chunk) => hash.update(chunk));
48
- await pipeline(content, hashTap, createGzip(), createWriteStream(fullPath));
49
- const { size } = await stat(fullPath);
50
- return {
51
- uri: `file://${fullPath}`,
52
- storageType: StorageType.Filesystem,
53
- storageLocation: basePath,
54
- storageKey: key,
55
- sizeBytes: size,
56
- checksumSha256: hash.digest("hex"),
57
- contentEncoding: ContentEncoding.Gzip,
58
- };
59
- };
60
41
  const storeBodyPart = (params) => {
61
42
  const { accountConfigId, accountId, messageId, partPath, content } = params;
62
43
  return storeInternal({
@@ -254,7 +235,6 @@ export const createFilesystemStorageService = (basePath, uploadUrls) => {
254
235
  };
255
236
  return {
256
237
  storeMessageBody,
257
- storeMessageBodyStream,
258
238
  storeBodyPart,
259
239
  bodyPartExists,
260
240
  retrieveBodyPart,
@@ -309,6 +289,9 @@ export const createFilesystemStorageService = (basePath, uploadUrls) => {
309
289
  await pipeline(body, createWriteStream(fullPath));
310
290
  return key;
311
291
  },
292
+ deleteExportArchive: (accountConfigId, exportRequestId) => rm(join(basePath, buildExportArchiveKey(accountConfigId, exportRequestId)), {
293
+ force: true,
294
+ }),
312
295
  getPresignedDownloadUrl: (_key, _expiresInSeconds) => {
313
296
  throw new Error("getPresignedDownloadUrl is not supported on the filesystem backend");
314
297
  },
@@ -1,13 +1,12 @@
1
- import { createHash } from "node:crypto";
2
- import { PassThrough } from "node:stream";
3
- import { pipeline } from "node:stream/promises";
4
- import { createGunzip, createGzip, gunzipSync, gzipSync } from "node:zlib";
1
+ import { promisify } from "node:util";
2
+ import { createGunzip, gunzipSync, gzip } from "node:zlib";
5
3
  import { DeleteObjectCommand, DeleteObjectsCommand, GetObjectCommand, HeadObjectCommand, ListObjectsV2Command, PutObjectCommand, } from "@aws-sdk/client-s3";
6
4
  import { Upload } from "@aws-sdk/lib-storage";
7
5
  import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
8
6
  import { ContentEncoding, StorageType } from "@remit/domain-enums";
9
7
  import { buildBodyPartKey, buildDeduplicatedKey, buildExportArchiveKey, buildMessageBodyKey, buildOutboxAttachmentKey, buildOutboxAttachmentPrefix, buildParsedBodyKey, computeChecksum, isStorageNotFoundError, } from "../storage.js";
10
8
  import { parseStorageUri } from "../uri.js";
9
+ const gzipAsync = promisify(gzip);
11
10
  export const createS3StorageService = (client, bucketName) => {
12
11
  const storeInternal = async (params) => {
13
12
  const { key, content, contentType, compress = true } = params;
@@ -15,7 +14,7 @@ export const createS3StorageService = (client, bucketName) => {
15
14
  const contentEncoding = compress
16
15
  ? ContentEncoding.Gzip
17
16
  : ContentEncoding.None;
18
- const body = compress ? gzipSync(content) : content;
17
+ const body = compress ? await gzipAsync(content) : content;
19
18
  await client.send(new PutObjectCommand({
20
19
  Bucket: bucketName,
21
20
  Key: key,
@@ -41,47 +40,6 @@ export const createS3StorageService = (client, bucketName) => {
41
40
  contentType: "message/rfc822",
42
41
  });
43
42
  };
44
- const storeMessageBodyStream = async (params) => {
45
- const { accountConfigId, accountId, messageId, content } = params;
46
- const key = buildMessageBodyKey(accountConfigId, accountId, messageId);
47
- // Hash the logical (pre-gzip) bytes as they flow, gzip them, and feed
48
- // the gzipped stream to a multipart Upload — the whole body is never
49
- // held in memory. `Upload` buffers only one part at a time.
50
- const hash = createHash("sha256");
51
- const hashTap = new PassThrough();
52
- hashTap.on("data", (chunk) => hash.update(chunk));
53
- const gzip = createGzip();
54
- const uploadBody = new PassThrough();
55
- let storedBytes = 0;
56
- uploadBody.on("data", (chunk) => {
57
- storedBytes += chunk.length;
58
- });
59
- const upload = new Upload({
60
- client,
61
- params: {
62
- Bucket: bucketName,
63
- Key: key,
64
- Body: uploadBody,
65
- ContentType: "message/rfc822",
66
- ContentEncoding: "gzip",
67
- },
68
- });
69
- const pumped = pipeline(content, hashTap, gzip, uploadBody);
70
- await Promise.all([upload.done(), pumped]).catch(async (error) => {
71
- // Abort the multipart upload so a failed pump doesn't leak parts.
72
- await upload.abort().catch(() => { });
73
- throw error;
74
- });
75
- return {
76
- uri: `s3://${bucketName}/${key}`,
77
- storageType: StorageType.S3,
78
- storageLocation: bucketName,
79
- storageKey: key,
80
- sizeBytes: storedBytes,
81
- checksumSha256: hash.digest("hex"),
82
- contentEncoding: ContentEncoding.Gzip,
83
- };
84
- };
85
43
  const storeBodyPart = (params) => {
86
44
  const { accountConfigId, accountId, messageId, partPath, content, contentType, } = params;
87
45
  return storeInternal({
@@ -384,10 +342,15 @@ export const createS3StorageService = (client, bucketName) => {
384
342
  });
385
343
  return key;
386
344
  };
345
+ const deleteExportArchive = async (accountConfigId, exportRequestId) => {
346
+ await client.send(new DeleteObjectCommand({
347
+ Bucket: bucketName,
348
+ Key: buildExportArchiveKey(accountConfigId, exportRequestId),
349
+ }));
350
+ };
387
351
  const getPresignedDownloadUrl = (key, expiresInSeconds) => getSignedUrl(client, new GetObjectCommand({ Bucket: bucketName, Key: key }), { expiresIn: expiresInSeconds });
388
352
  return {
389
353
  storeMessageBody,
390
- storeMessageBodyStream,
391
354
  storeBodyPart,
392
355
  bodyPartExists,
393
356
  retrieveBodyPart,
@@ -408,6 +371,7 @@ export const createS3StorageService = (client, bucketName) => {
408
371
  exists,
409
372
  delete: del,
410
373
  storeExportArchiveStream,
374
+ deleteExportArchive,
411
375
  getPresignedDownloadUrl,
412
376
  };
413
377
  };
package/dist/storage.d.ts CHANGED
@@ -25,14 +25,6 @@ export interface StoreMessageBodyParams {
25
25
  messageId: string;
26
26
  content: Buffer;
27
27
  }
28
- /** Parameters for streaming a message body (raw RFC822 content) to storage */
29
- export interface StoreMessageBodyStreamParams {
30
- accountConfigId: string;
31
- accountId: string;
32
- messageId: string;
33
- /** Readable stream of the raw message bytes; streamed to storage, never buffered whole. */
34
- content: Readable;
35
- }
36
28
  /** Parameters for storing a body part (attachment, inline content) */
37
29
  export interface StoreBodyPartParams {
38
30
  accountConfigId: string;
@@ -115,12 +107,6 @@ export interface StoreParsedBodyParams {
115
107
  export interface StorageService {
116
108
  /** Store a message body (raw RFC822 content) */
117
109
  storeMessageBody(params: StoreMessageBodyParams): Promise<StorageReference>;
118
- /**
119
- * Store a message body from a readable stream, never buffering the whole
120
- * body. Use this on the sync hot path so a ranged FETCH streams straight to
121
- * storage.
122
- */
123
- storeMessageBodyStream(params: StoreMessageBodyStreamParams): Promise<StorageReference>;
124
110
  /** Store a body part (attachment, inline content) */
125
111
  storeBodyPart(params: StoreBodyPartParams): Promise<StorageReference>;
126
112
  /**
@@ -211,6 +197,11 @@ export interface StorageService {
211
197
  * storage key, which `getPresignedDownloadUrl` can then sign.
212
198
  */
213
199
  storeExportArchiveStream(accountConfigId: string, exportRequestId: string, body: Readable): Promise<string>;
200
+ /**
201
+ * Delete the export archive of the given config + request. Deleting a
202
+ * missing archive is a no-op.
203
+ */
204
+ deleteExportArchive(accountConfigId: string, exportRequestId: string): Promise<void>;
214
205
  /**
215
206
  * Generate a presigned URL that grants temporary GET access to a raw S3 key.
216
207
  * Use for export archives — the key must already exist in the bucket.
package/dist/storage.js CHANGED
@@ -85,14 +85,6 @@ export const createMockStorageService = () => {
85
85
  const { accountConfigId, accountId, messageId, content } = params;
86
86
  return storeInternal(buildMessageBodyKey(accountConfigId, accountId, messageId), content);
87
87
  };
88
- const storeMessageBodyStream = async (params) => {
89
- const { accountConfigId, accountId, messageId, content } = params;
90
- const chunks = [];
91
- for await (const chunk of content) {
92
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
93
- }
94
- return storeInternal(buildMessageBodyKey(accountConfigId, accountId, messageId), Buffer.concat(chunks));
95
- };
96
88
  const storeBodyPart = async (params) => {
97
89
  const { accountConfigId, accountId, messageId, partPath, content } = params;
98
90
  return storeInternal(buildBodyPartKey(accountConfigId, accountId, messageId, partPath), content);
@@ -184,7 +176,6 @@ export const createMockStorageService = () => {
184
176
  };
185
177
  return {
186
178
  storeMessageBody,
187
- storeMessageBodyStream,
188
179
  storeBodyPart,
189
180
  bodyPartExists,
190
181
  retrieveBodyPart,
@@ -223,6 +214,9 @@ export const createMockStorageService = () => {
223
214
  storage.set(`mock://${key}`, Buffer.concat(chunks));
224
215
  return key;
225
216
  },
217
+ deleteExportArchive: async (accountConfigId, exportRequestId) => {
218
+ storage.delete(`mock://${buildExportArchiveKey(accountConfigId, exportRequestId)}`);
219
+ },
226
220
  getPresignedDownloadUrl: async (key, _expiresInSeconds) => `mock://presigned/${key}`,
227
221
  };
228
222
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/storage-service",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",