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