@fluxmedia/s3 0.1.0-alpha.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,195 +1,366 @@
1
- //#region rolldown:runtime
2
- var __create = Object.create;
3
- var __defProp = Object.defineProperty;
4
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
- var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
- var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __copyProps = (to, from, except, desc) => {
9
- if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
- key = keys[i];
11
- if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
- get: ((k) => from[k]).bind(null, key),
13
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
14
- });
15
- }
16
- return to;
17
- };
18
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
19
- value: mod,
20
- enumerable: true
21
- }) : target, mod));
1
+ 'use strict';
2
+
3
+ var core = require('@fluxmedia/core');
22
4
 
23
- //#endregion
24
- const __fluxmedia_core = __toESM(require("@fluxmedia/core"));
5
+ // src/s3-provider.ts
25
6
 
26
- //#region src/features.ts
27
- /**
28
- * Feature matrix for S3 provider.
29
- * S3 is storage-only - no transformation support.
30
- */
31
- const S3Features = {
32
- transformations: {
33
- resize: false,
34
- crop: false,
35
- format: false,
36
- quality: false,
37
- blur: false,
38
- rotate: false,
39
- effects: false
40
- },
41
- capabilities: {
42
- signedUploads: true,
43
- directUpload: true,
44
- multipartUpload: true,
45
- videoProcessing: false,
46
- aiTagging: false,
47
- facialDetection: false
48
- },
49
- storage: {
50
- maxFileSize: 5 * 1024 * 1024 * 1024,
51
- supportedFormats: ["*"]
52
- }
7
+ // src/features.ts
8
+ var S3Features = {
9
+ transformations: {
10
+ resize: false,
11
+ crop: false,
12
+ format: false,
13
+ quality: false,
14
+ blur: false,
15
+ rotate: false,
16
+ effects: false
17
+ },
18
+ capabilities: {
19
+ signedUploads: true,
20
+ directUpload: true,
21
+ multipartUpload: true,
22
+ videoProcessing: false,
23
+ aiTagging: false,
24
+ facialDetection: false
25
+ },
26
+ storage: {
27
+ maxFileSize: 5 * 1024 * 1024 * 1024,
28
+ // 5GB
29
+ supportedFormats: ["*"]
30
+ // All formats
31
+ }
53
32
  };
54
33
 
55
- //#endregion
56
- //#region src/s3-provider.ts
57
- /**
58
- * AWS S3 provider implementation.
59
- * Storage-focused provider without transformation support.
60
- */
34
+ // src/s3-provider.ts
35
+ var cachedS3Client = null;
36
+ var cachedDeleteObjectCommand = null;
37
+ var cachedHeadObjectCommand = null;
38
+ var cachedUpload = null;
39
+ async function getS3Imports() {
40
+ if (!cachedS3Client) {
41
+ const sdk = await import('@aws-sdk/client-s3');
42
+ cachedS3Client = sdk.S3Client;
43
+ cachedDeleteObjectCommand = sdk.DeleteObjectCommand;
44
+ cachedHeadObjectCommand = sdk.HeadObjectCommand;
45
+ }
46
+ return {
47
+ S3Client: cachedS3Client,
48
+ DeleteObjectCommand: cachedDeleteObjectCommand,
49
+ HeadObjectCommand: cachedHeadObjectCommand
50
+ };
51
+ }
52
+ async function getUploadClass() {
53
+ if (!cachedUpload) {
54
+ const libStorage = await import('@aws-sdk/lib-storage');
55
+ cachedUpload = libStorage.Upload;
56
+ }
57
+ return cachedUpload;
58
+ }
61
59
  var S3Provider = class {
62
- name = "s3";
63
- features = S3Features;
64
- client = null;
65
- config;
66
- constructor(config) {
67
- this.config = config;
68
- }
69
- /**
70
- * Lazy-loads the AWS S3 SDK to minimize bundle size.
71
- */
72
- async ensureClient() {
73
- if (!this.client) {
74
- const { S3Client } = await import("@aws-sdk/client-s3");
75
- this.client = new S3Client({
76
- region: this.config.region,
77
- credentials: {
78
- accessKeyId: this.config.accessKeyId,
79
- secretAccessKey: this.config.secretAccessKey
80
- },
81
- endpoint: this.config.endpoint,
82
- forcePathStyle: this.config.forcePathStyle
83
- });
84
- }
85
- return this.client;
86
- }
87
- async upload(file, options) {
88
- const client = await this.ensureClient();
89
- try {
90
- const { PutObjectCommand } = await import("@aws-sdk/client-s3");
91
- const key = this.generateKey(options);
92
- const body = file instanceof Buffer ? file : await this.fileToBuffer(file);
93
- if (options?.onProgress) options.onProgress(0);
94
- const command = new PutObjectCommand({
95
- Bucket: this.config.bucket,
96
- Key: key,
97
- Body: body,
98
- ContentType: this.getContentType(file)
99
- });
100
- await client.send(command);
101
- if (options?.onProgress) options.onProgress(100);
102
- return this.createResult(key, body.length);
103
- } catch (error) {
104
- throw (0, __fluxmedia_core.createMediaError)(__fluxmedia_core.MediaErrorCode.UPLOAD_FAILED, this.name, error);
105
- }
106
- }
107
- async delete(id) {
108
- const client = await this.ensureClient();
109
- try {
110
- const { DeleteObjectCommand } = await import("@aws-sdk/client-s3");
111
- const command = new DeleteObjectCommand({
112
- Bucket: this.config.bucket,
113
- Key: id
114
- });
115
- await client.send(command);
116
- } catch (error) {
117
- throw (0, __fluxmedia_core.createMediaError)(__fluxmedia_core.MediaErrorCode.DELETE_FAILED, this.name, error);
118
- }
119
- }
120
- async get(id) {
121
- const client = await this.ensureClient();
122
- try {
123
- const { HeadObjectCommand } = await import("@aws-sdk/client-s3");
124
- const command = new HeadObjectCommand({
125
- Bucket: this.config.bucket,
126
- Key: id
127
- });
128
- const response = await client.send(command);
129
- return {
130
- id,
131
- url: this.getUrl(id),
132
- publicUrl: this.getUrl(id),
133
- size: response.ContentLength ?? 0,
134
- format: this.extractFormat(id),
135
- provider: this.name,
136
- metadata: { contentType: response.ContentType },
137
- createdAt: response.LastModified ?? /* @__PURE__ */ new Date()
138
- };
139
- } catch (error) {
140
- throw (0, __fluxmedia_core.createMediaError)(__fluxmedia_core.MediaErrorCode.FILE_NOT_FOUND, this.name, error);
141
- }
142
- }
143
- getUrl(id, _transform) {
144
- if (this.config.endpoint) return `${this.config.endpoint}/${this.config.bucket}/${id}`;
145
- return `https://${this.config.bucket}.s3.${this.config.region}.amazonaws.com/${id}`;
146
- }
147
- async uploadMultiple(files, options) {
148
- const uploadPromises = files.map((file) => this.upload(file, options));
149
- return Promise.all(uploadPromises);
150
- }
151
- async deleteMultiple(ids) {
152
- const deletePromises = ids.map((id) => this.delete(id));
153
- await Promise.all(deletePromises);
154
- }
155
- get native() {
156
- return this.client;
157
- }
158
- generateKey(options) {
159
- const filename = options?.filename ?? this.generateRandomId();
160
- const folder = options?.folder ? `${options.folder}/` : "";
161
- return `${folder}${filename}`;
162
- }
163
- generateRandomId() {
164
- return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
165
- }
166
- getContentType(file) {
167
- if (file instanceof Buffer) return "application/octet-stream";
168
- return file.type || "application/octet-stream";
169
- }
170
- extractFormat(key) {
171
- const parts = key.split(".");
172
- return parts.length > 1 ? parts[parts.length - 1] ?? "" : "";
173
- }
174
- async fileToBuffer(file) {
175
- const arrayBuffer = await file.arrayBuffer();
176
- return Buffer.from(arrayBuffer);
177
- }
178
- createResult(key, size) {
179
- return {
180
- id: key,
181
- url: this.getUrl(key),
182
- publicUrl: this.getUrl(key),
183
- size,
184
- format: this.extractFormat(key),
185
- provider: this.name,
186
- metadata: {},
187
- createdAt: /* @__PURE__ */ new Date()
188
- };
189
- }
60
+ constructor(config) {
61
+ this.name = "s3";
62
+ this.features = S3Features;
63
+ this.client = null;
64
+ this.clientPromise = null;
65
+ const required = ["region", "bucket", "accessKeyId", "secretAccessKey"];
66
+ const missing = required.filter((field) => !config[field]);
67
+ if (missing.length > 0) {
68
+ throw core.createMediaError(
69
+ core.MediaErrorCode.INVALID_CONFIG,
70
+ "s3",
71
+ new Error(`Missing required S3 configuration: ${missing.join(", ")}`)
72
+ );
73
+ }
74
+ if (config.bucket.includes("/")) {
75
+ throw core.createMediaError(
76
+ core.MediaErrorCode.INVALID_CONFIG,
77
+ "s3",
78
+ new Error("Bucket name cannot contain slashes")
79
+ );
80
+ }
81
+ Object.defineProperty(this, "config", {
82
+ value: config,
83
+ writable: false,
84
+ enumerable: false,
85
+ configurable: false
86
+ });
87
+ }
88
+ /**
89
+ * Get safe config info for debugging (no secrets)
90
+ */
91
+ getConfigInfo() {
92
+ return {
93
+ bucket: this.config.bucket,
94
+ region: this.config.region,
95
+ endpoint: this.config.endpoint
96
+ };
97
+ }
98
+ /**
99
+ * Initializes the S3 client lazily with race condition protection.
100
+ */
101
+ async ensureClient() {
102
+ if (!this.clientPromise) {
103
+ this.clientPromise = this.initializeClient();
104
+ }
105
+ return this.clientPromise;
106
+ }
107
+ async initializeClient() {
108
+ const { S3Client } = await getS3Imports();
109
+ const client = new S3Client({
110
+ region: this.config.region,
111
+ credentials: {
112
+ accessKeyId: this.config.accessKeyId,
113
+ secretAccessKey: this.config.secretAccessKey
114
+ },
115
+ ...this.config.endpoint && { endpoint: this.config.endpoint },
116
+ ...this.config.forcePathStyle !== void 0 && { forcePathStyle: this.config.forcePathStyle }
117
+ });
118
+ this.client = client;
119
+ return client;
120
+ }
121
+ async upload(file, options) {
122
+ const client = await this.ensureClient();
123
+ const Upload = await getUploadClass();
124
+ try {
125
+ const key = this.generateKey(options);
126
+ const { contentType, extension } = await this.getContentType(file);
127
+ const upload = new Upload({
128
+ client,
129
+ params: {
130
+ Bucket: this.config.bucket,
131
+ Key: key,
132
+ Body: file,
133
+ ContentType: contentType,
134
+ Metadata: {
135
+ ...options?.metadata || {},
136
+ extension
137
+ }
138
+ },
139
+ // Configuration for multipart upload
140
+ queueSize: 4,
141
+ // Upload 4 parts in parallel
142
+ partSize: 5 * 1024 * 1024,
143
+ // 5MB per part (S3 minimum)
144
+ leavePartsOnError: false
145
+ // Auto-cleanup failed uploads
146
+ });
147
+ if (options?.onProgress) {
148
+ upload.on("httpUploadProgress", (progress) => {
149
+ if (progress.total) {
150
+ const percentComplete = progress.loaded / progress.total * 100;
151
+ options.onProgress(percentComplete);
152
+ }
153
+ });
154
+ }
155
+ await upload.done();
156
+ const size = file instanceof Buffer ? file.byteLength : file.size;
157
+ return this.createResult(key, size, extension, options?.metadata);
158
+ } catch (error) {
159
+ throw this.mapS3Error(error, core.MediaErrorCode.UPLOAD_FAILED);
160
+ }
161
+ }
162
+ async delete(id) {
163
+ const client = await this.ensureClient();
164
+ const { DeleteObjectCommand } = await getS3Imports();
165
+ try {
166
+ const command = new DeleteObjectCommand({
167
+ Bucket: this.config.bucket,
168
+ Key: id
169
+ });
170
+ await client.send(command);
171
+ } catch (error) {
172
+ throw this.mapS3Error(error, core.MediaErrorCode.DELETE_FAILED);
173
+ }
174
+ }
175
+ async get(id) {
176
+ const client = await this.ensureClient();
177
+ const { HeadObjectCommand } = await getS3Imports();
178
+ try {
179
+ const command = new HeadObjectCommand({
180
+ Bucket: this.config.bucket,
181
+ Key: id
182
+ });
183
+ const response = await client.send(command);
184
+ const metadata = response.Metadata;
185
+ return {
186
+ id,
187
+ url: this.getUrl(id),
188
+ publicUrl: this.getUrl(id),
189
+ size: response.ContentLength ?? 0,
190
+ format: metadata?.extension || "",
191
+ provider: this.name,
192
+ metadata: {
193
+ contentType: response.ContentType,
194
+ extension: metadata?.extension
195
+ },
196
+ createdAt: response.LastModified ?? /* @__PURE__ */ new Date()
197
+ };
198
+ } catch (error) {
199
+ throw this.mapS3Error(error, core.MediaErrorCode.FILE_NOT_FOUND);
200
+ }
201
+ }
202
+ getUrl(id, _transform) {
203
+ if (this.config.endpoint) {
204
+ return `${this.config.endpoint}/${this.config.bucket}/${id}`;
205
+ }
206
+ return `https://${this.config.bucket}.s3.${this.config.region}.amazonaws.com/${id}`;
207
+ }
208
+ async uploadMultiple(files, options) {
209
+ const concurrency = options?.concurrency ?? 5;
210
+ const results = [];
211
+ let completedCount = 0;
212
+ for (let i = 0; i < files.length; i += concurrency) {
213
+ const batch = files.slice(i, i + concurrency);
214
+ const batchResults = await Promise.all(
215
+ batch.map((file) => {
216
+ const { concurrency: _, onBatchProgress: __, ...uploadOptions } = options ?? {};
217
+ return this.upload(
218
+ file,
219
+ Object.keys(uploadOptions).length > 0 ? uploadOptions : void 0
220
+ ).then((result) => {
221
+ completedCount++;
222
+ options?.onBatchProgress?.(completedCount, files.length);
223
+ return result;
224
+ });
225
+ })
226
+ );
227
+ results.push(...batchResults);
228
+ }
229
+ return results;
230
+ }
231
+ async deleteMultiple(ids) {
232
+ const concurrency = 10;
233
+ const failed = [];
234
+ for (let i = 0; i < ids.length; i += concurrency) {
235
+ const batch = ids.slice(i, i + concurrency);
236
+ const results = await Promise.allSettled(
237
+ batch.map((id) => this.delete(id).then(() => ({ id })))
238
+ );
239
+ results.forEach((result, index) => {
240
+ const id = batch[index];
241
+ if (result.status === "rejected") {
242
+ failed.push({ id, error: result.reason });
243
+ }
244
+ });
245
+ }
246
+ if (failed.length > 0) {
247
+ throw core.createMediaError(
248
+ core.MediaErrorCode.DELETE_FAILED,
249
+ this.name,
250
+ new Error(
251
+ `Failed to delete ${failed.length} of ${ids.length} files: ${failed.map((f) => f.id).join(", ")}`
252
+ )
253
+ );
254
+ }
255
+ }
256
+ /**
257
+ * Access to the native AWS S3 client.
258
+ * Returns the full S3Client with all methods and types.
259
+ */
260
+ get native() {
261
+ return this.client;
262
+ }
263
+ // Prevent credential exposure in serialization
264
+ toJSON() {
265
+ return {
266
+ name: this.name,
267
+ features: this.features
268
+ };
269
+ }
270
+ generateKey(options) {
271
+ const baseFilename = options?.filename ?? this.generateRandomId();
272
+ const shouldMakeUnique = options?.uniqueFilename !== false;
273
+ const filename = shouldMakeUnique && options?.filename ? `${baseFilename}-${this.generateShortId()}` : baseFilename;
274
+ const folder = options?.folder ? `${options.folder}/` : "";
275
+ return `${folder}${filename}`;
276
+ }
277
+ generateRandomId() {
278
+ return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
279
+ }
280
+ generateShortId() {
281
+ const cryptoObj = typeof globalThis !== "undefined" ? globalThis.crypto : void 0;
282
+ if (cryptoObj?.getRandomValues) {
283
+ const buffer = new Uint8Array(6);
284
+ cryptoObj.getRandomValues(buffer);
285
+ return Array.from(buffer).map((b) => b.toString(36).padStart(2, "0")).join("").substring(0, 8);
286
+ }
287
+ const timestamp = Date.now().toString(36);
288
+ const random = Math.random().toString(36).substring(2, 8);
289
+ return `${timestamp}${random}`.substring(0, 12);
290
+ }
291
+ async getContentType(file) {
292
+ if (file instanceof Buffer) {
293
+ const detected = await core.getFileType(file);
294
+ return { contentType: detected?.mime ?? "application/octet-stream", extension: detected?.ext ?? "" };
295
+ }
296
+ return { contentType: file.type || "application/octet-stream", extension: file.name.split(".").pop() || "" };
297
+ }
298
+ createResult(key, size, extension, metadata) {
299
+ return {
300
+ id: key,
301
+ url: this.getUrl(key),
302
+ publicUrl: this.getUrl(key),
303
+ size,
304
+ format: extension,
305
+ provider: this.name,
306
+ metadata: metadata || {},
307
+ createdAt: /* @__PURE__ */ new Date()
308
+ };
309
+ }
310
+ /**
311
+ * Maps S3-specific errors to MediaError with appropriate codes
312
+ */
313
+ mapS3Error(error, defaultCode) {
314
+ const err = error;
315
+ const httpCode = err.$metadata?.httpStatusCode;
316
+ if (err.name === "NoSuchBucket") {
317
+ throw core.createMediaError(
318
+ core.MediaErrorCode.INVALID_CONFIG,
319
+ this.name,
320
+ new Error(`Bucket '${this.config.bucket}' does not exist`)
321
+ );
322
+ }
323
+ if (err.name === "AccessDenied" || err.name === "InvalidAccessKeyId" || httpCode === 403) {
324
+ throw core.createMediaError(
325
+ core.MediaErrorCode.UNAUTHORIZED,
326
+ this.name,
327
+ new Error("Access denied - check S3 credentials and bucket permissions")
328
+ );
329
+ }
330
+ if (err.name === "SignatureDoesNotMatch" || httpCode === 401) {
331
+ throw core.createMediaError(
332
+ core.MediaErrorCode.INVALID_CREDENTIALS,
333
+ this.name,
334
+ new Error("Invalid AWS credentials - check accessKeyId and secretAccessKey")
335
+ );
336
+ }
337
+ if (err.name === "NoSuchKey" || httpCode === 404) {
338
+ throw core.createMediaError(core.MediaErrorCode.FILE_NOT_FOUND, this.name, error);
339
+ }
340
+ if (err.name === "SlowDown" || err.name === "ServiceUnavailable" || httpCode === 503) {
341
+ throw core.createMediaError(
342
+ core.MediaErrorCode.NETWORK_ERROR,
343
+ this.name,
344
+ new Error("AWS S3 service temporarily unavailable or rate limited - try again later")
345
+ );
346
+ }
347
+ if (err.name === "TimeoutError" || err.message?.includes("ETIMEDOUT") || err.message?.includes("ECONNRESET")) {
348
+ throw core.createMediaError(
349
+ core.MediaErrorCode.NETWORK_ERROR,
350
+ this.name,
351
+ new Error("Network timeout - check connection or try smaller file")
352
+ );
353
+ }
354
+ if (err.name === "EntityTooLarge" || httpCode === 413) {
355
+ throw core.createMediaError(
356
+ core.MediaErrorCode.FILE_TOO_LARGE,
357
+ this.name,
358
+ new Error("File exceeds maximum allowed size")
359
+ );
360
+ }
361
+ throw core.createMediaError(defaultCode, this.name, error);
362
+ }
190
363
  };
191
364
 
192
- //#endregion
193
365
  exports.S3Features = S3Features;
194
366
  exports.S3Provider = S3Provider;
195
- //# sourceMappingURL=index.cjs.map