upload-file-spaces
Version:
A simple and reusable Node.js library for uploading files to DigitalOcean Spaces. Supports single and multiple file uploads, file type validation, and efficient integration with Express and Multer.
292 lines (260 loc) • 10.7 kB
JavaScript
const { S3Client, GetObjectCommand, HeadObjectCommand,
PutObjectCommand, DeleteObjectCommand, ListObjectsV2Command
} = require("@aws-sdk/client-s3");
const crypto = require('crypto');
const path = require("path");
const { pipeline } = require("stream");
const fs = require("fs");
const util = require("util");
// Promisify pipeline for easier async/await usage
const streamPipeline = util.promisify(pipeline);
class FileUploader {
/**
* Initializes the S3 client with given configuration.
* @param {Object} config - Configuration object.
* @param {string} config.accessKeyId - AWS access key ID.
* @param {string} config.secretAccessKey - AWS secret access key.
* @param {string} config.region - AWS region.
* @param {string} [config.endpoint] - Endpoint for S3-compatible services (e.g., DigitalOcean Spaces).
*/
constructor({ accessKeyId, secretAccessKey, region, endpoint, subDomainEndpoint = null }) {
this.s3 = new S3Client({
region,
endpoint:endpoint,
credentials: { accessKeyId, secretAccessKey }
});
this.region = region;
this.endpoint = endpoint;
this.subDomainEndpoint = subDomainEndpoint
}
/**
* Check a single file exist in the specified bucket.
* @param {Object} params - Upload parameters.
* @param {string} params.bucketName - Name of the bucket.
* @param {string} params.key - file.
* @returns {Promise<boolean>} - Returns the true is file exist else false.
*/
async fileExists({ bucketName, key }) {
try {
const command = new HeadObjectCommand({
Bucket: bucketName,
Key: key,
});
await this.s3.send(command); // If this succeeds, the file exists
// console.log(`File ${key} exists in bucket ${bucketName}`);
return true;
} catch (error) {
if (error.name === "NotFound") {
// console.log(`File ${key} does not exist in bucket ${bucketName}`);
return false;
}
// console.error("Error checking file existence:", error);
throw new Error("Could not verify file existence.");
}
}
/**
* Uploads a single file to the specified bucket.
* @param {Object} params - Upload parameters.
* @param {string} params.bucketName - Name of the bucket.
* @param {string} params.file - the local file. *
* @param {string} params.key - file name to save. *
* @param {string} params.folder - folder name to save.
* @param {string} [params.acl] - Access control for the file (e.g., 'public-read').
* @returns {Promise<string>} - Returns the file URL.
*/
async uploadFile({ bucketName, file, key = null, folder = null, acl = "public-read" }) {
try {
let fileName = key;
if(key == null){
const id = this.getUniqueId();
fileName = this.getFileName(id,file.originalname);
}
fileName = folder ? `${folder}/${fileName}` : fileName;
const command = new PutObjectCommand({
Bucket: bucketName,
Key: fileName,
Body: file.buffer,
ACL: acl,
ContentType: file.mimetype,
});
await this.s3.send(command);
return {
id: fileName,
url_preview: this.subDomainEndpoint ? `${this.subDomainEndpoint}/${fileName}` :
`https://${bucketName}.${this.region}.digitaloceanspaces.com/${fileName}`,
originalname: file.originalname
};
} catch (error) {
console.error("Error uploading file");
throw error;
}
}
/**
* Uploads a multiple files to the specified bucket.
* @param {Object} params - Upload parameters.
* @param {string} params.bucketName - Name of the bucket.
* @param {string} params.files - the local files.
* @param {string} params.folder - folder name to save.
* @param {string} [params.acl] - Access control for the file (e.g., 'public-read').
* @returns {Promise<string>} - Returns the file URL.
*/
async uploadMultipleFiles({ bucketName, files, folder = null, acl = "public-read" }) {
const uploadedFiles = [];
try {
await Promise.all(
files.map(async (file) => {
const id = this.getUniqueId();
let fileName = this.getFileName(id,file.originalname);
fileName = folder ? `${folder}/${fileName}` : fileName;
const command = new PutObjectCommand({
Bucket: bucketName,
Key: fileName,
Body: file.buffer,
ACL: acl,
ContentType: file.mimetype,
});
await this.s3.send(command);
uploadedFiles.push({
id: fileName,
url_preview: this.subDomainEndpoint ? `${this.subDomainEndpoint}/${fileName}` :
`https://${bucketName}.${this.region}.digitaloceanspaces.com/${fileName}`,
originalname: file.originalname
});
})
);
return uploadedFiles;
} catch (error) {
console.error("Error uploading file");
throw error;
}
}
/**
* Uploads a single buffer file to the specified bucket.
* @param {Object} params - Upload parameters.
* @param {string} params.bucketName - Name of the bucket.
* @param {string} params.buffer - the file buffer. *
* @param {string} params.key - file name to save. *
* @param {string} params.contentType - file type to save. *
* @param {string} params.folder - folder name to save.
* @param {string} [params.acl] - Access control for the file (e.g., 'public-read').
* @returns {Promise<string>} - Returns the file URL.
*/
async uploadBufferFile({ bucketName, buffer, key, contentType, folder = null, acl = "public-read" }) {
try {
let fileName = key;
fileName = folder ? `${folder}/${fileName}` : fileName;
const command = new PutObjectCommand({
Bucket: bucketName,
Key: fileName,
Body: buffer,
ACL: acl,
ContentType: contentType
});
await this.s3.send(command);
return {
id: fileName,
url_preview: this.subDomainEndpoint ? `${this.subDomainEndpoint}/${fileName}` :
`https://${bucketName}.${this.region}.digitaloceanspaces.com/${fileName}`,
originalname: key
};
} catch (error) {
console.error("Error uploading file");
throw error;
}
}
/**
* Delete multiple files to the specified bucket.
* @param {Object} params - Upload parameters.
* @param {string} params.bucketName - Name of the bucket.
* @param {string} params.files - the files.
* @returns {Promise<string>} - Returns the status.
*/
async deleteFiles({ bucketName, files }) {
try {
await Promise.all(
files.map(async (id) => {
const command = new DeleteObjectCommand({
Bucket: bucketName,
Key: id,
});
await this.s3.send(command);
})
);
return { success: true, message: `${files.length > 0 ? 'Files' : 'File' } deleted successfully.` };
} catch (error) {
console.error("Error deleting file:", error);
throw new Error("Could not delete the file. Please check the bucket and key.");
}
}
/**
* Download multiple files based on keys from the specified bucket.
* @param {Object} params - Upload parameters.
* @param {string} params.bucketName - Name of the bucket.
* @param {string} params.keys - the file keys.
* @param {string} params.downloadPath - Path to download files.
* @returns {Promise<string>} - Returns the status.
*/
async downloadMultipleFiles({ bucketName, keys, downloadPath }) {
try {
for (const key of keys) {
const command = new GetObjectCommand({
Bucket: bucketName,
Key: key,
});
const response = await this.s3.send(command);
const filePath = `${downloadPath}/${key.split('/').pop()}`;
// Stream the file content to the specified path
await streamPipeline(response.Body, fs.createWriteStream(filePath));
// console.log(`File ${key} downloaded successfully to ${filePath}`);
}
return { success: true, message: "All files downloaded successfully." };
} catch (error) {
console.error("Error downloading files:", error);
throw new Error("Could not download some or all files. Check the bucket and keys.");
}
}
// Method to download files from a specific folder
/**
* Download download files from a specific folder and the specified bucket.
* @param {Object} params - Upload parameters.
* @param {string} params.bucketName - Name of the bucket.
* @param {string} params.folder - the files folder name.
* @param {string} params.downloadPath - Path to download files.
* @returns {Promise<string>} - Returns the status.
*/
async downloadFromFolder({ bucketName, folder, downloadPath }) {
try {
const command = new ListObjectsV2Command({
Bucket: bucketName,
Prefix: folder, // Get all objects with the specified prefix (folder)
});
const response = await this.s3.send(command);
if (!response.Contents || response.Contents.length === 0) {
// console.log(`No files found in folder ${folder}`);
return { success: false, message: `No files found in folder ${folder}` };
}
const keys = response.Contents.map((file) => file.Key);
// Download each file
return await this.downloadMultipleFiles({ bucketName, keys, downloadPath });
} catch (error) {
console.error("Error downloading files from folder:", error);
throw new Error("Could not download files from the folder. Check the bucket and folder.");
}
}
/**
* Generate unique id.
*/
getUniqueId() {
const timestamp = Date.now().toString(16);
const mathRandom = Math.random().toString(16).substring(2, 10);
const cryptoRandom = crypto.randomBytes(8).toString('hex');
return `${timestamp}-${mathRandom}-${cryptoRandom}`;
}
/**
* Generate file name.
*/
getFileName(id, originalname){
return `${id}${path.extname(originalname).toLowerCase()}`;
}
}
module.exports = FileUploader;