@allegria/aws-file-manager 0.0.1 → 1.0.2

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.
Files changed (48) hide show
  1. package/README.md +157 -18
  2. package/dist/cjs/aws-file-manager.d.ts +318 -0
  3. package/dist/cjs/aws-file-manager.js +422 -0
  4. package/dist/cjs/aws-file-manager.js.map +1 -0
  5. package/dist/cjs/package.json +3 -0
  6. package/dist/examples/01-setup.d.ts +14 -0
  7. package/dist/examples/01-setup.js +42 -0
  8. package/dist/examples/01-setup.js.map +1 -0
  9. package/dist/examples/02-upload-multer.d.ts +13 -0
  10. package/dist/examples/02-upload-multer.js +63 -0
  11. package/dist/examples/02-upload-multer.js.map +1 -0
  12. package/dist/examples/03-upload-web.d.ts +12 -0
  13. package/dist/examples/03-upload-web.js +38 -0
  14. package/dist/examples/03-upload-web.js.map +1 -0
  15. package/dist/examples/04-signed-urls.d.ts +12 -0
  16. package/dist/examples/04-signed-urls.js +48 -0
  17. package/dist/examples/04-signed-urls.js.map +1 -0
  18. package/dist/examples/05-download.d.ts +14 -0
  19. package/dist/examples/05-download.js +53 -0
  20. package/dist/examples/05-download.js.map +1 -0
  21. package/dist/examples/06-delete.d.ts +17 -0
  22. package/dist/examples/06-delete.js +44 -0
  23. package/dist/examples/06-delete.js.map +1 -0
  24. package/dist/examples/07-copy.d.ts +14 -0
  25. package/dist/examples/07-copy.js +35 -0
  26. package/dist/examples/07-copy.js.map +1 -0
  27. package/dist/examples/08-list-paginated.d.ts +16 -0
  28. package/dist/examples/08-list-paginated.js +60 -0
  29. package/dist/examples/08-list-paginated.js.map +1 -0
  30. package/dist/examples/09-exists.d.ts +13 -0
  31. package/dist/examples/09-exists.js +32 -0
  32. package/dist/examples/09-exists.js.map +1 -0
  33. package/dist/lib/aws-file-manager.d.ts +262 -102
  34. package/dist/lib/aws-file-manager.js +353 -118
  35. package/dist/lib/aws-file-manager.js.map +1 -1
  36. package/dist/tests/aws-file-manager.test.d.ts +1 -0
  37. package/dist/tests/aws-file-manager.test.js +359 -0
  38. package/dist/tests/aws-file-manager.test.js.map +1 -0
  39. package/examples/01-setup.ts +49 -0
  40. package/examples/02-upload-multer.ts +82 -0
  41. package/examples/03-upload-web.ts +46 -0
  42. package/examples/04-signed-urls.ts +55 -0
  43. package/examples/05-download.ts +64 -0
  44. package/examples/06-delete.ts +53 -0
  45. package/examples/07-copy.ts +47 -0
  46. package/examples/08-list-paginated.ts +71 -0
  47. package/examples/09-exists.ts +39 -0
  48. package/package.json +36 -9
package/README.md CHANGED
@@ -1,11 +1,6 @@
1
1
  # AWS File Manager
2
2
 
3
- A TypeScript library for managing files in AWS S3.
4
-
5
- ## Features
6
-
7
- - Upload and download files from S3
8
- - Fully typed with TypeScript
3
+ A TypeScript library for managing files in AWS S3. Handles uploads, signed URL generation, downloads, deletes, copies, and bucket listing — with full TypeScript types and a clean API designed for server-side Node.js applications.
9
4
 
10
5
  ## Installation
11
6
 
@@ -13,21 +8,165 @@ A TypeScript library for managing files in AWS S3.
13
8
  npm install @allegria/aws-file-manager
14
9
  ```
15
10
 
16
- ## Examples
11
+ ## Why this library
12
+
13
+ - **No ACL footgun.** ACL headers are omitted by default, which is correct for all buckets created after April 2023 (BucketOwnerEnforced). Passing an ACL to such a bucket throws a hard AWS error.
14
+ - **UUID filenames.** `generateUniqueFileName: true` replaces the original filename with a UUID while preserving the extension. Timestamps collide under concurrent uploads; UUIDs don't.
15
+ - **Keys and signed URLs are separate.** `upload()` returns only the S3 key (persist this to your database). `getSignedUrl()` is a separate call you make at request time — signed URLs expire and must never be stored.
17
16
 
18
- #### Basic usage
17
+ ## Quick start
18
+
19
+ ```ts
20
+ import { AwsFileManager, fromMulterFile } from "@allegria/aws-file-manager";
19
21
 
