@geekmidas/storage 2.0.4 → 10.0.0-alpha.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.
- package/dist/aws.cjs +159 -4
- package/dist/aws.cjs.map +1 -0
- package/dist/aws.d.cts +51 -3
- package/dist/aws.d.cts.map +1 -0
- package/dist/aws.d.mts +51 -3
- package/dist/aws.d.mts.map +1 -0
- package/dist/aws.mjs +151 -3
- package/dist/aws.mjs.map +1 -0
- package/dist/chunk-B9dir_RE.mjs +11 -0
- package/dist/errors-Bce0CJ11.cjs +109 -0
- package/dist/errors-Bce0CJ11.cjs.map +1 -0
- package/dist/errors-D5UnAd-X.mjs +79 -0
- package/dist/errors-D5UnAd-X.mjs.map +1 -0
- package/dist/index.cjs +39 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +66 -2
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +66 -2
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +32 -0
- package/dist/index.mjs.map +1 -0
- package/dist/{StorageClient-COs9SOsU.d.cts → registry-DBsAUTT3.d.cts} +27 -2
- package/dist/registry-DBsAUTT3.d.cts.map +1 -0
- package/dist/{StorageClient-DH4tP-Mf.d.mts → registry-DEqSHjx-.d.mts} +27 -2
- package/dist/registry-DEqSHjx-.d.mts.map +1 -0
- package/dist/s3Url-BfztIltX.d.cts +39 -0
- package/dist/s3Url-BfztIltX.d.cts.map +1 -0
- package/dist/s3Url-Bo32UTTA.cjs +103 -0
- package/dist/s3Url-Bo32UTTA.cjs.map +1 -0
- package/dist/s3Url-C8FGPNrG.mjs +52 -0
- package/dist/s3Url-C8FGPNrG.mjs.map +1 -0
- package/dist/s3Url-COpMIgF9.d.mts +42 -0
- package/dist/s3Url-COpMIgF9.d.mts.map +1 -0
- package/dist/s3Url.cjs +5 -0
- package/dist/s3Url.d.cts +2 -0
- package/dist/s3Url.d.mts +2 -0
- package/dist/s3Url.mjs +4 -0
- package/package.json +16 -3
- package/CHANGELOG.md +0 -51
- package/dist/AmazonStorageClient-BHpWawGD.d.mts +0 -45
- package/dist/AmazonStorageClient-BHpWawGD.d.mts.map +0 -1
- package/dist/AmazonStorageClient-BNoY2PL6.d.cts +0 -45
- package/dist/AmazonStorageClient-BNoY2PL6.d.cts.map +0 -1
- package/dist/AmazonStorageClient-BvoZepKJ.cjs +0 -161
- package/dist/AmazonStorageClient-BvoZepKJ.cjs.map +0 -1
- package/dist/AmazonStorageClient-Dz-KCJ_4.mjs +0 -127
- package/dist/AmazonStorageClient-Dz-KCJ_4.mjs.map +0 -1
- package/dist/AmazonStorageClient.cjs +0 -5
- package/dist/AmazonStorageClient.d.cts +0 -3
- package/dist/AmazonStorageClient.d.mts +0 -3
- package/dist/AmazonStorageClient.mjs +0 -4
- package/dist/StorageClient-4ADnTPnJ.mjs +0 -11
- package/dist/StorageClient-4ADnTPnJ.mjs.map +0 -1
- package/dist/StorageClient-BDRVrMJj.cjs +0 -17
- package/dist/StorageClient-BDRVrMJj.cjs.map +0 -1
- package/dist/StorageClient-COs9SOsU.d.cts.map +0 -1
- package/dist/StorageClient-DH4tP-Mf.d.mts.map +0 -1
- package/dist/StorageClient.cjs +0 -3
- package/dist/StorageClient.d.cts +0 -2
- package/dist/StorageClient.d.mts +0 -2
- package/dist/StorageClient.mjs +0 -3
- package/src/AmazonStorageClient.ts +0 -207
- package/src/StorageClient.ts +0 -108
- package/src/__tests__/AmazonStorageClient.spec.ts +0 -606
- package/src/__tests__/StorageClient.spec.ts +0 -98
- package/src/aws.ts +0 -11
- package/src/index.ts +0 -1
- package/tsconfig.json +0 -9
- package/tsdown.config.ts +0 -9
|
@@ -1,606 +0,0 @@
|
|
|
1
|
-
import { S3Client } from '@aws-sdk/client-s3';
|
|
2
|
-
import { InMemoryCache } from '@geekmidas/cache/memory';
|
|
3
|
-
import { beforeAll, describe, expect, it } from 'vitest';
|
|
4
|
-
import {
|
|
5
|
-
AmazonCannedAccessControlList,
|
|
6
|
-
AmazonStorageClient,
|
|
7
|
-
} from '../AmazonStorageClient';
|
|
8
|
-
import { StorageProvider } from '../StorageClient';
|
|
9
|
-
|
|
10
|
-
describe('AmazonStorageClient Integration Tests', () => {
|
|
11
|
-
let client: AmazonStorageClient;
|
|
12
|
-
const testBucket = 'geekmidas';
|
|
13
|
-
const testKey = 'test-files/test.txt';
|
|
14
|
-
const testContent = 'Hello, World!';
|
|
15
|
-
const testContentType = 'text/plain';
|
|
16
|
-
|
|
17
|
-
beforeAll(async () => {
|
|
18
|
-
// Create storage client pointing to MinIO
|
|
19
|
-
client = AmazonStorageClient.create({
|
|
20
|
-
bucket: testBucket,
|
|
21
|
-
region: 'us-east-1',
|
|
22
|
-
accessKeyId: 'geekmidas',
|
|
23
|
-
secretAccessKey: 'geekmidas',
|
|
24
|
-
endpoint: 'http://localhost:9000',
|
|
25
|
-
acl: AmazonCannedAccessControlList.PublicRead,
|
|
26
|
-
forcePathStyle: true,
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
// Wait for MinIO to be ready
|
|
30
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
describe('provider property', () => {
|
|
34
|
-
it('should return AWS S3 provider', () => {
|
|
35
|
-
expect(client.provider).toBe(StorageProvider.AWSS3);
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
describe('upload and download workflow', () => {
|
|
40
|
-
it.skip('should upload a file directly and generate download URL', async () => {
|
|
41
|
-
// Upload file directly
|
|
42
|
-
await client.upload(testKey, testContent, testContentType);
|
|
43
|
-
|
|
44
|
-
// Generate download URL
|
|
45
|
-
const downloadUrl = await client.getDownloadURL({ path: testKey });
|
|
46
|
-
|
|
47
|
-
expect(downloadUrl).toMatch(
|
|
48
|
-
/^http:\/\/localhost:9000\/geekmidas\/test-files\/test\.txt/,
|
|
49
|
-
);
|
|
50
|
-
expect(downloadUrl).toContain('X-Amz-Algorithm=AWS4-HMAC-SHA256');
|
|
51
|
-
expect(downloadUrl).toContain('X-Amz-Credential=');
|
|
52
|
-
expect(downloadUrl).toContain('X-Amz-Date=');
|
|
53
|
-
expect(downloadUrl).toContain('X-Amz-SignedHeaders=');
|
|
54
|
-
expect(downloadUrl).toContain('X-Amz-Signature=');
|
|
55
|
-
|
|
56
|
-
// Verify file can be downloaded
|
|
57
|
-
const response = await fetch(downloadUrl);
|
|
58
|
-
expect(response.ok).toBe(true);
|
|
59
|
-
|
|
60
|
-
const downloadedContent = await response.text();
|
|
61
|
-
expect(downloadedContent).toBe(testContent);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('should upload a file with filename and generate download URL with content disposition', async () => {
|
|
65
|
-
const keyWithName = 'test-files/named-file.txt';
|
|
66
|
-
const fileName = 'my-document.txt';
|
|
67
|
-
|
|
68
|
-
await client.upload(keyWithName, testContent, testContentType);
|
|
69
|
-
|
|
70
|
-
const downloadUrl = await client.getDownloadURL({
|
|
71
|
-
path: keyWithName,
|
|
72
|
-
name: fileName,
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
expect(downloadUrl).toContain(
|
|
76
|
-
'response-content-disposition=attachment%3B%20filename%3Dmy-document.txt',
|
|
77
|
-
);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('should generate download URL with inline disposition', async () => {
|
|
81
|
-
const key = 'test-files/inline-file.txt';
|
|
82
|
-
await client.upload(key, testContent, testContentType);
|
|
83
|
-
|
|
84
|
-
const downloadUrl = await client.getDownloadURL({
|
|
85
|
-
path: key,
|
|
86
|
-
disposition: 'inline',
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
expect(downloadUrl).toContain('response-content-disposition=inline');
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('should generate download URL with inline disposition and filename', async () => {
|
|
93
|
-
const key = 'test-files/inline-named.txt';
|
|
94
|
-
await client.upload(key, testContent, testContentType);
|
|
95
|
-
|
|
96
|
-
const downloadUrl = await client.getDownloadURL({
|
|
97
|
-
path: key,
|
|
98
|
-
name: 'preview.txt',
|
|
99
|
-
disposition: 'inline',
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
expect(downloadUrl).toContain(
|
|
103
|
-
'response-content-disposition=inline%3B%20filename%3Dpreview.txt',
|
|
104
|
-
);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('should generate download URL with response content type override', async () => {
|
|
108
|
-
const key = 'test-files/content-type-override.bin';
|
|
109
|
-
await client.upload(key, testContent, 'application/octet-stream');
|
|
110
|
-
|
|
111
|
-
const downloadUrl = await client.getDownloadURL({
|
|
112
|
-
path: key,
|
|
113
|
-
responseContentType: 'application/pdf',
|
|
114
|
-
disposition: 'inline',
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
expect(downloadUrl).toContain('response-content-type=application%2Fpdf');
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it('should default to attachment disposition when name is set', async () => {
|
|
121
|
-
const key = 'test-files/default-disposition.txt';
|
|
122
|
-
await client.upload(key, testContent, testContentType);
|
|
123
|
-
|
|
124
|
-
const downloadUrl = await client.getDownloadURL({
|
|
125
|
-
path: key,
|
|
126
|
-
name: 'download.txt',
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
expect(downloadUrl).toContain(
|
|
130
|
-
'response-content-disposition=attachment%3B%20filename%3Ddownload.txt',
|
|
131
|
-
);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it('should handle binary data upload', async () => {
|
|
135
|
-
const binaryKey = 'test-files/binary.bin';
|
|
136
|
-
const binaryData = Buffer.from([0x48, 0x65, 0x6c, 0x6c, 0x6f]); // "Hello" in bytes
|
|
137
|
-
|
|
138
|
-
await client.upload(binaryKey, binaryData, 'application/octet-stream');
|
|
139
|
-
|
|
140
|
-
const downloadUrl = await client.getDownloadURL({ path: binaryKey });
|
|
141
|
-
const response = await fetch(downloadUrl);
|
|
142
|
-
const downloadedBuffer = await response.arrayBuffer();
|
|
143
|
-
|
|
144
|
-
expect(new Uint8Array(downloadedBuffer)).toEqual(
|
|
145
|
-
new Uint8Array(binaryData),
|
|
146
|
-
);
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
describe('presigned upload URLs', () => {
|
|
151
|
-
it('should generate presigned PUT upload URL', async () => {
|
|
152
|
-
const uploadKey = 'test-files/presigned-upload.txt';
|
|
153
|
-
const uploadUrl = await client.getUploadURL({
|
|
154
|
-
path: uploadKey,
|
|
155
|
-
contentType: testContentType,
|
|
156
|
-
contentLength: testContent.length,
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
expect(uploadUrl).toMatch(
|
|
160
|
-
/^http:\/\/localhost:9000\/geekmidas\/test-files\/presigned-upload\.txt/,
|
|
161
|
-
);
|
|
162
|
-
expect(uploadUrl).toContain('X-Amz-Algorithm=AWS4-HMAC-SHA256');
|
|
163
|
-
|
|
164
|
-
// Test actual upload using the presigned URL
|
|
165
|
-
const uploadResponse = await fetch(uploadUrl, {
|
|
166
|
-
method: 'PUT',
|
|
167
|
-
headers: {
|
|
168
|
-
'Content-Type': testContentType,
|
|
169
|
-
'Content-Length': testContent.length.toString(),
|
|
170
|
-
},
|
|
171
|
-
body: testContent,
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
expect(uploadResponse.ok).toBe(true);
|
|
175
|
-
|
|
176
|
-
// Verify file was uploaded
|
|
177
|
-
const downloadUrl = await client.getDownloadURL({ path: uploadKey });
|
|
178
|
-
const downloadResponse = await fetch(downloadUrl);
|
|
179
|
-
const downloadedContent = await downloadResponse.text();
|
|
180
|
-
expect(downloadedContent).toBe(testContent);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it('should generate presigned POST upload with form fields', async () => {
|
|
184
|
-
const uploadKey = 'test-files/presigned-post.txt';
|
|
185
|
-
const uploadData = await client.getUpload({
|
|
186
|
-
path: uploadKey,
|
|
187
|
-
contentType: testContentType,
|
|
188
|
-
contentLength: testContent.length,
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
expect(uploadData.url).toBe('http://localhost:9000/geekmidas');
|
|
192
|
-
expect(uploadData.fields).toContainEqual({
|
|
193
|
-
key: 'key',
|
|
194
|
-
value: uploadKey,
|
|
195
|
-
});
|
|
196
|
-
expect(uploadData.fields).toContainEqual({
|
|
197
|
-
key: 'acl',
|
|
198
|
-
value: 'public-read',
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
// Test actual upload using the presigned POST
|
|
202
|
-
const formData = new FormData();
|
|
203
|
-
uploadData.fields.forEach(({ key, value }) => {
|
|
204
|
-
formData.append(key, value);
|
|
205
|
-
});
|
|
206
|
-
formData.append(
|
|
207
|
-
'file',
|
|
208
|
-
new Blob([testContent], { type: testContentType }),
|
|
209
|
-
);
|
|
210
|
-
|
|
211
|
-
const uploadResponse = await fetch(uploadData.url, {
|
|
212
|
-
method: 'POST',
|
|
213
|
-
body: formData,
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
expect(uploadResponse.ok).toBe(true);
|
|
217
|
-
|
|
218
|
-
// Verify file was uploaded
|
|
219
|
-
const downloadUrl = await client.getDownloadURL({ path: uploadKey });
|
|
220
|
-
const downloadResponse = await fetch(downloadUrl);
|
|
221
|
-
const downloadedContent = await downloadResponse.blob();
|
|
222
|
-
expect(downloadedContent).toEqual(
|
|
223
|
-
new Blob([testContent], { type: testContentType }),
|
|
224
|
-
);
|
|
225
|
-
});
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
describe('versioning', () => {
|
|
229
|
-
it('should handle file versions', async () => {
|
|
230
|
-
const versionKey = 'test-files/versioned.txt';
|
|
231
|
-
const content1 = 'Version 1';
|
|
232
|
-
const content2 = 'Version 2';
|
|
233
|
-
|
|
234
|
-
// Upload first version
|
|
235
|
-
await client.upload(versionKey, content1, testContentType);
|
|
236
|
-
|
|
237
|
-
// Upload second version
|
|
238
|
-
await client.upload(versionKey, content2, testContentType);
|
|
239
|
-
|
|
240
|
-
// Get versions - MinIO might not enable versioning by default
|
|
241
|
-
// This test verifies the method works even if versioning is not enabled
|
|
242
|
-
const versions = await client.getVersions(versionKey);
|
|
243
|
-
expect(Array.isArray(versions)).toBe(true);
|
|
244
|
-
|
|
245
|
-
// If versioning is enabled, we should have multiple versions
|
|
246
|
-
// If not, we should have at least the current version
|
|
247
|
-
if (versions.length > 0) {
|
|
248
|
-
expect(versions[0]).toHaveProperty('id');
|
|
249
|
-
expect(versions[0]).toHaveProperty('createdAt');
|
|
250
|
-
expect(versions[0].createdAt).toBeInstanceOf(Date);
|
|
251
|
-
}
|
|
252
|
-
});
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
describe('error handling', () => {
|
|
256
|
-
it('should handle download URL generation for non-existent files', async () => {
|
|
257
|
-
// This should not throw - presigned URLs can be generated for non-existent files
|
|
258
|
-
const downloadUrl = await client.getDownloadURL({
|
|
259
|
-
path: 'non-existent/file.txt',
|
|
260
|
-
});
|
|
261
|
-
expect(downloadUrl).toMatch(
|
|
262
|
-
/^http:\/\/localhost:9000\/geekmidas\/non-existent\/file\.txt/,
|
|
263
|
-
);
|
|
264
|
-
|
|
265
|
-
// But accessing the URL should return 404
|
|
266
|
-
const response = await fetch(downloadUrl);
|
|
267
|
-
expect(response.status).toBe(404);
|
|
268
|
-
});
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
describe('custom expiration times', () => {
|
|
272
|
-
it('should respect custom expiration for download URLs', async () => {
|
|
273
|
-
const key = 'test-files/expiration-test.txt';
|
|
274
|
-
await client.upload(key, testContent, testContentType);
|
|
275
|
-
|
|
276
|
-
const shortExpirationUrl = await client.getDownloadURL({ path: key }, 1); // 1 second
|
|
277
|
-
const longExpirationUrl = await client.getDownloadURL(
|
|
278
|
-
{ path: key },
|
|
279
|
-
3600,
|
|
280
|
-
); // 1 hour
|
|
281
|
-
|
|
282
|
-
expect(shortExpirationUrl).toContain('X-Amz-Expires=1');
|
|
283
|
-
expect(longExpirationUrl).toContain('X-Amz-Expires=3600');
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
it('should respect custom expiration for upload URLs', async () => {
|
|
287
|
-
const key = 'test-files/upload-expiration-test.txt';
|
|
288
|
-
|
|
289
|
-
const shortExpirationUrl = await client.getUploadURL(
|
|
290
|
-
{
|
|
291
|
-
path: key,
|
|
292
|
-
contentType: testContentType,
|
|
293
|
-
contentLength: testContent.length,
|
|
294
|
-
},
|
|
295
|
-
1,
|
|
296
|
-
); // 1 second
|
|
297
|
-
|
|
298
|
-
const longExpirationUrl = await client.getUploadURL(
|
|
299
|
-
{
|
|
300
|
-
path: key,
|
|
301
|
-
contentType: testContentType,
|
|
302
|
-
contentLength: testContent.length,
|
|
303
|
-
},
|
|
304
|
-
3600,
|
|
305
|
-
); // 1 hour
|
|
306
|
-
|
|
307
|
-
expect(shortExpirationUrl).toContain('X-Amz-Expires=1');
|
|
308
|
-
expect(longExpirationUrl).toContain('X-Amz-Expires=3600');
|
|
309
|
-
});
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
describe('factory method', () => {
|
|
313
|
-
it('should create client with minimal configuration', () => {
|
|
314
|
-
const minimalClient = AmazonStorageClient.create({
|
|
315
|
-
bucket: 'geekmidas',
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
expect(minimalClient).toBeInstanceOf(AmazonStorageClient);
|
|
319
|
-
expect(minimalClient.provider).toBe(StorageProvider.AWSS3);
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
it('should create client with full configuration', () => {
|
|
323
|
-
const fullClient = AmazonStorageClient.create({
|
|
324
|
-
bucket: 'geekmidas',
|
|
325
|
-
region: 'us-west-2',
|
|
326
|
-
accessKeyId: 'test-key',
|
|
327
|
-
secretAccessKey: 'test-secret',
|
|
328
|
-
endpoint: 'https://custom-s3.example.com',
|
|
329
|
-
acl: AmazonCannedAccessControlList.Private,
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
expect(fullClient).toBeInstanceOf(AmazonStorageClient);
|
|
333
|
-
expect(fullClient.provider).toBe(StorageProvider.AWSS3);
|
|
334
|
-
});
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
describe('different ACL configurations', () => {
|
|
338
|
-
it('should work with different ACL settings', async () => {
|
|
339
|
-
const privateClient = AmazonStorageClient.create({
|
|
340
|
-
bucket: testBucket,
|
|
341
|
-
region: 'us-east-1',
|
|
342
|
-
accessKeyId: 'minioadmin',
|
|
343
|
-
secretAccessKey: 'minioadmin',
|
|
344
|
-
endpoint: 'http://localhost:9000',
|
|
345
|
-
acl: AmazonCannedAccessControlList.Private,
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
const uploadData = await privateClient.getUpload({
|
|
349
|
-
path: 'test-files/private.txt',
|
|
350
|
-
contentType: testContentType,
|
|
351
|
-
contentLength: testContent.length,
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
expect(uploadData.fields).toContainEqual({
|
|
355
|
-
key: 'acl',
|
|
356
|
-
value: 'private',
|
|
357
|
-
});
|
|
358
|
-
});
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
describe('S3Client integration', () => {
|
|
362
|
-
it('should work with externally created S3Client', () => {
|
|
363
|
-
const s3Client = new S3Client({
|
|
364
|
-
region: 'us-east-1',
|
|
365
|
-
credentials: {
|
|
366
|
-
accessKeyId: 'minioadmin',
|
|
367
|
-
secretAccessKey: 'minioadmin',
|
|
368
|
-
},
|
|
369
|
-
endpoint: 'http://localhost:9000',
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
const storageClient = new AmazonStorageClient(
|
|
373
|
-
s3Client,
|
|
374
|
-
testBucket,
|
|
375
|
-
AmazonCannedAccessControlList.PublicRead,
|
|
376
|
-
);
|
|
377
|
-
|
|
378
|
-
expect(storageClient).toBeInstanceOf(AmazonStorageClient);
|
|
379
|
-
expect(storageClient.provider).toBe(StorageProvider.AWSS3);
|
|
380
|
-
});
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
describe('cache functionality', () => {
|
|
384
|
-
it('should cache download URLs', async () => {
|
|
385
|
-
const cache = new InMemoryCache();
|
|
386
|
-
const clientWithCache = AmazonStorageClient.create({
|
|
387
|
-
bucket: testBucket,
|
|
388
|
-
region: 'us-east-1',
|
|
389
|
-
accessKeyId: 'geekmidas',
|
|
390
|
-
secretAccessKey: 'geekmidas',
|
|
391
|
-
endpoint: 'http://localhost:9000',
|
|
392
|
-
forcePathStyle: true,
|
|
393
|
-
cache,
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
const key = 'test-files/cache-test.txt';
|
|
397
|
-
await clientWithCache.upload(key, testContent, testContentType);
|
|
398
|
-
|
|
399
|
-
// First call should generate and cache the URL
|
|
400
|
-
const url1 = await clientWithCache.getDownloadURL({ path: key });
|
|
401
|
-
|
|
402
|
-
// Second call should return the cached URL
|
|
403
|
-
const url2 = await clientWithCache.getDownloadURL({ path: key });
|
|
404
|
-
|
|
405
|
-
// URLs should be identical (same cached value)
|
|
406
|
-
expect(url1).toBe(url2);
|
|
407
|
-
|
|
408
|
-
// Verify the URL was cached
|
|
409
|
-
const cacheKey = `download-url:${key}`;
|
|
410
|
-
const cachedUrl = await cache.get(cacheKey);
|
|
411
|
-
expect(cachedUrl).toBe(url1);
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
it('should respect cache expiration based on URL expiry', async () => {
|
|
415
|
-
const cache = new InMemoryCache();
|
|
416
|
-
const clientWithCache = AmazonStorageClient.create({
|
|
417
|
-
bucket: testBucket,
|
|
418
|
-
region: 'us-east-1',
|
|
419
|
-
accessKeyId: 'geekmidas',
|
|
420
|
-
secretAccessKey: 'geekmidas',
|
|
421
|
-
endpoint: 'http://localhost:9000',
|
|
422
|
-
forcePathStyle: true,
|
|
423
|
-
cache,
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
const key = 'test-files/cache-expiry.txt';
|
|
427
|
-
await clientWithCache.upload(key, testContent, testContentType);
|
|
428
|
-
|
|
429
|
-
// Generate URL with short expiry that won't be cached
|
|
430
|
-
await clientWithCache.getDownloadURL({ path: key }, 30); // 30 seconds
|
|
431
|
-
|
|
432
|
-
// Check cache doesn't contain the URL (too short to cache)
|
|
433
|
-
const cacheKey = `download-url:${key}`;
|
|
434
|
-
const cachedShortUrl = await cache.get(cacheKey);
|
|
435
|
-
expect(cachedShortUrl).toBeUndefined();
|
|
436
|
-
|
|
437
|
-
// Generate URL with longer expiry that will be cached
|
|
438
|
-
const longExpiryUrl = await clientWithCache.getDownloadURL(
|
|
439
|
-
{ path: key },
|
|
440
|
-
3600,
|
|
441
|
-
); // 1 hour
|
|
442
|
-
|
|
443
|
-
// Check cache contains the URL
|
|
444
|
-
const cachedLongUrl = await cache.get(cacheKey);
|
|
445
|
-
expect(cachedLongUrl).toBe(longExpiryUrl);
|
|
446
|
-
});
|
|
447
|
-
|
|
448
|
-
it('should use cached URL when available', async () => {
|
|
449
|
-
const cache = new InMemoryCache();
|
|
450
|
-
const clientWithCache = AmazonStorageClient.create({
|
|
451
|
-
bucket: testBucket,
|
|
452
|
-
region: 'us-east-1',
|
|
453
|
-
accessKeyId: 'geekmidas',
|
|
454
|
-
secretAccessKey: 'geekmidas',
|
|
455
|
-
endpoint: 'http://localhost:9000',
|
|
456
|
-
forcePathStyle: true,
|
|
457
|
-
cache,
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
const key = 'test-files/pre-cached.txt';
|
|
461
|
-
const cachedUrl = 'https://pre-cached-url.example.com';
|
|
462
|
-
const cacheKey = `download-url:${key}`;
|
|
463
|
-
|
|
464
|
-
// Pre-populate cache
|
|
465
|
-
await cache.set(cacheKey, cachedUrl);
|
|
466
|
-
|
|
467
|
-
// Get download URL should return cached value
|
|
468
|
-
const url = await clientWithCache.getDownloadURL({ path: key });
|
|
469
|
-
expect(url).toBe(cachedUrl);
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
it('should work without cache', async () => {
|
|
473
|
-
// Client without cache should work normally
|
|
474
|
-
const clientNoCache = AmazonStorageClient.create({
|
|
475
|
-
bucket: testBucket,
|
|
476
|
-
region: 'us-east-1',
|
|
477
|
-
accessKeyId: 'geekmidas',
|
|
478
|
-
secretAccessKey: 'geekmidas',
|
|
479
|
-
endpoint: 'http://localhost:9000',
|
|
480
|
-
forcePathStyle: true,
|
|
481
|
-
});
|
|
482
|
-
|
|
483
|
-
const key = 'test-files/no-cache.txt';
|
|
484
|
-
await clientNoCache.upload(key, testContent, testContentType);
|
|
485
|
-
|
|
486
|
-
const url1 = await clientNoCache.getDownloadURL({ path: key });
|
|
487
|
-
const url2 = await clientNoCache.getDownloadURL({ path: key });
|
|
488
|
-
|
|
489
|
-
// Without cache, new URLs might be generated each time
|
|
490
|
-
// Both should be valid URLs
|
|
491
|
-
expect(url1).toMatch(
|
|
492
|
-
/^http:\/\/localhost:9000\/geekmidas\/test-files\/no-cache\.txt/,
|
|
493
|
-
);
|
|
494
|
-
expect(url2).toMatch(
|
|
495
|
-
/^http:\/\/localhost:9000\/geekmidas\/test-files\/no-cache\.txt/,
|
|
496
|
-
);
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
it('should pass cache to constructor', () => {
|
|
500
|
-
const cache = new InMemoryCache();
|
|
501
|
-
const s3Client = new S3Client({
|
|
502
|
-
region: 'us-east-1',
|
|
503
|
-
credentials: {
|
|
504
|
-
accessKeyId: 'geekmidas',
|
|
505
|
-
secretAccessKey: 'geekmidas',
|
|
506
|
-
},
|
|
507
|
-
endpoint: 'http://localhost:9000',
|
|
508
|
-
forcePathStyle: true,
|
|
509
|
-
});
|
|
510
|
-
|
|
511
|
-
const clientWithCache = new AmazonStorageClient(
|
|
512
|
-
s3Client,
|
|
513
|
-
testBucket,
|
|
514
|
-
AmazonCannedAccessControlList.PublicRead,
|
|
515
|
-
cache,
|
|
516
|
-
);
|
|
517
|
-
|
|
518
|
-
expect(clientWithCache.cache).toBe(cache);
|
|
519
|
-
});
|
|
520
|
-
});
|
|
521
|
-
|
|
522
|
-
describe('delete', () => {
|
|
523
|
-
it('should delete an uploaded file', async () => {
|
|
524
|
-
const key = 'test-files/delete-test.txt';
|
|
525
|
-
await client.upload(key, testContent, testContentType);
|
|
526
|
-
|
|
527
|
-
// Verify file exists
|
|
528
|
-
const downloadUrl = await client.getDownloadURL({ path: key });
|
|
529
|
-
const before = await fetch(downloadUrl);
|
|
530
|
-
expect(before.ok).toBe(true);
|
|
531
|
-
|
|
532
|
-
// Delete file
|
|
533
|
-
await client.delete(key);
|
|
534
|
-
|
|
535
|
-
// Verify file is gone
|
|
536
|
-
const afterUrl = await client.getDownloadURL({ path: key });
|
|
537
|
-
const after = await fetch(afterUrl);
|
|
538
|
-
expect(after.status).toBe(404);
|
|
539
|
-
});
|
|
540
|
-
|
|
541
|
-
it('should not throw when deleting a non-existent file', async () => {
|
|
542
|
-
await expect(
|
|
543
|
-
client.delete('non-existent/file.txt'),
|
|
544
|
-
).resolves.not.toThrow();
|
|
545
|
-
});
|
|
546
|
-
});
|
|
547
|
-
|
|
548
|
-
describe('getVersionDownloadURL', () => {
|
|
549
|
-
it('should generate version-specific download URL', async () => {
|
|
550
|
-
const versionKey = 'test-files/version-download.txt';
|
|
551
|
-
const versionId = 'test-version-id';
|
|
552
|
-
|
|
553
|
-
// Upload a file first
|
|
554
|
-
await client.upload(versionKey, testContent, testContentType);
|
|
555
|
-
|
|
556
|
-
// Generate version-specific download URL
|
|
557
|
-
const downloadUrl = await client.getVersionDownloadURL(
|
|
558
|
-
{ path: versionKey, name: 'versioned-file.txt' },
|
|
559
|
-
versionId,
|
|
560
|
-
);
|
|
561
|
-
|
|
562
|
-
expect(downloadUrl).toMatch(
|
|
563
|
-
/^http:\/\/localhost:9000\/geekmidas\/test-files\/version-download\.txt/,
|
|
564
|
-
);
|
|
565
|
-
expect(downloadUrl).toContain('X-Amz-Algorithm=AWS4-HMAC-SHA256');
|
|
566
|
-
expect(downloadUrl).toContain(
|
|
567
|
-
'response-content-disposition=attachment%3B%20filename%3Dversioned-file.txt',
|
|
568
|
-
);
|
|
569
|
-
});
|
|
570
|
-
|
|
571
|
-
it('should generate version URL with inline disposition', async () => {
|
|
572
|
-
const versionKey = 'test-files/version-inline.txt';
|
|
573
|
-
const versionId = 'test-version-id';
|
|
574
|
-
|
|
575
|
-
await client.upload(versionKey, testContent, testContentType);
|
|
576
|
-
|
|
577
|
-
const downloadUrl = await client.getVersionDownloadURL(
|
|
578
|
-
{ path: versionKey, name: 'inline-file.txt', disposition: 'inline' },
|
|
579
|
-
versionId,
|
|
580
|
-
);
|
|
581
|
-
|
|
582
|
-
expect(downloadUrl).toContain(
|
|
583
|
-
'response-content-disposition=inline%3B%20filename%3Dinline-file.txt',
|
|
584
|
-
);
|
|
585
|
-
});
|
|
586
|
-
|
|
587
|
-
it('should generate version URL without filename', async () => {
|
|
588
|
-
const versionKey = 'test-files/version-no-name.txt';
|
|
589
|
-
const versionId = 'test-version-id';
|
|
590
|
-
|
|
591
|
-
// Upload a file first
|
|
592
|
-
await client.upload(versionKey, testContent, testContentType);
|
|
593
|
-
|
|
594
|
-
// Generate version-specific download URL without filename
|
|
595
|
-
const downloadUrl = await client.getVersionDownloadURL(
|
|
596
|
-
{ path: versionKey },
|
|
597
|
-
versionId,
|
|
598
|
-
);
|
|
599
|
-
|
|
600
|
-
expect(downloadUrl).toMatch(
|
|
601
|
-
/^http:\/\/localhost:9000\/geekmidas\/test-files\/version-no-name\.txt/,
|
|
602
|
-
);
|
|
603
|
-
expect(downloadUrl).not.toContain('response-content-disposition');
|
|
604
|
-
});
|
|
605
|
-
});
|
|
606
|
-
});
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { StorageProvider } from '../StorageClient';
|
|
3
|
-
|
|
4
|
-
describe('StorageClient', () => {
|
|
5
|
-
describe('StorageProvider enum', () => {
|
|
6
|
-
it('should have correct provider values', () => {
|
|
7
|
-
expect(StorageProvider.AWSS3).toBe('geekimdas.toolbox.storage.aws.s3');
|
|
8
|
-
expect(StorageProvider.GCP).toBe('geekimdas.toolbox.storage.gcp');
|
|
9
|
-
expect(StorageProvider.AZURE).toBe('geekimdas.toolbox.storage.azure');
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it('should have unique provider values', () => {
|
|
13
|
-
const values = Object.values(StorageProvider);
|
|
14
|
-
const uniqueValues = new Set(values);
|
|
15
|
-
expect(uniqueValues.size).toBe(values.length);
|
|
16
|
-
});
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
describe('interfaces', () => {
|
|
20
|
-
it('should define DocumentVersion interface correctly', () => {
|
|
21
|
-
// This is a compile-time test to ensure the interface exists
|
|
22
|
-
// and has the correct shape
|
|
23
|
-
const version: import('../StorageClient').DocumentVersion = {
|
|
24
|
-
id: 'test-id',
|
|
25
|
-
createdAt: new Date(),
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
expect(version.id).toBe('test-id');
|
|
29
|
-
expect(version.createdAt).toBeInstanceOf(Date);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('should define File interface correctly', () => {
|
|
33
|
-
const file: import('../StorageClient').File = {
|
|
34
|
-
path: 'test/path.txt',
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
expect(file.path).toBe('test/path.txt');
|
|
38
|
-
expect(file.name).toBeUndefined();
|
|
39
|
-
|
|
40
|
-
const fileWithName: import('../StorageClient').File = {
|
|
41
|
-
path: 'test/path.txt',
|
|
42
|
-
name: 'display-name.txt',
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
expect(fileWithName.path).toBe('test/path.txt');
|
|
46
|
-
expect(fileWithName.name).toBe('display-name.txt');
|
|
47
|
-
|
|
48
|
-
const fileWithDisposition: import('../StorageClient').File = {
|
|
49
|
-
path: 'test/path.txt',
|
|
50
|
-
disposition: 'inline',
|
|
51
|
-
responseContentType: 'application/pdf',
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
expect(fileWithDisposition.disposition).toBe('inline');
|
|
55
|
-
expect(fileWithDisposition.responseContentType).toBe('application/pdf');
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('should define GetUploadParams interface correctly', () => {
|
|
59
|
-
const params: import('../StorageClient').GetUploadParams = {
|
|
60
|
-
path: 'test/upload.txt',
|
|
61
|
-
contentType: 'text/plain',
|
|
62
|
-
contentLength: 100,
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
expect(params.path).toBe('test/upload.txt');
|
|
66
|
-
expect(params.contentType).toBe('text/plain');
|
|
67
|
-
expect(params.contentLength).toBe(100);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('should define GetUploadResponse interface correctly', () => {
|
|
71
|
-
const response: import('../StorageClient').GetUploadResponse = {
|
|
72
|
-
url: 'https://example.com/upload',
|
|
73
|
-
fields: [
|
|
74
|
-
{ key: 'key', value: 'test/path.txt' },
|
|
75
|
-
{ key: 'acl', value: 'public-read' },
|
|
76
|
-
],
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
expect(response.url).toBe('https://example.com/upload');
|
|
80
|
-
expect(response.fields).toHaveLength(2);
|
|
81
|
-
expect(response.fields[0]).toEqual({
|
|
82
|
-
key: 'key',
|
|
83
|
-
value: 'test/path.txt',
|
|
84
|
-
});
|
|
85
|
-
expect(response.fields[1]).toEqual({ key: 'acl', value: 'public-read' });
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('should define UploadField type correctly', () => {
|
|
89
|
-
const field: import('../StorageClient').UploadField = {
|
|
90
|
-
key: 'test-key',
|
|
91
|
-
value: 'test-value',
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
expect(field.key).toBe('test-key');
|
|
95
|
-
expect(field.value).toBe('test-value');
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
});
|