@ahhaohho/s3-upload-sdk
Version:
S3 + CloudFront presigned URL SDK for AhhaOhho platform
206 lines (205 loc) • 7.16 kB
JavaScript
;
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;