20
- ```js
21
22
  const fileManager = new AwsFileManager({
22
- region: AWS_REGION,
23
- bucketName: AWS_BUCKET_NAME,
24
- accessKeyId: AWS_ACCESS_KEY,
25
- secretAccessKey: AWS_SECRET_ACCESS_KEY,
23
+ region: "us-east-1",
24
+ bucketName: "my-app-uploads",
25
+ basePath: "uploads", // optional: namespaces all keys under this prefix
26
26
  });
27
27
 
28
- // To upload
29
- const uploadResult = await fileManager.upload('my-photo.jpg');
28
+ // In an Express/multer route:
29
+ const fileInput = fromMulterFile(req.file);
30
+
31
+ const result = await fileManager.upload(fileInput, {
32
+ folder: "avatars",
33
+ generateUniqueFileName: true,
34
+ });
35
+
36
+ // Persist result.key to your database — not the URL
37
+ // await db.files.create({ s3Key: result.key });
38
+
39
+ // Generate a signed URL on demand (e.g. when serving the file to a client)
40
+ const url = await fileManager.getSignedUrl(result.key, {
41
+ disposition: "inline",
42
+ });
43
+ ```
44
+
45
+ ## Core concepts
46
+
47
+ ### FileInput and adapters
48
+
49
+ `upload()` takes a `FileInput` — a normalised object with `buffer`, `originalName`, `mimeType`, and `size`. Use the provided adapters to construct one:
50
+
51
+ ```ts
52
+ // Express + multer (server-side)
53
+ import { fromMulterFile } from "@allegria/aws-file-manager";
54
+ const fileInput = fromMulterFile(req.file);
55
+
56
+ // Next.js App Router / Web API File (browser or edge)
57
+ import { fromWebFile } from "@allegria/aws-file-manager";
58
+ const fileInput = await fromWebFile(formData.get("file") as File);
59
+
60
+ // Or build it directly
61
+ const fileInput: FileInput = {
62
+ buffer: myBuffer,
63
+ originalName: "photo.jpg",
64
+ mimeType: "image/jpeg",
65
+ size: myBuffer.length,
66
+ };
67
+ ```
68
+
69
+ ### Keys vs signed URLs
70
+
71
+ | What | Where to store | Lifetime |
72
+ |---|---|---|
73
+ | S3 key (`result.key`) | Your database | Permanent |
74
+ | Signed URL | Never store | Minutes to hours |
75
+
76
+ The S3 key is the stable identifier for a file. Generate a signed URL at request time when you need to give a client access to a private file.
77
+
78
+ ### basePath namespacing
79
+
80
+ Set `basePath` in the constructor to prefix every key with a sub-folder:
81
+
82
+ ```ts
83
+ const fm = new AwsFileManager({ ..., basePath: "uploads" });
84
+ // upload to folder 'avatars' → key: 'uploads/avatars/<filename>'
85
+ ```
86
+
87
+ ## API reference
88
+
89
+ ### Constructor
90
+
91
+ ```ts
92
+ new AwsFileManager(config: AwsFileManagerConfig)
93
+ ```
94
+
95
+ See [Configuration reference](#configuration-reference) below.
96
+
97
+ ### Methods
98
+
99
+ | Method | Signature | Returns | Notes |
100
+ |---|---|---|---|
101
+ | `upload` | `(file: FileInput, options?: UploadOptions)` | `Promise<UploadResult>` | Stores the file; returns key + metadata |
102
+ | `getSignedUrl` | `(key: string, options?: SignedUrlOptions)` | `Promise<string>` | Short-lived presigned URL for private objects |
103
+ | `download` | `(key: string, mode?: 'buffer' \| 'stream')` | `Promise<DownloadResult \| null>` | Returns `null` when key not found |
104
+ | `delete` | `(key: string)` | `Promise<void>` | Idempotent — missing key does not throw |
105
+ | `deleteMany` | `(keys: string[])` | `Promise<void>` | Chunks at 1 000 keys per S3 request |
106
+ | `copy` | `(sourceKey, destKey, options?)` | `Promise<void>` | Server-side copy within the bucket |
107
+ | `list` | `(options?: ListOptions)` | `Promise<ListResult>` | Paginated via `continuationToken` |
108
+ | `exists` | `(key: string)` | `Promise<boolean>` | Lightweight key existence check |
109
+ | `getS3Client` | `()` | `S3Client` | Access the underlying client for advanced use |
110
+
111
+ ### Adapter functions
112
+
113
+ | Function | Signature | Notes |
114
+ |---|---|---|
115
+ | `fromMulterFile` | `(multerFile) => FileInput` | Sync — for Express + multer |
116
+ | `fromWebFile` | `(webFile: File) => Promise<FileInput>` | Async — for Web API File / Next.js App Router |
117
+
118
+ ## Examples
119
+
120
+ The `examples/` directory contains runnable TypeScript snippets for every method:
121
+
122
+ | File | Description |
123
+ |---|---|
124
+ | [01-setup.ts](examples/01-setup.ts) | Instantiation: explicit credentials, env vars, IAM role |
125
+ | [02-upload-multer.ts](examples/02-upload-multer.ts) | Upload from Express + multer |
126
+ | [03-upload-web.ts](examples/03-upload-web.ts) | Upload from Next.js App Router (Web API File) |
127
+ | [04-signed-urls.ts](examples/04-signed-urls.ts) | Inline, attachment, and custom-TTL signed URLs |
128
+ | [05-download.ts](examples/05-download.ts) | Download as Buffer or stream; pipe to HTTP response |
129
+ | [06-delete.ts](examples/06-delete.ts) | Delete single file or all variants at once |
130
+ | [07-copy.ts](examples/07-copy.ts) | Copy / move objects (server-side, no re-upload) |
131
+ | [08-list-paginated.ts](examples/08-list-paginated.ts) | Paginated listing for reconciliation jobs |
132
+ | [09-exists.ts](examples/09-exists.ts) | Key existence check for integrity validation |
133
+
134
+ ## Configuration reference
135
+
136
+ ```ts
137
+ interface AwsFileManagerConfig {
138
+ region: string; // AWS region, e.g. 'us-east-1'
139
+ bucketName: string; // S3 bucket name
140
+ accessKeyId?: string; // Omit to use environment/IAM resolution
141
+ secretAccessKey?: string; // Omit to use environment/IAM resolution
142
+ basePath?: string; // Prefix for all keys, e.g. 'uploads'
143
+ urlExpirationSeconds?: number; // Signed URL TTL (default: 3600)
144
+ storageClass?: StorageClass; // Default storage class (default: INTELLIGENT_TIERING)
145
+ }
146
+ ```
147
+
148
+ **Credential resolution order** (when `accessKeyId`/`secretAccessKey` are omitted):
149
+
150
+ 1. `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables
151
+ 2. `~/.aws/credentials` file
152
+ 3. EC2/ECS/Lambda instance metadata (IAM role) — recommended for production
153
+
154
+ ## Notes
155
+
156
+ **ACL behaviour.** No ACL is sent with `PutObjectCommand`. This is correct for all S3 buckets created after April 2023, which use `BucketOwnerEnforced` by default. If your bucket predates that change and requires legacy ACLs, call `getS3Client()` and issue the command directly.
157
+
158
+ **Storage class.** The default storage class is `INTELLIGENT_TIERING`, which automatically moves objects between access tiers based on usage patterns. Override per-upload via `UploadOptions.storageClass`, or change the instance default via `AwsFileManagerConfig.storageClass`.
159
+
160
+ **`deleteMany` chunking.** The S3 batch delete API accepts at most 1 000 keys per request. `deleteMany` splits larger arrays into 1 000-key chunks and sends them in parallel automatically.
161
+
162
+ ## Development
163
+
164
+ ```bash
165
+ pnpm test # run tests once
166
+ pnpm test:watch # run tests in watch mode
167
+ pnpm build # compile TypeScript
168
+ ```
169
+
170
+ ## License
30
171
 
31
- // To download
32
- const downloadResult = await fileManager.download('my-photo.jpg', 'buffer');
33
- ```
172
+ MIT
@@ -0,0 +1,318 @@
1
+ import { S3Client, StorageClass } from "@aws-sdk/client-s3";
2
+ import type { Readable } from "stream";
3
+ /**
4
+ * Configuration options for AwsFileManager.
5
+ * Credentials are optional — when omitted, the SDK resolves them from the
6
+ * environment (IAM role, AWS_ACCESS_KEY_ID, ~/.aws/credentials, etc.).
7
+ * Prefer environment/IAM resolution in production.
8
+ */
9
+ export interface AwsFileManagerConfig {
10
+ /** AWS region, e.g. 'us-east-1' */
11
+ region: string;
12
+ /** S3 bucket name */
13
+ bucketName: string;
14
+ /** AWS access key ID — omit to rely on environment/IAM resolution */
15
+ accessKeyId?: string;
16
+ /** AWS secret access key — omit to rely on environment/IAM resolution */
17
+ secretAccessKey?: string;
18
+ /**
19
+ * Base path prefix prepended to every key (no leading/trailing slashes).
20
+ * Useful for namespacing all writes to a sub-folder, e.g. 'uploads'.
21
+ */
22
+ basePath?: string;
23
+ /** Signed URL TTL in seconds (default: 3600) */
24
+ urlExpirationSeconds?: number;
25
+ /** Default S3 storage class (default: INTELLIGENT_TIERING) */
26
+ storageClass?: StorageClass;
27
+ }
28
+ /**
29
+ * A normalised file input that works for both server-side (multer Buffer) and
30
+ * browser-side (Web API File) callers. Use `fromMullerFile()` or
31
+ * `fromWebFile()` helpers to construct one, or build it directly.
32
+ */
33
+ export interface FileInput {
34
+ /** Raw file bytes */
35
+ buffer: Buffer;
36
+ /** Original filename as provided by the uploader */
37
+ originalName: string;
38
+ /** MIME type, e.g. 'image/jpeg' */
39
+ mimeType: string;
40
+ /** File size in bytes */
41
+ size: number;
42
+ }
43
+ /** Options that control how a single file is stored in S3. */
44
+ export interface UploadOptions {
45
+ /**
46
+ * Full S3 key (path + filename) for this object, relative to basePath.
47
+ * When provided, `folder` and `generateUniqueFileName` are ignored.
48
+ * Prefer this in pipeline usage where the calling code owns key generation.
49
+ */
50
+ key?: string;
51
+ /**
52
+ * Folder path appended to basePath (no leading/trailing slashes).
53
+ * Used when `key` is not provided.
54
+ */
55
+ folder?: string;
56
+ /**
57
+ * Custom filename to use instead of originalName.
58
+ * Used when `key` is not provided.
59
+ */
60
+ fileName?: string;
61
+ /**
62
+ * When true and `key` is not provided, replaces the filename with a UUID
63
+ * while preserving the extension. Prevents collisions under concurrent
64
+ * uploads.
65
+ */
66
+ generateUniqueFileName?: boolean;
67
+ /** Override the detected MIME type */
68
+ contentType?: string;
69
+ /** Arbitrary string metadata stored alongside the S3 object */
70
+ metadata?: Record<string, string>;
71
+ /** Per-upload storage class override */
72
+ storageClass?: StorageClass;
73
+ }
74
+ /** Options for listing objects in a folder */
75
+ export interface ListOptions {
76
+ /** Folder path appended to basePath */
77
+ folder?: string;
78
+ /** Additional prefix filter within the folder */
79
+ prefix?: string;
80
+ /** Maximum number of keys to return (1–1000, default: 1000) */
81
+ maxResults?: number;
82
+ /** Continuation token from a previous list call for pagination */
83
+ continuationToken?: string;
84
+ }
85
+ /** Options for generating a signed URL for an existing key */
86
+ export interface SignedUrlOptions {
87
+ /** TTL override in seconds. Falls back to instance default. */
88
+ expiresIn?: number;
89
+ /**
90
+ * Sets Content-Disposition on the response.
91
+ * 'inline' — browser renders the file (images, PDFs).
92
+ * 'attachment' — browser downloads the file.
93
+ */
94
+ disposition?: "inline" | "attachment";
95
+ /** Original filename to suggest in Content-Disposition (for attachments) */
96
+ fileName?: string;
97
+ }
98
+ /** Metadata returned after a successful upload */
99
+ export interface UploadResult {
100
+ /** Full S3 key for this object — store this in your database */
101
+ key: string;
102
+ /** Original filename as provided by the uploader */
103
+ originalName: string;
104
+ /** Filename used in S3 (may differ from originalName if UUID was generated) */
105
+ storedName: string;
106
+ /** Stored file size in bytes */
107
+ size: number;
108
+ /** MIME type */
109
+ contentType: string;
110
+ /** S3 ETag (without surrounding quotes) */
111
+ etag?: string;
112
+ /** Storage class applied */
113
+ storageClass: string;
114
+ }
115
+ /** A single entry returned by list() */
116
+ export interface ListEntry {
117
+ /** Full S3 key */
118
+ key: string;
119
+ /** Filename portion of the key */
120
+ fileName: string;
121
+ /** Object size in bytes */
122
+ size: number;
123
+ /** Last modified timestamp */
124
+ lastModified: Date;
125
+ /** Storage class */
126
+ storageClass?: string;
127
+ /** ETag (without surrounding quotes) */
128
+ etag?: string;
129
+ }
130
+ /** Paginated result from list() */
131
+ export interface ListResult {
132
+ entries: ListEntry[];
133
+ /** Pass this to the next list() call to fetch the next page */
134
+ continuationToken?: string;
135
+ /** True when there are more results beyond this page */
136
+ hasMore: boolean;
137
+ }
138
+ export type DownloadMode = "buffer" | "stream";
139
+ export type DownloadResult<T extends DownloadMode> = T extends "buffer" ? {
140
+ buffer: Buffer;
141
+ metadata: ObjectMetadata;
142
+ } : {
143
+ stream: Readable;
144
+ metadata: ObjectMetadata;
145
+ };
146
+ export interface ObjectMetadata {
147
+ contentType?: string;
148
+ contentLength?: number;
149
+ lastModified?: Date;
150
+ metadata?: Record<string, string>;
151
+ storageClass?: string;
152
+ }
153
+ /**
154
+ * Adapts a multer file (Express.Multer.File) to FileInput.
155
+ *
156
+ * @example
157
+ * // In an Express route with multer:
158
+ * const fileInput = fromMulterFile(req.file);
159
+ */
160
+ export declare function fromMulterFile(multerFile: {
161
+ buffer: Buffer;
162
+ originalname: string;
163
+ mimetype: string;
164
+ size: number;
165
+ }): FileInput;
166
+ /**
167
+ * Adapts a Web API File (browser / Next.js App Router FormData) to FileInput.
168
+ *
169
+ * @example
170
+ * // In a Next.js App Router route handler:
171
+ * const formData = await request.formData();
172
+ * const fileInput = await fromWebFile(formData.get('file') as File);
173
+ */
174
+ export declare function fromWebFile(webFile: File): Promise<FileInput>;
175
+ export declare class AwsFileManager {
176
+ private readonly s3;
177
+ private readonly bucketName;
178
+ private readonly basePath;
179
+ private readonly urlExpirationSeconds;
180
+ private readonly storageClass;
181
+ static readonly DEFAULTS: {
182
+ readonly storageClass: "INTELLIGENT_TIERING";
183
+ readonly urlExpirationSeconds: 3600;
184
+ };
185
+ constructor(config: AwsFileManagerConfig);
186
+ /**
187
+ * Uploads a file to S3 and returns storage metadata (no URL).
188
+ *
189
+ * The returned `key` is what you persist to your database. Generate signed
190
+ * URLs on demand via `getSignedUrl()` — never store them, as they expire.
191
+ *
192
+ * @example
193
+ * // In a pipeline step:
194
+ * const result = await fileManager.upload(ctx.fileInput, { key: ctx.s3Key });
195
+ * ctx.uploadResult = result;
196
+ */
197
+ upload(file: FileInput, options?: UploadOptions): Promise<UploadResult>;
198
+ /**
199
+ * Generates a short-lived signed URL for a private S3 object.
200
+ *
201
+ * Call this at request time; never store the returned URL. Signed URLs
202
+ * are credentials — treat them accordingly.
203
+ *
204
+ * @example
205
+ * const url = await fileManager.getSignedUrl(file.s3Key, {
206
+ * disposition: 'inline',
207
+ * fileName: file.originalName,
208
+ * });
209
+ */
210
+ getSignedUrl(key: string, options?: SignedUrlOptions): Promise<string>;
211
+ /**
212
+ * Downloads an S3 object as a Buffer or a Node.js Readable stream.
213
+ *
214
+ * Use `'stream'` (default) when piping to a response or writing to disk.
215
+ * Use `'buffer'` when you need the full bytes in memory (e.g. for processing).
216
+ *
217
+ * Returns `null` when the object does not exist.
218
+ *
219
+ * @example
220
+ * const result = await fileManager.download(key, 'buffer');
221
+ * if (result) {
222
+ * const { buffer, metadata } = result;
223
+ * }
224
+ */
225
+ download<T extends DownloadMode = "stream">(key: string, mode?: T): Promise<DownloadResult<T> | null>;
226
+ /**
227
+ * Permanently deletes an object from S3.
228
+ *
229
+ * Note: this only removes the S3 object. Your database record should be
230
+ * soft-deleted first, and this called as a cleanup step after the DB
231
+ * transaction commits — so a crash mid-way leaves a recoverable orphan
232
+ * rather than a missing file with an intact DB row.
233
+ *
234
+ * @example
235
+ * await fileManager.delete(file.s3Key);
236
+ */
237
+ delete(key: string): Promise<void>;
238
+ /**
239
+ * Deletes multiple objects in a single call.
240
+ * Silently skips keys that do not exist.
241
+ *
242
+ * @example
243
+ * await fileManager.deleteMany([original.s3Key, thumbnail.s3Key]);
244
+ */
245
+ deleteMany(keys: string[]): Promise<void>;
246
+ /**
247
+ * Copies an object within the same bucket.
248
+ *
249
+ * Useful when duplicating files across entities (e.g. cloning a note)
250
+ * or reorganising keys without re-uploading bytes.
251
+ *
252
+ * @example
253
+ * await fileManager.copy(sourceFile.s3Key, newKey);
254
+ */
255
+ copy(sourceKey: string, destinationKey: string, options?: {
256
+ storageClass?: StorageClass;
257
+ metadata?: Record<string, string>;
258
+ }): Promise<void>;
259
+ /**
260
+ * Lists objects under a folder prefix.
261
+ *
262
+ * Results are paginated. Pass the returned `continuationToken` back
263
+ * into subsequent calls to walk through large result sets.
264
+ *
265
+ * Primarily intended for the server-side storage reconciliation job
266
+ * (nightly scan to reconcile DB byte counts against S3 actuals).
267
+ *
268
+ * @example
269
+ * let token: string | undefined;
270
+ * do {
271
+ * const result = await fileManager.list({ folder: 'acme-corp', continuationToken: token });
272
+ * processBatch(result.entries);
273
+ * token = result.continuationToken;
274
+ * } while (result.hasMore);
275
+ */
276
+ list(options?: ListOptions): Promise<ListResult>;
277
+ /**
278
+ * Returns true if an object exists at the given key.
279
+ *
280
+ * @example
281
+ * if (!(await fileManager.exists(key))) {
282
+ * throw new Error('Referenced file is missing from storage');
283
+ * }
284
+ */
285
+ exists(key: string): Promise<boolean>;
286
+ /**
287
+ * Returns the underlying S3Client for operations not covered by this class.
288
+ * Use sparingly — prefer adding methods here so the abstraction stays coherent.
289
+ */
290
+ getS3Client(): S3Client;
291
+ /**
292
+ * Resolves the final S3 key and stored filename from upload options.
293
+ *
294
+ * Priority order for the key:
295
+ * 1. options.key (explicit full key — pipeline usage)
296
+ * 2. basePath + folder + fileName (if provided)
297
+ * 3. basePath + folder + UUID-based name (if generateUniqueFileName)
298
+ * 4. basePath + folder + originalName (fallback)
299
+ */
300
+ private resolveKey;
301
+ /**
302
+ * Builds a full S3 key from basePath, optional folder, and filename.
303
+ */
304
+ private buildFullKey;
305
+ /**
306
+ * Builds a prefix string for list operations.
307
+ */
308
+ private buildPrefix;
309
+ /**
310
+ * Generates a UUID-based filename, preserving the original extension.
311
+ * UUIDs are collision-proof under any upload concurrency.
312
+ */
313
+ private makeUniqueFileName;
314
+ /**
315
+ * Builds a Content-Disposition header value from SignedUrlOptions.
316
+ */
317
+ private buildContentDisposition;
318
+ }