@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
@@ -1,158 +1,318 @@
1
1
  import { S3Client, StorageClass } from "@aws-sdk/client-s3";
2
2
  import type { Readable } from "stream";
3
3
  /**
4
- * Configuration options for the AWS File Manager
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.
5
8
  */
6
9
  export interface AwsFileManagerConfig {
7
- /** AWS region */
10
+ /** AWS region, e.g. 'us-east-1' */
8
11
  region: string;
9
12
  /** S3 bucket name */
10
13
  bucketName: string;
11
- /** AWS access key ID (optional if using environment variables) */
14
+ /** AWS access key ID omit to rely on environment/IAM resolution */
12
15
  accessKeyId?: string;
13
- /** AWS secret access key (optional if using environment variables) */
16
+ /** AWS secret access key omit to rely on environment/IAM resolution */
14
17
  secretAccessKey?: string;
15
- /** Default S3 ACL ('private', 'public-read', etc.) (default: 'private') */
16
- acl?: string;
17
- /** Base folder path in the bucket (default: '') */
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
+ */
18
22
  basePath?: string;
19
- /** URL expiration time in seconds for signed URLs (default: 3600) */
23
+ /** Signed URL TTL in seconds (default: 3600) */
20
24
  urlExpirationSeconds?: number;
21
- /** Default storage class for uploaded files (default: 'STANDARD') */
25
+ /** Default S3 storage class (default: INTELLIGENT_TIERING) */
22
26
  storageClass?: StorageClass;
23
27
  }
24
28
  /**
25
- * Options for uploading a file to S3
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.
26
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. */
27
44
  export interface UploadOptions {
28
- /** Folder path in the bucket (appended to basePath) */
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
+ */
29
55
  folder?: string;
30
- /** Override the file's content type */
31
- contentType?: string;
32
- /** Override the default ACL */
33
- acl?: string;
34
- /** Custom file name */
56
+ /**
57
+ * Custom filename to use instead of originalName.
58
+ * Used when `key` is not provided.
59
+ */
35
60
  fileName?: string;
36
- generateFileName?: boolean;
37
- /** Additional metadata for the file */
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 */
38
70
  metadata?: Record<string, string>;
39
- /** S3 storage class for the file */
71
+ /** Per-upload storage class override */
40
72
  storageClass?: StorageClass;
41
73
  }
42
- /**
43
- * Options for finding files in S3
44
- */
45
- export interface FindOptions {
46
- /** Folder path to search in (appended to basePath) */
74
+ /** Options for listing objects in a folder */
75
+ export interface ListOptions {
76
+ /** Folder path appended to basePath */
47
77
  folder?: string;
48
- /** Maximum number of results to return */
78
+ /** Additional prefix filter within the folder */
79
+ prefix?: string;
80
+ /** Maximum number of keys to return (1–1000, default: 1000) */
49
81
  maxResults?: number;
50
- /** Continuation token for pagination */
82
+ /** Continuation token from a previous list call for pagination */
51
83
  continuationToken?: string;
52
- /** Filter by file prefix */
53
- prefix?: string;
54
- /** Generate signed URLs for the files */
55
- generateUrls?: boolean;
56
84
  }
57
- /**
58
- * Result of a successful S3 upload
59
- */
60
- export interface FileResult {
61
- /** The S3 object key */
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 */
62
101
  key: string;
63
- /** The URL of the file (signed or public depending on ACL) */
64
- url: string;
65
- /** The file name */
66
- fileName: string;
67
- /** The original file name (for uploads) */
68
- originalName?: string;
69
- /** The file size in bytes */
70
- size?: number;
71
- /** The file's MIME type */
72
- contentType?: string;
73
- /** The ETag from S3 */
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) */
74
111
  etag?: string;
75
- /** Last modified date */
76
- lastModified?: Date;
77
- /** Additional metadata */
78
- metadata?: Record<string, string>;
79
- /** Storage class of the file */
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 */
80
126
  storageClass?: string;
127
+ /** ETag (without surrounding quotes) */
128
+ etag?: string;
81
129
  }
82
- /**
83
- * Result of a find operation
84
- */
85
- export interface FindResult {
86
- /** List of files found */
87
- files: FileResult[];
88
- /** Continuation token for pagination */
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 */
89
134
  continuationToken?: string;
90
- /** Whether there are more results */
135
+ /** True when there are more results beyond this page */
91
136
  hasMore: boolean;
92
137
  }
93
138
  export type DownloadMode = "buffer" | "stream";
94
139
  export type DownloadResult<T extends DownloadMode> = T extends "buffer" ? {
95
140
  buffer: Buffer;
96
- metadata: Record<string, any>;
141
+ metadata: ObjectMetadata;
97
142
  } : {
98
143
  stream: Readable;
99
- metadata: Record<string, any>;
144
+ metadata: ObjectMetadata;
100
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;
101
166
  /**
102
- * AWS File Manager class for handling S3 file operations
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);
103
173
  */
104
- export default class AwsFileManager {
105
- private s3Client;
106
- private bucketName;
107
- private acl;
108
- private basePath;
109
- private urlExpirationSeconds;
110
- private storageClass;
111
- static DEFAULTS: {
112
- storageClass: "INTELLIGENT_TIERING";
113
- urlExpirationSeconds: number;
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;
114
184
  };
185
+ constructor(config: AwsFileManagerConfig);
115
186
  /**
116
- * Creates a new AWS File Manager instance
117
- * @param config - Configuration options
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;
118
196
  */
119
- constructor(config: AwsFileManagerConfig);
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>;
120
238
  /**
121
- * Generates a full S3 key including the base path and folder
122
- * @param fileName - The file name
123
- * @param folder - Optional folder path
124
- * @returns Full S3 key
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]);
125
244
  */
126
- private getFullKey;
245
+ deleteMany(keys: string[]): Promise<void>;
127
246
  /**
128
- * Generates a file name for S3
129
- * @param originalName - The original file name
130
- * @param customName - Optional custom file name
131
- * @returns Generated file name
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);
132
254
  */
133
- private generateFileName;
255
+ copy(sourceKey: string, destinationKey: string, options?: {
256
+ storageClass?: StorageClass;
257
+ metadata?: Record<string, string>;
258
+ }): Promise<void>;
134
259
  /**
135
- * Uploads a single file to S3
136
- * @param file - The file to upload (Buffer or Multer file)
137
- * @param options - Upload options
138
- * @returns Upload result
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);
139
275
  */
140
- upload(file: File, options?: UploadOptions): Promise<FileResult>;
276
+ list(options?: ListOptions): Promise<ListResult>;
141
277
  /**
142
- * Downloads a file from S3
143
- * @param key - The S3 object key
144
- * @returns File buffer and metadata or null if not found
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
+ * }
145
284
  */
146
- download<T extends DownloadMode = "stream">(key: string, mode?: T): Promise<{
147
- buffer: Buffer;
148
- metadata: Record<string, any>;
149
- } | {
150
- stream: Readable;
151
- metadata: Record<string, any>;
152
- } | null>;
285
+ exists(key: string): Promise<boolean>;
153
286
  /**
154
- * Gets the S3 client instance
155
- * @returns S3 client
287
+ * Returns the underlying S3Client for operations not covered by this class.
288
+ * Use sparingly — prefer adding methods here so the abstraction stays coherent.
156
289
  */
157
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;
158
318
  }