@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 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
- return getSignedUrl(this.client, command, { expiresIn });
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-D-y4QLDq.cjs";
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-CJbHRTVV.mjs";
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
- return (0, __aws_sdk_s3_request_presigner.getSignedUrl)(this.client, command, { expiresIn });
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-DT9DeKVz.cjs');
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-D-y4QLDq.cjs";
2
- import { AmazonCannedAccessControlList, AmazonStorageClient } from "./AmazonStorageClient-C16fG3G4.cjs";
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-CJbHRTVV.mjs";
2
- import { AmazonCannedAccessControlList, AmazonStorageClient } from "./AmazonStorageClient-CP8Tx5oE.mjs";
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-DJMWn8vO.mjs";
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
  *
@@ -1,2 +1,2 @@
1
- import { DocumentVersion, File, GetUploadParams, GetUploadResponse, StorageClient, StorageProvider, UploadField } from "./StorageClient-D-y4QLDq.cjs";
1
+ import { DocumentVersion, File, GetUploadParams, GetUploadResponse, StorageClient, StorageProvider, UploadField } from "./StorageClient-CcOAW3YV.cjs";
2
2
  export { DocumentVersion, File, GetUploadParams, GetUploadResponse, StorageClient, StorageProvider, UploadField };
@@ -1,2 +1,2 @@
1
- import { DocumentVersion, File, GetUploadParams, GetUploadResponse, StorageClient, StorageProvider, UploadField } from "./StorageClient-CJbHRTVV.mjs";
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-DT9DeKVz.cjs');
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-DJMWn8vO.mjs";
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-DT9DeKVz.cjs');
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-D-y4QLDq.cjs";
2
- import { AmazonCannedAccessControlList, AmazonStorageClient } from "./AmazonStorageClient-C16fG3G4.cjs";
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-CJbHRTVV.mjs";
2
- import { AmazonCannedAccessControlList, AmazonStorageClient } from "./AmazonStorageClient-CP8Tx5oE.mjs";
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-DJMWn8vO.mjs";
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-D-y4QLDq.cjs";
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-CJbHRTVV.mjs";
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.4",
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
- return getSignedUrl(this.client, command, { expiresIn });
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
  }
@@ -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
  });