@b2y/document-module
Version:
A flexible multi-provider storage adapter for file operations across S3, Azure Blob, Google Drive, and local storage
144 lines (122 loc) • 4.51 kB
JavaScript
// src/providers/AmazonS3StorageProvider.js
const { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand } = require('@aws-sdk/client-s3');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
const fs = require('fs');
const path = require('path');
const stream = require('stream');
const BaseStorageProvider = require('./BaseStorageProvider');
const logger = require('../../logger');
class AmazonS3StorageProvider extends BaseStorageProvider {
constructor(config = {}) {
super();
// Allow passing configuration directly or using environment variables
this.bucketName = config.bucketName || process.env.AWS_S3_BUCKET_NAME;
if (!this.bucketName) {
throw new Error("AWS S3 bucket name must be specified.");
}
this.client = new S3Client({
region: config.region || process.env.AWS_REGION,
credentials: {
accessKeyId: config.accessKeyId || process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: config.secretAccessKey || process.env.AWS_SECRET_ACCESS_KEY
}
});
// Default URL expiration time in seconds (24 hours)
this.urlExpiresIn = config.urlExpiresIn || process.env.AWS_URL_EXPIRES_IN
? parseInt(config.urlExpiresIn || process.env.AWS_URL_EXPIRES_IN)
: 86400;
}
async uploadFile(file, destinationPath) {
try {
let fileContent;
// If file is a stream, use it directly
if (file.stream) {
fileContent = file.stream;
}
// If file is a path, read from fs
else if (typeof file === 'string') {
fileContent = fs.createReadStream(file);
}
// If file is from multer
else if (file.path) {
fileContent = fs.createReadStream(file.path);
}
// If file is a buffer
else if (Buffer.isBuffer(file)) {
fileContent = file;
}
// Construct the full S3 key with bucket name at the beginning
const fullPath = `${this.bucketName}/${destinationPath}`;
const params = {
Bucket: this.bucketName,
Key: destinationPath,
Body: fileContent,
ContentType: file.mimetype || 'application/octet-stream'
};
const command = new PutObjectCommand(params);
await this.client.send(command);
// Return the path to be stored in the database
return {
success: true,
path: fullPath,
// No URL is returned as we'll generate it on demand
};
} catch (error) {
logger.error('Error uploading file to S3:', error);
throw error;
}
}
async getFile(filePath) {
try {
// Extract the actual key from the full path if needed
let key = filePath;
if (filePath.startsWith(`${this.bucketName}/`)) {
key = filePath.substring(this.bucketName.length + 1);
}
const params = {
Bucket: this.bucketName,
Key: key
};
const command = new GetObjectCommand(params);
const response = await this.client.send(command);
return response.Body;
} catch (error) {
logger.error('Error getting file from S3:', error);
throw error;
}
}
async deleteFile(filePath) {
try {
// Extract the actual key from the full path if needed
let key = filePath;
if (filePath.startsWith(`${this.bucketName}/`)) {
key = filePath.substring(this.bucketName.length + 1);
}
const params = {
Bucket: this.bucketName,
Key: key
};
const command = new DeleteObjectCommand(params);
await this.client.send(command);
return { success: true };
} catch (error) {
console.error('Error deleting file from S3:', error);
throw error;
}
}
async getPublicUrl(filePath) {
// Extract the actual key from the full path if needed
let key = filePath;
if (filePath.startsWith(`${this.bucketName}/`)) {
key = filePath.substring(this.bucketName.length + 1);
}
// Generate a presigned URL that expires after the configured time
const command = new GetObjectCommand({
Bucket: this.bucketName,
Key: key
});
// Return a promise that resolves to the signed URL
return getSignedUrl(this.client, command, { expiresIn: this.urlExpiresIn });
}
}
module.exports = AmazonS3StorageProvider;