UNPKG

@ahhaohho/s3-upload-sdk

Version:

S3 + CloudFront presigned URL SDK for AhhaOhho platform

206 lines (205 loc) 7.16 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.S3UploadClient = void 0; const axios_1 = __importDefault(require("axios")); /** * S3 Upload Client * Handles file uploads to S3 using presigned URLs with CloudFront distribution */ class S3UploadClient { constructor(config) { this.config = config; this.axiosInstance = axios_1.default.create({ baseURL: config.apiBaseUrl, timeout: 30000, headers: { 'Content-Type': 'application/json', ...config.headers, }, }); // Add auth token if provided if (config.authToken) { this.axiosInstance.defaults.headers.common['Authorization'] = `Bearer ${config.authToken}`; } } /** * Update auth token */ setAuthToken(token) { this.config.authToken = token; this.axiosInstance.defaults.headers.common['Authorization'] = `Bearer ${token}`; } /** * Get presigned URL from API server */ async getPresignedUrl(folder, filename, contentType) { try { const response = await this.axiosInstance.post('/communities/s3/presigned-url', { folder, filename, contentType, }); return response.data.data || response.data; } catch (error) { const uploadError = new Error(error.response?.data?.message || 'Failed to get presigned URL'); uploadError.code = 'PRESIGNED_URL_ERROR'; uploadError.statusCode = error.response?.status; uploadError.details = error.response?.data; throw uploadError; } } /** * Upload file to S3 using presigned URL */ async uploadToS3(file, uploadUrl, contentType, onProgress) { try { await axios_1.default.put(uploadUrl, file, { headers: { 'Content-Type': contentType, }, onUploadProgress: (progressEvent) => { if (onProgress && progressEvent.total) { const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total); onProgress(progress); } }, }); } catch (error) { const uploadError = new Error('Failed to upload file to S3'); uploadError.code = 'S3_UPLOAD_ERROR'; uploadError.statusCode = error.response?.status; uploadError.details = error.response?.data; throw uploadError; } } /** * Generate filename from File or Blob */ generateFilename(file, customFilename) { const timestamp = Date.now(); const random = Math.random().toString(36).substring(2, 8); // Get extension from file let extension = ''; if (file instanceof File && file.name) { const parts = file.name.split('.'); if (parts.length > 1) { extension = parts[parts.length - 1]; } } else if (file.type) { // Infer extension from MIME type const mimeToExt = { 'image/jpeg': 'jpg', 'image/jpg': 'jpg', 'image/png': 'png', 'image/gif': 'gif', 'image/webp': 'webp', }; extension = mimeToExt[file.type] || ''; } if (customFilename) { return extension ? `${customFilename}-${timestamp}-${random}.${extension}` : `${customFilename}-${timestamp}-${random}`; } return extension ? `file-${timestamp}-${random}.${extension}` : `file-${timestamp}-${random}`; } /** * Get content type from File or Blob */ getContentType(file, customContentType) { if (customContentType) { return customContentType; } if (file.type) { return file.type; } return 'application/octet-stream'; } /** * Upload a file to S3 and return the CloudFront URL * * @param options - Upload options * @returns Upload result with CloudFront URL * * @example * ```typescript * const result = await client.upload({ * file: fileInput.files[0], * folder: 'announcements', * onProgress: (progress) => console.log(`${progress}%`) * }); * console.log('File URL:', result.url); * ``` */ async upload(options) { const { file, folder, filename, contentType, onProgress } = options; // Generate filename and content type const finalFilename = this.generateFilename(file, filename); const finalContentType = this.getContentType(file, contentType); try { // Step 1: Get presigned URL from API const presignedData = await this.getPresignedUrl(folder, finalFilename, finalContentType); // Step 2: Upload file to S3 using presigned URL await this.uploadToS3(file, presignedData.uploadUrl, finalContentType, onProgress); // Step 3: Return the public CloudFront URL return { url: presignedData.publicUrl, key: presignedData.key, size: file.size, }; } catch (error) { throw error; } } /** * Upload multiple files in parallel * * @param files - Array of files with their options * @returns Array of upload results * * @example * ```typescript * const results = await client.uploadMultiple([ * { file: file1, folder: 'announcements' }, * { file: file2, folder: 'announcements' } * ]); * const urls = results.map(r => r.url); * ``` */ async uploadMultiple(files) { const uploadPromises = files.map((options) => this.upload(options)); return Promise.all(uploadPromises); } /** * Validate file before upload * * @param file - File to validate * @param maxSizeMB - Maximum file size in MB * @param allowedTypes - Allowed MIME types * @returns Validation result */ validateFile(file, maxSizeMB = 10, allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif']) { // Check file size const maxSizeBytes = maxSizeMB * 1024 * 1024; if (file.size > maxSizeBytes) { return { valid: false, error: `File size exceeds ${maxSizeMB}MB`, }; } // Check file type if (file.type && allowedTypes.length > 0 && !allowedTypes.includes(file.type)) { return { valid: false, error: `File type ${file.type} is not allowed. Allowed types: ${allowedTypes.join(', ')}`, }; } return { valid: true }; } } exports.S3UploadClient = S3UploadClient;