@geekmidas/storage 0.0.4 → 0.0.5
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 +71 -1
- package/dist/{AmazonStorageClient-DJMWn8vO.mjs → AmazonStorageClient-Bkw-x2o2.mjs} +12 -5
- package/dist/{AmazonStorageClient-C16fG3G4.d.cts → AmazonStorageClient-DaktozmE.d.mts} +5 -2
- package/dist/{AmazonStorageClient-CP8Tx5oE.d.mts → AmazonStorageClient-aduVU3_p.d.cts} +5 -2
- package/dist/{AmazonStorageClient-DT9DeKVz.cjs → AmazonStorageClient-tyGekctm.cjs} +12 -5
- package/dist/AmazonStorageClient.cjs +1 -1
- package/dist/AmazonStorageClient.d.cts +2 -2
- package/dist/AmazonStorageClient.d.mts +2 -2
- package/dist/AmazonStorageClient.mjs +1 -1
- package/dist/{StorageClient-D-y4QLDq.d.cts → StorageClient-CcOAW3YV.d.cts} +3 -0
- package/dist/{StorageClient-CJbHRTVV.d.mts → StorageClient-CveQoxJa.d.mts} +3 -0
- package/dist/StorageClient.d.cts +1 -1
- package/dist/StorageClient.d.mts +1 -1
- package/dist/__tests__/AmazonStorageClient.spec.cjs +115 -1
- package/dist/__tests__/AmazonStorageClient.spec.mjs +115 -1
- package/dist/aws.cjs +1 -1
- package/dist/aws.d.cts +2 -2
- package/dist/aws.d.mts +2 -2
- package/dist/aws.mjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/package.json +4 -2
- package/src/AmazonStorageClient.ts +22 -3
- package/src/StorageClient.ts +2 -0
- package/src/__tests__/AmazonStorageClient.spec.ts +183 -0
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ A comprehensive, type-safe storage client for cloud storage services with suppor
|
|
|
11
11
|
- **Direct uploads**: Upload files directly to storage without intermediate servers
|
|
12
12
|
- **Flexible configuration**: Support for custom endpoints (useful for MinIO, LocalStack, etc.)
|
|
13
13
|
- **Modern async/await API**: Promise-based interface throughout
|
|
14
|
+
- **URL caching**: Built-in support for caching presigned URLs to reduce API calls and improve performance
|
|
14
15
|
|
|
15
16
|
## Installation
|
|
16
17
|
|
|
@@ -32,6 +33,7 @@ npm install @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-pr
|
|
|
32
33
|
|
|
33
34
|
```typescript
|
|
34
35
|
import { AmazonStorageClient } from '@geekmidas/storage/aws';
|
|
36
|
+
import { InMemoryCache } from '@geekmidas/cache/memory';
|
|
35
37
|
|
|
36
38
|
// Create client with credentials
|
|
37
39
|
const storage = AmazonStorageClient.create({
|
|
@@ -41,6 +43,16 @@ const storage = AmazonStorageClient.create({
|
|
|
41
43
|
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
|
42
44
|
});
|
|
43
45
|
|
|
46
|
+
// Create client with caching enabled
|
|
47
|
+
const cache = new InMemoryCache<string>();
|
|
48
|
+
const storageWithCache = AmazonStorageClient.create({
|
|
49
|
+
bucket: 'my-bucket',
|
|
50
|
+
region: 'us-east-1',
|
|
51
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
52
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
|
53
|
+
cache,
|
|
54
|
+
});
|
|
55
|
+
|
|
44
56
|
// Upload a file directly
|
|
45
57
|
await storage.upload('documents/readme.txt', 'Hello, World!', 'text/plain');
|
|
46
58
|
|
|
@@ -82,6 +94,7 @@ The core interface that all storage providers implement:
|
|
|
82
94
|
```typescript
|
|
83
95
|
interface StorageClient {
|
|
84
96
|
readonly provider: StorageProvider;
|
|
97
|
+
readonly cache?: Cache<string>;
|
|
85
98
|
|
|
86
99
|
// Direct upload
|
|
87
100
|
upload(key: string, data: string | Buffer, contentType: string): Promise<void>;
|
|
@@ -114,6 +127,8 @@ AmazonStorageClient.create(options: AmazonStorageClientCreateOptions)
|
|
|
114
127
|
- `secretAccessKey`: AWS secret access key
|
|
115
128
|
- `endpoint`: Custom S3 endpoint (useful for MinIO, LocalStack)
|
|
116
129
|
- `acl`: Canned ACL for uploads (default: `authenticated-read`)
|
|
130
|
+
- `cache`: Optional cache implementation for storing presigned URLs
|
|
131
|
+
- `forcePathStyle`: Force path-style URLs (useful for MinIO)
|
|
117
132
|
|
|
118
133
|
#### Methods
|
|
119
134
|
|
|
@@ -132,7 +147,7 @@ await storage.upload('files/binary.dat', buffer, 'application/octet-stream');
|
|
|
132
147
|
|
|
133
148
|
##### `getDownloadURL(file: File, expiresIn?: number): Promise<string>`
|
|
134
149
|
|
|
135
|
-
Generate a presigned download URL.
|
|
150
|
+
Generate a presigned download URL. When a cache is configured, URLs will be cached based on the file path.
|
|
136
151
|
|
|
137
152
|
```typescript
|
|
138
153
|
// Simple download URL
|
|
@@ -148,6 +163,12 @@ const url = await storage.getDownloadURL({
|
|
|
148
163
|
const url = await storage.getDownloadURL({ path: 'documents/file.pdf' }, 3600);
|
|
149
164
|
```
|
|
150
165
|
|
|
166
|
+
**Caching behavior:**
|
|
167
|
+
- URLs are cached with key format: `download-url:{file.path}`
|
|
168
|
+
- Cache TTL is set to `expiresIn - 60` seconds (with 1 minute buffer)
|
|
169
|
+
- URLs with expiration < 60 seconds are not cached
|
|
170
|
+
- Cached URLs are returned immediately without generating new presigned URLs
|
|
171
|
+
|
|
151
172
|
##### `getUploadURL(params: GetUploadParams, expiresIn?: number): Promise<string>`
|
|
152
173
|
|
|
153
174
|
Generate a presigned PUT upload URL.
|
|
@@ -257,6 +278,7 @@ enum StorageProvider {
|
|
|
257
278
|
```typescript
|
|
258
279
|
import { S3Client } from '@aws-sdk/client-s3';
|
|
259
280
|
import { AmazonStorageClient, AmazonCannedAccessControlList } from '@geekmidas/storage/aws';
|
|
281
|
+
import { InMemoryCache } from '@geekmidas/cache/memory';
|
|
260
282
|
|
|
261
283
|
const s3Client = new S3Client({
|
|
262
284
|
region: 'us-east-1',
|
|
@@ -266,11 +288,21 @@ const s3Client = new S3Client({
|
|
|
266
288
|
},
|
|
267
289
|
});
|
|
268
290
|
|
|
291
|
+
// Without cache
|
|
269
292
|
const storage = new AmazonStorageClient(
|
|
270
293
|
s3Client,
|
|
271
294
|
'my-bucket',
|
|
272
295
|
AmazonCannedAccessControlList.PublicRead
|
|
273
296
|
);
|
|
297
|
+
|
|
298
|
+
// With cache
|
|
299
|
+
const cache = new InMemoryCache<string>();
|
|
300
|
+
const storageWithCache = new AmazonStorageClient(
|
|
301
|
+
s3Client,
|
|
302
|
+
'my-bucket',
|
|
303
|
+
AmazonCannedAccessControlList.PublicRead,
|
|
304
|
+
cache
|
|
305
|
+
);
|
|
274
306
|
```
|
|
275
307
|
|
|
276
308
|
### Access Control Lists (ACLs)
|
|
@@ -294,6 +326,44 @@ Available ACLs:
|
|
|
294
326
|
- `LogDeliveryWrite` - Log delivery service gets write access
|
|
295
327
|
- `AwsExecRead` - Amazon EC2 gets read access for AMI bundles
|
|
296
328
|
|
|
329
|
+
### Caching
|
|
330
|
+
|
|
331
|
+
Caching presigned URLs can significantly reduce the number of API calls to AWS S3 and improve performance.
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
import { AmazonStorageClient } from '@geekmidas/storage/aws';
|
|
335
|
+
import { InMemoryCache } from '@geekmidas/cache/memory';
|
|
336
|
+
import { UpstashCache } from '@geekmidas/cache/upstash';
|
|
337
|
+
|
|
338
|
+
// In-memory cache for development/testing
|
|
339
|
+
const memoryCache = new InMemoryCache<string>();
|
|
340
|
+
const storage = AmazonStorageClient.create({
|
|
341
|
+
bucket: 'my-bucket',
|
|
342
|
+
cache: memoryCache,
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// Redis cache for production
|
|
346
|
+
const redisCache = new UpstashCache<string>({
|
|
347
|
+
url: process.env.UPSTASH_REDIS_URL,
|
|
348
|
+
token: process.env.UPSTASH_REDIS_TOKEN,
|
|
349
|
+
});
|
|
350
|
+
const productionStorage = AmazonStorageClient.create({
|
|
351
|
+
bucket: 'my-bucket',
|
|
352
|
+
cache: redisCache,
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// Cache behavior example
|
|
356
|
+
const url1 = await storage.getDownloadURL({ path: 'file.pdf' }); // Generates new URL
|
|
357
|
+
const url2 = await storage.getDownloadURL({ path: 'file.pdf' }); // Returns cached URL
|
|
358
|
+
console.log(url1 === url2); // true
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
**Cache keys and TTL:**
|
|
362
|
+
- Download URLs are cached with key: `download-url:{path}`
|
|
363
|
+
- Cache TTL is automatically calculated as `expiresIn - 60` seconds
|
|
364
|
+
- URLs expiring in less than 60 seconds are not cached
|
|
365
|
+
- Upload URLs are not cached (they are typically single-use)
|
|
366
|
+
|
|
297
367
|
### Error Handling
|
|
298
368
|
|
|
299
369
|
```typescript
|
|
@@ -7,7 +7,7 @@ import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
|
7
7
|
var AmazonStorageClient = class AmazonStorageClient {
|
|
8
8
|
provider = StorageProvider.AWSS3;
|
|
9
9
|
static create(options) {
|
|
10
|
-
const { bucket, region, accessKeyId, acl, endpoint, secretAccessKey, forcePathStyle = false } = options;
|
|
10
|
+
const { bucket, region, accessKeyId, acl, endpoint, secretAccessKey, forcePathStyle = false, cache } = options;
|
|
11
11
|
const hasCredentials = accessKeyId && secretAccessKey;
|
|
12
12
|
const credentials = hasCredentials ? {
|
|
13
13
|
accessKeyId,
|
|
@@ -19,12 +19,13 @@ var AmazonStorageClient = class AmazonStorageClient {
|
|
|
19
19
|
endpoint,
|
|
20
20
|
forcePathStyle
|
|
21
21
|
});
|
|
22
|
-
return new AmazonStorageClient(client, bucket, acl);
|
|
22
|
+
return new AmazonStorageClient(client, bucket, acl, cache);
|
|
23
23
|
}
|
|
24
|
-
constructor(client, bucket, acl = AmazonCannedAccessControlList.AuthenticatedRead) {
|
|
24
|
+
constructor(client, bucket, acl = AmazonCannedAccessControlList.AuthenticatedRead, cache) {
|
|
25
25
|
this.client = client;
|
|
26
26
|
this.bucket = bucket;
|
|
27
27
|
this.acl = acl;
|
|
28
|
+
this.cache = cache;
|
|
28
29
|
}
|
|
29
30
|
getVersionDownloadURL(file, versionId) {
|
|
30
31
|
const ResponseContentDisposition = file.name ? `attachment; filename=${encodeURIComponent(file.name)}` : void 0;
|
|
@@ -47,14 +48,20 @@ var AmazonStorageClient = class AmazonStorageClient {
|
|
|
47
48
|
createdAt: version.LastModified || /* @__PURE__ */ new Date()
|
|
48
49
|
}));
|
|
49
50
|
}
|
|
50
|
-
getDownloadURL(file, expiresIn = 60 * 60) {
|
|
51
|
+
async getDownloadURL(file, expiresIn = 60 * 60) {
|
|
52
|
+
const cacheKey = `download-url:${file.path}`;
|
|
53
|
+
const cachedURL = await this.cache?.get(cacheKey);
|
|
54
|
+
if (cachedURL) return cachedURL;
|
|
51
55
|
const ResponseContentDisposition = file.name ? `attachment; filename=${encodeURIComponent(file.name)}` : void 0;
|
|
52
56
|
const command = new GetObjectCommand({
|
|
53
57
|
Bucket: this.bucket,
|
|
54
58
|
Key: file.path,
|
|
55
59
|
ResponseContentDisposition
|
|
56
60
|
});
|
|
57
|
-
|
|
61
|
+
const url = await getSignedUrl(this.client, command, { expiresIn });
|
|
62
|
+
const cacheExpiration = Math.max(expiresIn - 60, 0);
|
|
63
|
+
if (cacheExpiration) await this.cache?.set(cacheKey, url, cacheExpiration);
|
|
64
|
+
return url;
|
|
58
65
|
}
|
|
59
66
|
async getUploadURL(params, expiresIn = 60 * 60) {
|
|
60
67
|
const command = new PutObjectCommand({
|
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import { DocumentVersion, File, GetUploadParams, GetUploadResponse, StorageClient, StorageProvider } from "./StorageClient-
|
|
1
|
+
import { DocumentVersion, File, GetUploadParams, GetUploadResponse, StorageClient, StorageProvider } from "./StorageClient-CveQoxJa.mjs";
|
|
2
2
|
import { S3Client } from "@aws-sdk/client-s3";
|
|
3
|
+
import { Cache } from "@geekmidas/cache";
|
|
3
4
|
|
|
4
5
|
//#region src/AmazonStorageClient.d.ts
|
|
5
6
|
declare class AmazonStorageClient implements StorageClient {
|
|
6
7
|
private readonly client;
|
|
7
8
|
private readonly bucket;
|
|
8
9
|
private readonly acl;
|
|
10
|
+
readonly cache?: Cache<string>;
|
|
9
11
|
readonly provider = StorageProvider.AWSS3;
|
|
10
12
|
static create(options: AmazonStorageClientCreateOptions): AmazonStorageClient;
|
|
11
|
-
constructor(client: S3Client, bucket: string, acl?: AmazonCannedAccessControlList);
|
|
13
|
+
constructor(client: S3Client, bucket: string, acl?: AmazonCannedAccessControlList, cache?: Cache<string>);
|
|
12
14
|
getVersionDownloadURL(file: File, versionId: string): Promise<string>;
|
|
13
15
|
getVersions(key: string): Promise<DocumentVersion[]>;
|
|
14
16
|
getDownloadURL(file: File, expiresIn?: number): Promise<string>;
|
|
@@ -34,6 +36,7 @@ interface AmazonStorageClientCreateOptions {
|
|
|
34
36
|
secretAccessKey?: string;
|
|
35
37
|
endpoint?: string;
|
|
36
38
|
forcePathStyle?: boolean;
|
|
39
|
+
cache?: Cache<string>;
|
|
37
40
|
}
|
|
38
41
|
//#endregion
|
|
39
42
|
export { AmazonCannedAccessControlList, AmazonStorageClient };
|
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import { DocumentVersion, File, GetUploadParams, GetUploadResponse, StorageClient, StorageProvider } from "./StorageClient-
|
|
1
|
+
import { DocumentVersion, File, GetUploadParams, GetUploadResponse, StorageClient, StorageProvider } from "./StorageClient-CcOAW3YV.cjs";
|
|
2
2
|
import { S3Client } from "@aws-sdk/client-s3";
|
|
3
|
+
import { Cache } from "@geekmidas/cache";
|
|
3
4
|
|
|
4
5
|
//#region src/AmazonStorageClient.d.ts
|
|
5
6
|
declare class AmazonStorageClient implements StorageClient {
|
|
6
7
|
private readonly client;
|
|
7
8
|
private readonly bucket;
|
|
8
9
|
private readonly acl;
|
|
10
|
+
readonly cache?: Cache<string>;
|
|
9
11
|
readonly provider = StorageProvider.AWSS3;
|
|
10
12
|
static create(options: AmazonStorageClientCreateOptions): AmazonStorageClient;
|
|
11
|
-
constructor(client: S3Client, bucket: string, acl?: AmazonCannedAccessControlList);
|
|
13
|
+
constructor(client: S3Client, bucket: string, acl?: AmazonCannedAccessControlList, cache?: Cache<string>);
|
|
12
14
|
getVersionDownloadURL(file: File, versionId: string): Promise<string>;
|
|
13
15
|
getVersions(key: string): Promise<DocumentVersion[]>;
|
|
14
16
|
getDownloadURL(file: File, expiresIn?: number): Promise<string>;
|
|
@@ -34,6 +36,7 @@ interface AmazonStorageClientCreateOptions {
|
|
|
34
36
|
secretAccessKey?: string;
|
|
35
37
|
endpoint?: string;
|
|
36
38
|
forcePathStyle?: boolean;
|
|
39
|
+
cache?: Cache<string>;
|
|
37
40
|
}
|
|
38
41
|
//#endregion
|
|
39
42
|
export { AmazonCannedAccessControlList, AmazonStorageClient };
|
|
@@ -8,7 +8,7 @@ const __aws_sdk_s3_request_presigner = require_chunk.__toESM(require("@aws-sdk/s
|
|
|
8
8
|
var AmazonStorageClient = class AmazonStorageClient {
|
|
9
9
|
provider = require_StorageClient.StorageProvider.AWSS3;
|
|
10
10
|
static create(options) {
|
|
11
|
-
const { bucket, region, accessKeyId, acl, endpoint, secretAccessKey, forcePathStyle = false } = options;
|
|
11
|
+
const { bucket, region, accessKeyId, acl, endpoint, secretAccessKey, forcePathStyle = false, cache } = options;
|
|
12
12
|
const hasCredentials = accessKeyId && secretAccessKey;
|
|
13
13
|
const credentials = hasCredentials ? {
|
|
14
14
|
accessKeyId,
|
|
@@ -20,12 +20,13 @@ var AmazonStorageClient = class AmazonStorageClient {
|
|
|
20
20
|
endpoint,
|
|
21
21
|
forcePathStyle
|
|
22
22
|
});
|
|
23
|
-
return new AmazonStorageClient(client, bucket, acl);
|
|
23
|
+
return new AmazonStorageClient(client, bucket, acl, cache);
|
|
24
24
|
}
|
|
25
|
-
constructor(client, bucket, acl = AmazonCannedAccessControlList.AuthenticatedRead) {
|
|
25
|
+
constructor(client, bucket, acl = AmazonCannedAccessControlList.AuthenticatedRead, cache) {
|
|
26
26
|
this.client = client;
|
|
27
27
|
this.bucket = bucket;
|
|
28
28
|
this.acl = acl;
|
|
29
|
+
this.cache = cache;
|
|
29
30
|
}
|
|
30
31
|
getVersionDownloadURL(file, versionId) {
|
|
31
32
|
const ResponseContentDisposition = file.name ? `attachment; filename=${encodeURIComponent(file.name)}` : void 0;
|
|
@@ -48,14 +49,20 @@ var AmazonStorageClient = class AmazonStorageClient {
|
|
|
48
49
|
createdAt: version.LastModified || /* @__PURE__ */ new Date()
|
|
49
50
|
}));
|
|
50
51
|
}
|
|
51
|
-
getDownloadURL(file, expiresIn = 60 * 60) {
|
|
52
|
+
async getDownloadURL(file, expiresIn = 60 * 60) {
|
|
53
|
+
const cacheKey = `download-url:${file.path}`;
|
|
54
|
+
const cachedURL = await this.cache?.get(cacheKey);
|
|
55
|
+
if (cachedURL) return cachedURL;
|
|
52
56
|
const ResponseContentDisposition = file.name ? `attachment; filename=${encodeURIComponent(file.name)}` : void 0;
|
|
53
57
|
const command = new __aws_sdk_client_s3.GetObjectCommand({
|
|
54
58
|
Bucket: this.bucket,
|
|
55
59
|
Key: file.path,
|
|
56
60
|
ResponseContentDisposition
|
|
57
61
|
});
|
|
58
|
-
|
|
62
|
+
const url = await (0, __aws_sdk_s3_request_presigner.getSignedUrl)(this.client, command, { expiresIn });
|
|
63
|
+
const cacheExpiration = Math.max(expiresIn - 60, 0);
|
|
64
|
+
if (cacheExpiration) await this.cache?.set(cacheKey, url, cacheExpiration);
|
|
65
|
+
return url;
|
|
59
66
|
}
|
|
60
67
|
async getUploadURL(params, expiresIn = 60 * 60) {
|
|
61
68
|
const command = new __aws_sdk_client_s3.PutObjectCommand({
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
require('./StorageClient-BDRVrMJj.cjs');
|
|
2
|
-
const require_AmazonStorageClient = require('./AmazonStorageClient-
|
|
2
|
+
const require_AmazonStorageClient = require('./AmazonStorageClient-tyGekctm.cjs');
|
|
3
3
|
|
|
4
4
|
exports.AmazonCannedAccessControlList = require_AmazonStorageClient.AmazonCannedAccessControlList;
|
|
5
5
|
exports.AmazonStorageClient = require_AmazonStorageClient.AmazonStorageClient;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import "./StorageClient-
|
|
2
|
-
import { AmazonCannedAccessControlList, AmazonStorageClient } from "./AmazonStorageClient-
|
|
1
|
+
import "./StorageClient-CcOAW3YV.cjs";
|
|
2
|
+
import { AmazonCannedAccessControlList, AmazonStorageClient } from "./AmazonStorageClient-aduVU3_p.cjs";
|
|
3
3
|
export { AmazonCannedAccessControlList, AmazonStorageClient };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import "./StorageClient-
|
|
2
|
-
import { AmazonCannedAccessControlList, AmazonStorageClient } from "./AmazonStorageClient-
|
|
1
|
+
import "./StorageClient-CveQoxJa.mjs";
|
|
2
|
+
import { AmazonCannedAccessControlList, AmazonStorageClient } from "./AmazonStorageClient-DaktozmE.mjs";
|
|
3
3
|
export { AmazonCannedAccessControlList, AmazonStorageClient };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import "./StorageClient-CZGIC_fz.mjs";
|
|
2
|
-
import { AmazonCannedAccessControlList, AmazonStorageClient } from "./AmazonStorageClient-
|
|
2
|
+
import { AmazonCannedAccessControlList, AmazonStorageClient } from "./AmazonStorageClient-Bkw-x2o2.mjs";
|
|
3
3
|
|
|
4
4
|
export { AmazonCannedAccessControlList, AmazonStorageClient };
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { Cache } from "@geekmidas/cache";
|
|
2
|
+
|
|
1
3
|
//#region src/StorageClient.d.ts
|
|
2
4
|
interface DocumentVersion {
|
|
3
5
|
id: string;
|
|
@@ -10,6 +12,7 @@ declare enum StorageProvider {
|
|
|
10
12
|
}
|
|
11
13
|
interface StorageClient {
|
|
12
14
|
readonly provider: StorageProvider;
|
|
15
|
+
readonly cache?: Cache<string>;
|
|
13
16
|
/**
|
|
14
17
|
* Get a URL to upload a file to.
|
|
15
18
|
*
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { Cache } from "@geekmidas/cache";
|
|
2
|
+
|
|
1
3
|
//#region src/StorageClient.d.ts
|
|
2
4
|
interface DocumentVersion {
|
|
3
5
|
id: string;
|
|
@@ -10,6 +12,7 @@ declare enum StorageProvider {
|
|
|
10
12
|
}
|
|
11
13
|
interface StorageClient {
|
|
12
14
|
readonly provider: StorageProvider;
|
|
15
|
+
readonly cache?: Cache<string>;
|
|
13
16
|
/**
|
|
14
17
|
* Get a URL to upload a file to.
|
|
15
18
|
*
|
package/dist/StorageClient.d.cts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { DocumentVersion, File, GetUploadParams, GetUploadResponse, StorageClient, StorageProvider, UploadField } from "./StorageClient-
|
|
1
|
+
import { DocumentVersion, File, GetUploadParams, GetUploadResponse, StorageClient, StorageProvider, UploadField } from "./StorageClient-CcOAW3YV.cjs";
|
|
2
2
|
export { DocumentVersion, File, GetUploadParams, GetUploadResponse, StorageClient, StorageProvider, UploadField };
|
package/dist/StorageClient.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { DocumentVersion, File, GetUploadParams, GetUploadResponse, StorageClient, StorageProvider, UploadField } from "./StorageClient-
|
|
1
|
+
import { DocumentVersion, File, GetUploadParams, GetUploadResponse, StorageClient, StorageProvider, UploadField } from "./StorageClient-CveQoxJa.mjs";
|
|
2
2
|
export { DocumentVersion, File, GetUploadParams, GetUploadResponse, StorageClient, StorageProvider, UploadField };
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
const require_chunk = require('../chunk-DWy1uDak.cjs');
|
|
2
2
|
const require_StorageClient = require('../StorageClient-BDRVrMJj.cjs');
|
|
3
|
-
const require_AmazonStorageClient = require('../AmazonStorageClient-
|
|
3
|
+
const require_AmazonStorageClient = require('../AmazonStorageClient-tyGekctm.cjs');
|
|
4
4
|
const require_vi_bdSIJ99Y = require('../vi.bdSIJ99Y-D0m6qImj.cjs');
|
|
5
5
|
const __aws_sdk_client_s3 = require_chunk.__toESM(require("@aws-sdk/client-s3"));
|
|
6
|
+
const __geekmidas_cache_memory = require_chunk.__toESM(require("@geekmidas/cache/memory"));
|
|
6
7
|
|
|
7
8
|
//#region src/__tests__/AmazonStorageClient.spec.ts
|
|
8
9
|
require_vi_bdSIJ99Y.describe("AmazonStorageClient Integration Tests", () => {
|
|
@@ -229,6 +230,119 @@ require_vi_bdSIJ99Y.describe("AmazonStorageClient Integration Tests", () => {
|
|
|
229
230
|
require_vi_bdSIJ99Y.globalExpect(storageClient.provider).toBe(require_StorageClient.StorageProvider.AWSS3);
|
|
230
231
|
});
|
|
231
232
|
});
|
|
233
|
+
require_vi_bdSIJ99Y.describe("cache functionality", () => {
|
|
234
|
+
require_vi_bdSIJ99Y.it("should cache download URLs", async () => {
|
|
235
|
+
const cache = new __geekmidas_cache_memory.InMemoryCache();
|
|
236
|
+
const clientWithCache = require_AmazonStorageClient.AmazonStorageClient.create({
|
|
237
|
+
bucket: testBucket,
|
|
238
|
+
region: "us-east-1",
|
|
239
|
+
accessKeyId: "geekmidas",
|
|
240
|
+
secretAccessKey: "geekmidas",
|
|
241
|
+
endpoint: "http://localhost:9000",
|
|
242
|
+
forcePathStyle: true,
|
|
243
|
+
cache
|
|
244
|
+
});
|
|
245
|
+
const key = "test-files/cache-test.txt";
|
|
246
|
+
await clientWithCache.upload(key, testContent, testContentType);
|
|
247
|
+
const url1 = await clientWithCache.getDownloadURL({ path: key });
|
|
248
|
+
const url2 = await clientWithCache.getDownloadURL({ path: key });
|
|
249
|
+
require_vi_bdSIJ99Y.globalExpect(url1).toBe(url2);
|
|
250
|
+
const cacheKey = `download-url:${key}`;
|
|
251
|
+
const cachedUrl = await cache.get(cacheKey);
|
|
252
|
+
require_vi_bdSIJ99Y.globalExpect(cachedUrl).toBe(url1);
|
|
253
|
+
});
|
|
254
|
+
require_vi_bdSIJ99Y.it("should respect cache expiration based on URL expiry", async () => {
|
|
255
|
+
const cache = new __geekmidas_cache_memory.InMemoryCache();
|
|
256
|
+
const clientWithCache = require_AmazonStorageClient.AmazonStorageClient.create({
|
|
257
|
+
bucket: testBucket,
|
|
258
|
+
region: "us-east-1",
|
|
259
|
+
accessKeyId: "geekmidas",
|
|
260
|
+
secretAccessKey: "geekmidas",
|
|
261
|
+
endpoint: "http://localhost:9000",
|
|
262
|
+
forcePathStyle: true,
|
|
263
|
+
cache
|
|
264
|
+
});
|
|
265
|
+
const key = "test-files/cache-expiry.txt";
|
|
266
|
+
await clientWithCache.upload(key, testContent, testContentType);
|
|
267
|
+
await clientWithCache.getDownloadURL({ path: key }, 30);
|
|
268
|
+
const cacheKey = `download-url:${key}`;
|
|
269
|
+
const cachedShortUrl = await cache.get(cacheKey);
|
|
270
|
+
require_vi_bdSIJ99Y.globalExpect(cachedShortUrl).toBeUndefined();
|
|
271
|
+
const longExpiryUrl = await clientWithCache.getDownloadURL({ path: key }, 3600);
|
|
272
|
+
const cachedLongUrl = await cache.get(cacheKey);
|
|
273
|
+
require_vi_bdSIJ99Y.globalExpect(cachedLongUrl).toBe(longExpiryUrl);
|
|
274
|
+
});
|
|
275
|
+
require_vi_bdSIJ99Y.it("should use cached URL when available", async () => {
|
|
276
|
+
const cache = new __geekmidas_cache_memory.InMemoryCache();
|
|
277
|
+
const clientWithCache = require_AmazonStorageClient.AmazonStorageClient.create({
|
|
278
|
+
bucket: testBucket,
|
|
279
|
+
region: "us-east-1",
|
|
280
|
+
accessKeyId: "geekmidas",
|
|
281
|
+
secretAccessKey: "geekmidas",
|
|
282
|
+
endpoint: "http://localhost:9000",
|
|
283
|
+
forcePathStyle: true,
|
|
284
|
+
cache
|
|
285
|
+
});
|
|
286
|
+
const key = "test-files/pre-cached.txt";
|
|
287
|
+
const cachedUrl = "https://pre-cached-url.example.com";
|
|
288
|
+
const cacheKey = `download-url:${key}`;
|
|
289
|
+
await cache.set(cacheKey, cachedUrl);
|
|
290
|
+
const url = await clientWithCache.getDownloadURL({ path: key });
|
|
291
|
+
require_vi_bdSIJ99Y.globalExpect(url).toBe(cachedUrl);
|
|
292
|
+
});
|
|
293
|
+
require_vi_bdSIJ99Y.it("should work without cache", async () => {
|
|
294
|
+
const clientNoCache = require_AmazonStorageClient.AmazonStorageClient.create({
|
|
295
|
+
bucket: testBucket,
|
|
296
|
+
region: "us-east-1",
|
|
297
|
+
accessKeyId: "geekmidas",
|
|
298
|
+
secretAccessKey: "geekmidas",
|
|
299
|
+
endpoint: "http://localhost:9000",
|
|
300
|
+
forcePathStyle: true
|
|
301
|
+
});
|
|
302
|
+
const key = "test-files/no-cache.txt";
|
|
303
|
+
await clientNoCache.upload(key, testContent, testContentType);
|
|
304
|
+
const url1 = await clientNoCache.getDownloadURL({ path: key });
|
|
305
|
+
const url2 = await clientNoCache.getDownloadURL({ path: key });
|
|
306
|
+
require_vi_bdSIJ99Y.globalExpect(url1).toMatch(/^http:\/\/localhost:9000\/geekmidas\/test-files\/no-cache\.txt/);
|
|
307
|
+
require_vi_bdSIJ99Y.globalExpect(url2).toMatch(/^http:\/\/localhost:9000\/geekmidas\/test-files\/no-cache\.txt/);
|
|
308
|
+
});
|
|
309
|
+
require_vi_bdSIJ99Y.it("should pass cache to constructor", () => {
|
|
310
|
+
const cache = new __geekmidas_cache_memory.InMemoryCache();
|
|
311
|
+
const s3Client = new __aws_sdk_client_s3.S3Client({
|
|
312
|
+
region: "us-east-1",
|
|
313
|
+
credentials: {
|
|
314
|
+
accessKeyId: "geekmidas",
|
|
315
|
+
secretAccessKey: "geekmidas"
|
|
316
|
+
},
|
|
317
|
+
endpoint: "http://localhost:9000",
|
|
318
|
+
forcePathStyle: true
|
|
319
|
+
});
|
|
320
|
+
const clientWithCache = new require_AmazonStorageClient.AmazonStorageClient(s3Client, testBucket, require_AmazonStorageClient.AmazonCannedAccessControlList.PublicRead, cache);
|
|
321
|
+
require_vi_bdSIJ99Y.globalExpect(clientWithCache.cache).toBe(cache);
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
require_vi_bdSIJ99Y.describe("getVersionDownloadURL", () => {
|
|
325
|
+
require_vi_bdSIJ99Y.it("should generate version-specific download URL", async () => {
|
|
326
|
+
const versionKey = "test-files/version-download.txt";
|
|
327
|
+
const versionId = "test-version-id";
|
|
328
|
+
await client.upload(versionKey, testContent, testContentType);
|
|
329
|
+
const downloadUrl = await client.getVersionDownloadURL({
|
|
330
|
+
path: versionKey,
|
|
331
|
+
name: "versioned-file.txt"
|
|
332
|
+
}, versionId);
|
|
333
|
+
require_vi_bdSIJ99Y.globalExpect(downloadUrl).toMatch(/^http:\/\/localhost:9000\/geekmidas\/test-files\/version-download\.txt/);
|
|
334
|
+
require_vi_bdSIJ99Y.globalExpect(downloadUrl).toContain("X-Amz-Algorithm=AWS4-HMAC-SHA256");
|
|
335
|
+
require_vi_bdSIJ99Y.globalExpect(downloadUrl).toContain("response-content-disposition=attachment%3B%20filename%3Dversioned-file.txt");
|
|
336
|
+
});
|
|
337
|
+
require_vi_bdSIJ99Y.it("should generate version URL without filename", async () => {
|
|
338
|
+
const versionKey = "test-files/version-no-name.txt";
|
|
339
|
+
const versionId = "test-version-id";
|
|
340
|
+
await client.upload(versionKey, testContent, testContentType);
|
|
341
|
+
const downloadUrl = await client.getVersionDownloadURL({ path: versionKey }, versionId);
|
|
342
|
+
require_vi_bdSIJ99Y.globalExpect(downloadUrl).toMatch(/^http:\/\/localhost:9000\/geekmidas\/test-files\/version-no-name\.txt/);
|
|
343
|
+
require_vi_bdSIJ99Y.globalExpect(downloadUrl).not.toContain("response-content-disposition");
|
|
344
|
+
});
|
|
345
|
+
});
|
|
232
346
|
});
|
|
233
347
|
|
|
234
348
|
//#endregion
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { beforeAll, describe, globalExpect, it } from "../vi.bdSIJ99Y-CucmVasy.mjs";
|
|
2
2
|
import { StorageProvider } from "../StorageClient-CZGIC_fz.mjs";
|
|
3
|
-
import { AmazonCannedAccessControlList, AmazonStorageClient } from "../AmazonStorageClient-
|
|
3
|
+
import { AmazonCannedAccessControlList, AmazonStorageClient } from "../AmazonStorageClient-Bkw-x2o2.mjs";
|
|
4
4
|
import { S3Client } from "@aws-sdk/client-s3";
|
|
5
|
+
import { InMemoryCache } from "@geekmidas/cache/memory";
|
|
5
6
|
|
|
6
7
|
//#region src/__tests__/AmazonStorageClient.spec.ts
|
|
7
8
|
describe("AmazonStorageClient Integration Tests", () => {
|
|
@@ -228,6 +229,119 @@ describe("AmazonStorageClient Integration Tests", () => {
|
|
|
228
229
|
globalExpect(storageClient.provider).toBe(StorageProvider.AWSS3);
|
|
229
230
|
});
|
|
230
231
|
});
|
|
232
|
+
describe("cache functionality", () => {
|
|
233
|
+
it("should cache download URLs", async () => {
|
|
234
|
+
const cache = new InMemoryCache();
|
|
235
|
+
const clientWithCache = AmazonStorageClient.create({
|
|
236
|
+
bucket: testBucket,
|
|
237
|
+
region: "us-east-1",
|
|
238
|
+
accessKeyId: "geekmidas",
|
|
239
|
+
secretAccessKey: "geekmidas",
|
|
240
|
+
endpoint: "http://localhost:9000",
|
|
241
|
+
forcePathStyle: true,
|
|
242
|
+
cache
|
|
243
|
+
});
|
|
244
|
+
const key = "test-files/cache-test.txt";
|
|
245
|
+
await clientWithCache.upload(key, testContent, testContentType);
|
|
246
|
+
const url1 = await clientWithCache.getDownloadURL({ path: key });
|
|
247
|
+
const url2 = await clientWithCache.getDownloadURL({ path: key });
|
|
248
|
+
globalExpect(url1).toBe(url2);
|
|
249
|
+
const cacheKey = `download-url:${key}`;
|
|
250
|
+
const cachedUrl = await cache.get(cacheKey);
|
|
251
|
+
globalExpect(cachedUrl).toBe(url1);
|
|
252
|
+
});
|
|
253
|
+
it("should respect cache expiration based on URL expiry", async () => {
|
|
254
|
+
const cache = new InMemoryCache();
|
|
255
|
+
const clientWithCache = AmazonStorageClient.create({
|
|
256
|
+
bucket: testBucket,
|
|
257
|
+
region: "us-east-1",
|
|
258
|
+
accessKeyId: "geekmidas",
|
|
259
|
+
secretAccessKey: "geekmidas",
|
|
260
|
+
endpoint: "http://localhost:9000",
|
|
261
|
+
forcePathStyle: true,
|
|
262
|
+
cache
|
|
263
|
+
});
|
|
264
|
+
const key = "test-files/cache-expiry.txt";
|
|
265
|
+
await clientWithCache.upload(key, testContent, testContentType);
|
|
266
|
+
await clientWithCache.getDownloadURL({ path: key }, 30);
|
|
267
|
+
const cacheKey = `download-url:${key}`;
|
|
268
|
+
const cachedShortUrl = await cache.get(cacheKey);
|
|
269
|
+
globalExpect(cachedShortUrl).toBeUndefined();
|
|
270
|
+
const longExpiryUrl = await clientWithCache.getDownloadURL({ path: key }, 3600);
|
|
271
|
+
const cachedLongUrl = await cache.get(cacheKey);
|
|
272
|
+
globalExpect(cachedLongUrl).toBe(longExpiryUrl);
|
|
273
|
+
});
|
|
274
|
+
it("should use cached URL when available", async () => {
|
|
275
|
+
const cache = new InMemoryCache();
|
|
276
|
+
const clientWithCache = AmazonStorageClient.create({
|
|
277
|
+
bucket: testBucket,
|
|
278
|
+
region: "us-east-1",
|
|
279
|
+
accessKeyId: "geekmidas",
|
|
280
|
+
secretAccessKey: "geekmidas",
|
|
281
|
+
endpoint: "http://localhost:9000",
|
|
282
|
+
forcePathStyle: true,
|
|
283
|
+
cache
|
|
284
|
+
});
|
|
285
|
+
const key = "test-files/pre-cached.txt";
|
|
286
|
+
const cachedUrl = "https://pre-cached-url.example.com";
|
|
287
|
+
const cacheKey = `download-url:${key}`;
|
|
288
|
+
await cache.set(cacheKey, cachedUrl);
|
|
289
|
+
const url = await clientWithCache.getDownloadURL({ path: key });
|
|
290
|
+
globalExpect(url).toBe(cachedUrl);
|
|
291
|
+
});
|
|
292
|
+
it("should work without cache", async () => {
|
|
293
|
+
const clientNoCache = AmazonStorageClient.create({
|
|
294
|
+
bucket: testBucket,
|
|
295
|
+
region: "us-east-1",
|
|
296
|
+
accessKeyId: "geekmidas",
|
|
297
|
+
secretAccessKey: "geekmidas",
|
|
298
|
+
endpoint: "http://localhost:9000",
|
|
299
|
+
forcePathStyle: true
|
|
300
|
+
});
|
|
301
|
+
const key = "test-files/no-cache.txt";
|
|
302
|
+
await clientNoCache.upload(key, testContent, testContentType);
|
|
303
|
+
const url1 = await clientNoCache.getDownloadURL({ path: key });
|
|
304
|
+
const url2 = await clientNoCache.getDownloadURL({ path: key });
|
|
305
|
+
globalExpect(url1).toMatch(/^http:\/\/localhost:9000\/geekmidas\/test-files\/no-cache\.txt/);
|
|
306
|
+
globalExpect(url2).toMatch(/^http:\/\/localhost:9000\/geekmidas\/test-files\/no-cache\.txt/);
|
|
307
|
+
});
|
|
308
|
+
it("should pass cache to constructor", () => {
|
|
309
|
+
const cache = new InMemoryCache();
|
|
310
|
+
const s3Client = new S3Client({
|
|
311
|
+
region: "us-east-1",
|
|
312
|
+
credentials: {
|
|
313
|
+
accessKeyId: "geekmidas",
|
|
314
|
+
secretAccessKey: "geekmidas"
|
|
315
|
+
},
|
|
316
|
+
endpoint: "http://localhost:9000",
|
|
317
|
+
forcePathStyle: true
|
|
318
|
+
});
|
|
319
|
+
const clientWithCache = new AmazonStorageClient(s3Client, testBucket, AmazonCannedAccessControlList.PublicRead, cache);
|
|
320
|
+
globalExpect(clientWithCache.cache).toBe(cache);
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
describe("getVersionDownloadURL", () => {
|
|
324
|
+
it("should generate version-specific download URL", async () => {
|
|
325
|
+
const versionKey = "test-files/version-download.txt";
|
|
326
|
+
const versionId = "test-version-id";
|
|
327
|
+
await client.upload(versionKey, testContent, testContentType);
|
|
328
|
+
const downloadUrl = await client.getVersionDownloadURL({
|
|
329
|
+
path: versionKey,
|
|
330
|
+
name: "versioned-file.txt"
|
|
331
|
+
}, versionId);
|
|
332
|
+
globalExpect(downloadUrl).toMatch(/^http:\/\/localhost:9000\/geekmidas\/test-files\/version-download\.txt/);
|
|
333
|
+
globalExpect(downloadUrl).toContain("X-Amz-Algorithm=AWS4-HMAC-SHA256");
|
|
334
|
+
globalExpect(downloadUrl).toContain("response-content-disposition=attachment%3B%20filename%3Dversioned-file.txt");
|
|
335
|
+
});
|
|
336
|
+
it("should generate version URL without filename", async () => {
|
|
337
|
+
const versionKey = "test-files/version-no-name.txt";
|
|
338
|
+
const versionId = "test-version-id";
|
|
339
|
+
await client.upload(versionKey, testContent, testContentType);
|
|
340
|
+
const downloadUrl = await client.getVersionDownloadURL({ path: versionKey }, versionId);
|
|
341
|
+
globalExpect(downloadUrl).toMatch(/^http:\/\/localhost:9000\/geekmidas\/test-files\/version-no-name\.txt/);
|
|
342
|
+
globalExpect(downloadUrl).not.toContain("response-content-disposition");
|
|
343
|
+
});
|
|
344
|
+
});
|
|
231
345
|
});
|
|
232
346
|
|
|
233
347
|
//#endregion
|
package/dist/aws.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
require('./StorageClient-BDRVrMJj.cjs');
|
|
2
|
-
const require_AmazonStorageClient = require('./AmazonStorageClient-
|
|
2
|
+
const require_AmazonStorageClient = require('./AmazonStorageClient-tyGekctm.cjs');
|
|
3
3
|
|
|
4
4
|
exports.AmazonCannedAccessControlList = require_AmazonStorageClient.AmazonCannedAccessControlList;
|
|
5
5
|
exports.AmazonStorageClient = require_AmazonStorageClient.AmazonStorageClient;
|
package/dist/aws.d.cts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { DocumentVersion, File, GetUploadParams, GetUploadResponse, StorageClient } from "./StorageClient-
|
|
2
|
-
import { AmazonCannedAccessControlList, AmazonStorageClient } from "./AmazonStorageClient-
|
|
1
|
+
import { DocumentVersion, File, GetUploadParams, GetUploadResponse, StorageClient } from "./StorageClient-CcOAW3YV.cjs";
|
|
2
|
+
import { AmazonCannedAccessControlList, AmazonStorageClient } from "./AmazonStorageClient-aduVU3_p.cjs";
|
|
3
3
|
export { AmazonCannedAccessControlList, AmazonStorageClient, DocumentVersion, File, GetUploadParams, GetUploadResponse, StorageClient };
|
package/dist/aws.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { DocumentVersion, File, GetUploadParams, GetUploadResponse, StorageClient } from "./StorageClient-
|
|
2
|
-
import { AmazonCannedAccessControlList, AmazonStorageClient } from "./AmazonStorageClient-
|
|
1
|
+
import { DocumentVersion, File, GetUploadParams, GetUploadResponse, StorageClient } from "./StorageClient-CveQoxJa.mjs";
|
|
2
|
+
import { AmazonCannedAccessControlList, AmazonStorageClient } from "./AmazonStorageClient-DaktozmE.mjs";
|
|
3
3
|
export { AmazonCannedAccessControlList, AmazonStorageClient, DocumentVersion, File, GetUploadParams, GetUploadResponse, StorageClient };
|
package/dist/aws.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import "./StorageClient-CZGIC_fz.mjs";
|
|
2
|
-
import { AmazonCannedAccessControlList, AmazonStorageClient } from "./AmazonStorageClient-
|
|
2
|
+
import { AmazonCannedAccessControlList, AmazonStorageClient } from "./AmazonStorageClient-Bkw-x2o2.mjs";
|
|
3
3
|
|
|
4
4
|
export { AmazonCannedAccessControlList, AmazonStorageClient };
|
package/dist/index.d.cts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { StorageClient } from "./StorageClient-
|
|
1
|
+
import { StorageClient } from "./StorageClient-CcOAW3YV.cjs";
|
|
2
2
|
export { StorageClient };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { StorageClient } from "./StorageClient-
|
|
1
|
+
import { StorageClient } from "./StorageClient-CveQoxJa.mjs";
|
|
2
2
|
export { StorageClient };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geekmidas/storage",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"registry": "https://registry.npmjs.org/",
|
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
"require": "./dist/index.cjs"
|
|
20
20
|
}
|
|
21
21
|
},
|
|
22
|
-
"dependencies": {
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@geekmidas/cache": "0.0.7"
|
|
24
|
+
},
|
|
23
25
|
"peerDependencies": {
|
|
24
26
|
"zod": "~3.25.67",
|
|
25
27
|
"@aws-sdk/client-s3": "~3.844.0",
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
import { createPresignedPost } from '@aws-sdk/s3-presigned-post';
|
|
8
8
|
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
9
9
|
|
|
10
|
+
import type { Cache } from '@geekmidas/cache';
|
|
10
11
|
import {
|
|
11
12
|
type DocumentVersion,
|
|
12
13
|
type File,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
|
|
19
20
|
export class AmazonStorageClient implements StorageClient {
|
|
20
21
|
readonly provider = StorageProvider.AWSS3;
|
|
22
|
+
|
|
21
23
|
static create(
|
|
22
24
|
options: AmazonStorageClientCreateOptions,
|
|
23
25
|
): AmazonStorageClient {
|
|
@@ -29,6 +31,7 @@ export class AmazonStorageClient implements StorageClient {
|
|
|
29
31
|
endpoint,
|
|
30
32
|
secretAccessKey,
|
|
31
33
|
forcePathStyle = false,
|
|
34
|
+
cache,
|
|
32
35
|
} = options;
|
|
33
36
|
const hasCredentials = accessKeyId && secretAccessKey;
|
|
34
37
|
const credentials = hasCredentials
|
|
@@ -42,12 +45,13 @@ export class AmazonStorageClient implements StorageClient {
|
|
|
42
45
|
forcePathStyle,
|
|
43
46
|
});
|
|
44
47
|
|
|
45
|
-
return new AmazonStorageClient(client, bucket, acl);
|
|
48
|
+
return new AmazonStorageClient(client, bucket, acl, cache);
|
|
46
49
|
}
|
|
47
50
|
constructor(
|
|
48
51
|
private readonly client: S3Client,
|
|
49
52
|
private readonly bucket: string,
|
|
50
53
|
private readonly acl = AmazonCannedAccessControlList.AuthenticatedRead,
|
|
54
|
+
readonly cache?: Cache<string>,
|
|
51
55
|
) {}
|
|
52
56
|
|
|
53
57
|
getVersionDownloadURL(file: File, versionId: string): Promise<string> {
|
|
@@ -79,7 +83,14 @@ export class AmazonStorageClient implements StorageClient {
|
|
|
79
83
|
}));
|
|
80
84
|
}
|
|
81
85
|
|
|
82
|
-
getDownloadURL(file: File, expiresIn = 60 * 60): Promise<string> {
|
|
86
|
+
async getDownloadURL(file: File, expiresIn = 60 * 60): Promise<string> {
|
|
87
|
+
const cacheKey = `download-url:${file.path}`;
|
|
88
|
+
const cachedURL = await this.cache?.get(cacheKey);
|
|
89
|
+
|
|
90
|
+
if (cachedURL) {
|
|
91
|
+
return cachedURL;
|
|
92
|
+
}
|
|
93
|
+
|
|
83
94
|
const ResponseContentDisposition = file.name
|
|
84
95
|
? `attachment; filename=${encodeURIComponent(file.name)}`
|
|
85
96
|
: undefined;
|
|
@@ -90,7 +101,14 @@ export class AmazonStorageClient implements StorageClient {
|
|
|
90
101
|
ResponseContentDisposition,
|
|
91
102
|
});
|
|
92
103
|
|
|
93
|
-
|
|
104
|
+
const url = await getSignedUrl(this.client, command, { expiresIn });
|
|
105
|
+
const cacheExpiration = Math.max(expiresIn - 60, 0);
|
|
106
|
+
|
|
107
|
+
if (cacheExpiration) {
|
|
108
|
+
await this.cache?.set(cacheKey, url, cacheExpiration);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return url;
|
|
94
112
|
}
|
|
95
113
|
|
|
96
114
|
async getUploadURL(
|
|
@@ -173,4 +191,5 @@ interface AmazonStorageClientCreateOptions {
|
|
|
173
191
|
secretAccessKey?: string;
|
|
174
192
|
endpoint?: string;
|
|
175
193
|
forcePathStyle?: boolean;
|
|
194
|
+
cache?: Cache<string>;
|
|
176
195
|
}
|
package/src/StorageClient.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Cache } from '@geekmidas/cache';
|
|
1
2
|
export interface DocumentVersion {
|
|
2
3
|
id: string;
|
|
3
4
|
createdAt: Date;
|
|
@@ -11,6 +12,7 @@ export enum StorageProvider {
|
|
|
11
12
|
|
|
12
13
|
export interface StorageClient {
|
|
13
14
|
readonly provider: StorageProvider;
|
|
15
|
+
readonly cache?: Cache<string>;
|
|
14
16
|
/**
|
|
15
17
|
* Get a URL to upload a file to.
|
|
16
18
|
*
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { S3Client } from '@aws-sdk/client-s3';
|
|
2
|
+
import { InMemoryCache } from '@geekmidas/cache/memory';
|
|
2
3
|
import { beforeAll, describe, expect, it } from 'vitest';
|
|
3
4
|
import {
|
|
4
5
|
AmazonCannedAccessControlList,
|
|
@@ -324,4 +325,186 @@ describe('AmazonStorageClient Integration Tests', () => {
|
|
|
324
325
|
expect(storageClient.provider).toBe(StorageProvider.AWSS3);
|
|
325
326
|
});
|
|
326
327
|
});
|
|
328
|
+
|
|
329
|
+
describe('cache functionality', () => {
|
|
330
|
+
it('should cache download URLs', async () => {
|
|
331
|
+
const cache = new InMemoryCache<string>();
|
|
332
|
+
const clientWithCache = AmazonStorageClient.create({
|
|
333
|
+
bucket: testBucket,
|
|
334
|
+
region: 'us-east-1',
|
|
335
|
+
accessKeyId: 'geekmidas',
|
|
336
|
+
secretAccessKey: 'geekmidas',
|
|
337
|
+
endpoint: 'http://localhost:9000',
|
|
338
|
+
forcePathStyle: true,
|
|
339
|
+
cache,
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
const key = 'test-files/cache-test.txt';
|
|
343
|
+
await clientWithCache.upload(key, testContent, testContentType);
|
|
344
|
+
|
|
345
|
+
// First call should generate and cache the URL
|
|
346
|
+
const url1 = await clientWithCache.getDownloadURL({ path: key });
|
|
347
|
+
|
|
348
|
+
// Second call should return the cached URL
|
|
349
|
+
const url2 = await clientWithCache.getDownloadURL({ path: key });
|
|
350
|
+
|
|
351
|
+
// URLs should be identical (same cached value)
|
|
352
|
+
expect(url1).toBe(url2);
|
|
353
|
+
|
|
354
|
+
// Verify the URL was cached
|
|
355
|
+
const cacheKey = `download-url:${key}`;
|
|
356
|
+
const cachedUrl = await cache.get(cacheKey);
|
|
357
|
+
expect(cachedUrl).toBe(url1);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('should respect cache expiration based on URL expiry', async () => {
|
|
361
|
+
const cache = new InMemoryCache<string>();
|
|
362
|
+
const clientWithCache = AmazonStorageClient.create({
|
|
363
|
+
bucket: testBucket,
|
|
364
|
+
region: 'us-east-1',
|
|
365
|
+
accessKeyId: 'geekmidas',
|
|
366
|
+
secretAccessKey: 'geekmidas',
|
|
367
|
+
endpoint: 'http://localhost:9000',
|
|
368
|
+
forcePathStyle: true,
|
|
369
|
+
cache,
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
const key = 'test-files/cache-expiry.txt';
|
|
373
|
+
await clientWithCache.upload(key, testContent, testContentType);
|
|
374
|
+
|
|
375
|
+
// Generate URL with short expiry that won't be cached
|
|
376
|
+
await clientWithCache.getDownloadURL({ path: key }, 30); // 30 seconds
|
|
377
|
+
|
|
378
|
+
// Check cache doesn't contain the URL (too short to cache)
|
|
379
|
+
const cacheKey = `download-url:${key}`;
|
|
380
|
+
const cachedShortUrl = await cache.get(cacheKey);
|
|
381
|
+
expect(cachedShortUrl).toBeUndefined();
|
|
382
|
+
|
|
383
|
+
// Generate URL with longer expiry that will be cached
|
|
384
|
+
const longExpiryUrl = await clientWithCache.getDownloadURL(
|
|
385
|
+
{ path: key },
|
|
386
|
+
3600,
|
|
387
|
+
); // 1 hour
|
|
388
|
+
|
|
389
|
+
// Check cache contains the URL
|
|
390
|
+
const cachedLongUrl = await cache.get(cacheKey);
|
|
391
|
+
expect(cachedLongUrl).toBe(longExpiryUrl);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('should use cached URL when available', async () => {
|
|
395
|
+
const cache = new InMemoryCache<string>();
|
|
396
|
+
const clientWithCache = AmazonStorageClient.create({
|
|
397
|
+
bucket: testBucket,
|
|
398
|
+
region: 'us-east-1',
|
|
399
|
+
accessKeyId: 'geekmidas',
|
|
400
|
+
secretAccessKey: 'geekmidas',
|
|
401
|
+
endpoint: 'http://localhost:9000',
|
|
402
|
+
forcePathStyle: true,
|
|
403
|
+
cache,
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
const key = 'test-files/pre-cached.txt';
|
|
407
|
+
const cachedUrl = 'https://pre-cached-url.example.com';
|
|
408
|
+
const cacheKey = `download-url:${key}`;
|
|
409
|
+
|
|
410
|
+
// Pre-populate cache
|
|
411
|
+
await cache.set(cacheKey, cachedUrl);
|
|
412
|
+
|
|
413
|
+
// Get download URL should return cached value
|
|
414
|
+
const url = await clientWithCache.getDownloadURL({ path: key });
|
|
415
|
+
expect(url).toBe(cachedUrl);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it('should work without cache', async () => {
|
|
419
|
+
// Client without cache should work normally
|
|
420
|
+
const clientNoCache = AmazonStorageClient.create({
|
|
421
|
+
bucket: testBucket,
|
|
422
|
+
region: 'us-east-1',
|
|
423
|
+
accessKeyId: 'geekmidas',
|
|
424
|
+
secretAccessKey: 'geekmidas',
|
|
425
|
+
endpoint: 'http://localhost:9000',
|
|
426
|
+
forcePathStyle: true,
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
const key = 'test-files/no-cache.txt';
|
|
430
|
+
await clientNoCache.upload(key, testContent, testContentType);
|
|
431
|
+
|
|
432
|
+
const url1 = await clientNoCache.getDownloadURL({ path: key });
|
|
433
|
+
const url2 = await clientNoCache.getDownloadURL({ path: key });
|
|
434
|
+
|
|
435
|
+
// Without cache, new URLs might be generated each time
|
|
436
|
+
// Both should be valid URLs
|
|
437
|
+
expect(url1).toMatch(
|
|
438
|
+
/^http:\/\/localhost:9000\/geekmidas\/test-files\/no-cache\.txt/,
|
|
439
|
+
);
|
|
440
|
+
expect(url2).toMatch(
|
|
441
|
+
/^http:\/\/localhost:9000\/geekmidas\/test-files\/no-cache\.txt/,
|
|
442
|
+
);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it('should pass cache to constructor', () => {
|
|
446
|
+
const cache = new InMemoryCache<string>();
|
|
447
|
+
const s3Client = new S3Client({
|
|
448
|
+
region: 'us-east-1',
|
|
449
|
+
credentials: {
|
|
450
|
+
accessKeyId: 'geekmidas',
|
|
451
|
+
secretAccessKey: 'geekmidas',
|
|
452
|
+
},
|
|
453
|
+
endpoint: 'http://localhost:9000',
|
|
454
|
+
forcePathStyle: true,
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
const clientWithCache = new AmazonStorageClient(
|
|
458
|
+
s3Client,
|
|
459
|
+
testBucket,
|
|
460
|
+
AmazonCannedAccessControlList.PublicRead,
|
|
461
|
+
cache,
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
expect(clientWithCache.cache).toBe(cache);
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
describe('getVersionDownloadURL', () => {
|
|
469
|
+
it('should generate version-specific download URL', async () => {
|
|
470
|
+
const versionKey = 'test-files/version-download.txt';
|
|
471
|
+
const versionId = 'test-version-id';
|
|
472
|
+
|
|
473
|
+
// Upload a file first
|
|
474
|
+
await client.upload(versionKey, testContent, testContentType);
|
|
475
|
+
|
|
476
|
+
// Generate version-specific download URL
|
|
477
|
+
const downloadUrl = await client.getVersionDownloadURL(
|
|
478
|
+
{ path: versionKey, name: 'versioned-file.txt' },
|
|
479
|
+
versionId,
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
expect(downloadUrl).toMatch(
|
|
483
|
+
/^http:\/\/localhost:9000\/geekmidas\/test-files\/version-download\.txt/,
|
|
484
|
+
);
|
|
485
|
+
expect(downloadUrl).toContain('X-Amz-Algorithm=AWS4-HMAC-SHA256');
|
|
486
|
+
expect(downloadUrl).toContain(
|
|
487
|
+
'response-content-disposition=attachment%3B%20filename%3Dversioned-file.txt',
|
|
488
|
+
);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it('should generate version URL without filename', async () => {
|
|
492
|
+
const versionKey = 'test-files/version-no-name.txt';
|
|
493
|
+
const versionId = 'test-version-id';
|
|
494
|
+
|
|
495
|
+
// Upload a file first
|
|
496
|
+
await client.upload(versionKey, testContent, testContentType);
|
|
497
|
+
|
|
498
|
+
// Generate version-specific download URL without filename
|
|
499
|
+
const downloadUrl = await client.getVersionDownloadURL(
|
|
500
|
+
{ path: versionKey },
|
|
501
|
+
versionId,
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
expect(downloadUrl).toMatch(
|
|
505
|
+
/^http:\/\/localhost:9000\/geekmidas\/test-files\/version-no-name\.txt/,
|
|
506
|
+
);
|
|
507
|
+
expect(downloadUrl).not.toContain('response-content-disposition');
|
|
508
|
+
});
|
|
509
|
+
});
|
|
327
510
|
});
|