@remit/storage-service 0.0.10 → 0.0.11
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/package.json +1 -1
- package/src/backends/filesystem.ts +22 -0
- package/src/backends/s3.ts +23 -0
- package/src/outbox-attachment-retrieve.test.ts +56 -0
- package/src/storage.ts +18 -0
package/package.json
CHANGED
|
@@ -400,6 +400,27 @@ export const createFilesystemStorageService = (
|
|
|
400
400
|
return stats ? { sizeBytes: stats.size } : null;
|
|
401
401
|
};
|
|
402
402
|
|
|
403
|
+
const retrieveOutboxAttachment: StorageService["retrieveOutboxAttachment"] = (
|
|
404
|
+
accountConfigId,
|
|
405
|
+
accountId,
|
|
406
|
+
outboxMessageId,
|
|
407
|
+
outboxAttachmentId,
|
|
408
|
+
) =>
|
|
409
|
+
readFile(
|
|
410
|
+
join(
|
|
411
|
+
basePath,
|
|
412
|
+
buildOutboxAttachmentKey(
|
|
413
|
+
accountConfigId,
|
|
414
|
+
accountId,
|
|
415
|
+
outboxMessageId,
|
|
416
|
+
outboxAttachmentId,
|
|
417
|
+
),
|
|
418
|
+
),
|
|
419
|
+
).catch((error: unknown) => {
|
|
420
|
+
if (isStorageNotFoundError(error)) return null;
|
|
421
|
+
throw error;
|
|
422
|
+
});
|
|
423
|
+
|
|
403
424
|
// This backend has no presigned anything, so the URL addresses the
|
|
404
425
|
// deployment's own upload route. Its authority is the same HMAC the read side
|
|
405
426
|
// uses on /content, under the write label, covering the storage key, the
|
|
@@ -540,6 +561,7 @@ export const createFilesystemStorageService = (
|
|
|
540
561
|
deleteOutboxAttachments,
|
|
541
562
|
deleteOutboxAttachment,
|
|
542
563
|
statOutboxAttachment,
|
|
564
|
+
retrieveOutboxAttachment,
|
|
543
565
|
createOutboxAttachmentUploadUrl,
|
|
544
566
|
storeDeduplicated,
|
|
545
567
|
storeParsedBody,
|
package/src/backends/s3.ts
CHANGED
|
@@ -467,6 +467,28 @@ export const createS3StorageService = (
|
|
|
467
467
|
return { sizeBytes: head.ContentLength ?? 0 };
|
|
468
468
|
};
|
|
469
469
|
|
|
470
|
+
const retrieveOutboxAttachment: StorageService["retrieveOutboxAttachment"] =
|
|
471
|
+
async (accountConfigId, accountId, outboxMessageId, outboxAttachmentId) => {
|
|
472
|
+
const key = buildOutboxAttachmentKey(
|
|
473
|
+
accountConfigId,
|
|
474
|
+
accountId,
|
|
475
|
+
outboxMessageId,
|
|
476
|
+
outboxAttachmentId,
|
|
477
|
+
);
|
|
478
|
+
const response = await client
|
|
479
|
+
.send(new GetObjectCommand({ Bucket: bucketName, Key: key }))
|
|
480
|
+
.catch((error: unknown) => {
|
|
481
|
+
if (isStorageNotFoundError(error)) return null;
|
|
482
|
+
throw error;
|
|
483
|
+
});
|
|
484
|
+
if (!response) return null;
|
|
485
|
+
if (!response.Body) {
|
|
486
|
+
throw new Error(`Empty response body for outbox attachment: ${key}`);
|
|
487
|
+
}
|
|
488
|
+
const bytes = await response.Body.transformToByteArray();
|
|
489
|
+
return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
490
|
+
};
|
|
491
|
+
|
|
470
492
|
// The browser PUTs straight to the bucket and these bytes never touch the
|
|
471
493
|
// API. `ContentLength` on the command hoists `content-length` into
|
|
472
494
|
// X-Amz-SignedHeaders, so S3 recomputes the signature over the length it was
|
|
@@ -718,6 +740,7 @@ export const createS3StorageService = (
|
|
|
718
740
|
deleteOutboxAttachments,
|
|
719
741
|
deleteOutboxAttachment,
|
|
720
742
|
statOutboxAttachment,
|
|
743
|
+
retrieveOutboxAttachment,
|
|
721
744
|
createOutboxAttachmentUploadUrl,
|
|
722
745
|
storeDeduplicated,
|
|
723
746
|
storeParsedBody,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { describe, it } from "node:test";
|
|
6
|
+
import { createFilesystemStorageService } from "./backends/filesystem.js";
|
|
7
|
+
import { createMockStorageService, type StorageService } from "./storage.js";
|
|
8
|
+
|
|
9
|
+
const OWNER = {
|
|
10
|
+
accountConfigId: "cfg-1",
|
|
11
|
+
accountId: "acc-1",
|
|
12
|
+
outboxMessageId: "ob-1",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const readsBackWhatWasStored = async (storage: StorageService) => {
|
|
16
|
+
const content = Buffer.from([0x25, 0x50, 0x44, 0x46, 0x00, 0xff]);
|
|
17
|
+
await storage.storeOutboxAttachment({
|
|
18
|
+
...OWNER,
|
|
19
|
+
outboxAttachmentId: "att-1",
|
|
20
|
+
content,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
assert.deepEqual(
|
|
24
|
+
await storage.retrieveOutboxAttachment(
|
|
25
|
+
OWNER.accountConfigId,
|
|
26
|
+
OWNER.accountId,
|
|
27
|
+
OWNER.outboxMessageId,
|
|
28
|
+
"att-1",
|
|
29
|
+
),
|
|
30
|
+
content,
|
|
31
|
+
);
|
|
32
|
+
assert.equal(
|
|
33
|
+
await storage.retrieveOutboxAttachment(
|
|
34
|
+
OWNER.accountConfigId,
|
|
35
|
+
OWNER.accountId,
|
|
36
|
+
OWNER.outboxMessageId,
|
|
37
|
+
"att-missing",
|
|
38
|
+
),
|
|
39
|
+
null,
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
describe("retrieveOutboxAttachment", () => {
|
|
44
|
+
it("reads back the exact bytes on the filesystem backend, and null for none", async () => {
|
|
45
|
+
const basePath = await mkdtemp(join(tmpdir(), "remit-outbox-retrieve-"));
|
|
46
|
+
try {
|
|
47
|
+
await readsBackWhatWasStored(createFilesystemStorageService(basePath));
|
|
48
|
+
} finally {
|
|
49
|
+
await rm(basePath, { recursive: true, force: true });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("reads back the exact bytes on the mock backend, and null for none", async () => {
|
|
54
|
+
await readsBackWhatWasStored(createMockStorageService());
|
|
55
|
+
});
|
|
56
|
+
});
|
package/src/storage.ts
CHANGED
|
@@ -266,6 +266,17 @@ export interface StorageService {
|
|
|
266
266
|
outboxAttachmentId: string,
|
|
267
267
|
): Promise<{ sizeBytes: number } | null>;
|
|
268
268
|
|
|
269
|
+
/**
|
|
270
|
+
* The bytes of one attachment, for building the MIME part it goes out as.
|
|
271
|
+
* Null when nothing is stored under the key.
|
|
272
|
+
*/
|
|
273
|
+
retrieveOutboxAttachment(
|
|
274
|
+
accountConfigId: string,
|
|
275
|
+
accountId: string,
|
|
276
|
+
outboxMessageId: string,
|
|
277
|
+
outboxAttachmentId: string,
|
|
278
|
+
): Promise<Buffer | null>;
|
|
279
|
+
|
|
269
280
|
/**
|
|
270
281
|
* Mint somewhere to PUT one file. The backend decides what that is — block
|
|
271
282
|
* storage direct, or this deployment's upload route — and the caller never
|
|
@@ -650,6 +661,12 @@ export const createMockStorageService = (): StorageService => {
|
|
|
650
661
|
return content ? { sizeBytes: content.length } : null;
|
|
651
662
|
};
|
|
652
663
|
|
|
664
|
+
const retrieveOutboxAttachment: StorageService["retrieveOutboxAttachment"] =
|
|
665
|
+
async (accountConfigId, accountId, outboxMessageId, outboxAttachmentId) =>
|
|
666
|
+
storage.get(
|
|
667
|
+
`mock://${buildOutboxAttachmentKey(accountConfigId, accountId, outboxMessageId, outboxAttachmentId)}`,
|
|
668
|
+
) ?? null;
|
|
669
|
+
|
|
653
670
|
const createOutboxAttachmentUploadUrl: StorageService["createOutboxAttachmentUploadUrl"] =
|
|
654
671
|
async (params) => ({
|
|
655
672
|
uploadUrl: `mock://upload/${buildOutboxAttachmentKey(params.accountConfigId, params.accountId, params.outboxMessageId, params.outboxAttachmentId)}?max=${params.sizeBytes}&exp=${params.expiresAt}`,
|
|
@@ -805,6 +822,7 @@ export const createMockStorageService = (): StorageService => {
|
|
|
805
822
|
deleteOutboxAttachments,
|
|
806
823
|
deleteOutboxAttachment,
|
|
807
824
|
statOutboxAttachment,
|
|
825
|
+
retrieveOutboxAttachment,
|
|
808
826
|
createOutboxAttachmentUploadUrl,
|
|
809
827
|
storeDeduplicated,
|
|
810
828
|
storeParsedBody,
|