@remit/storage-service 0.0.13 → 0.0.14

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,
@@ -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({
@@ -387,7 +345,6 @@ export const createS3StorageService = (client, bucketName) => {
387
345
  const getPresignedDownloadUrl = (key, expiresInSeconds) => getSignedUrl(client, new GetObjectCommand({ Bucket: bucketName, Key: key }), { expiresIn: expiresInSeconds });
388
346
  return {
389
347
  storeMessageBody,
390
- storeMessageBodyStream,
391
348
  storeBodyPart,
392
349
  bodyPartExists,
393
350
  retrieveBodyPart,
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
  /**
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,
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.14",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",