@d3oxy/s3-pilot
Version:
A TypeScript wrapper for AWS S3 with support for multiple clients, buckets, and secure file downloads.
546 lines (545 loc) • 26.3 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __asyncValues = (this && this.__asyncValues) || function (o) {
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
var m = o[Symbol.asyncIterator], i;
return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.S3Pilot = void 0;
const client_s3_1 = require("@aws-sdk/client-s3");
const s3_request_presigner_1 = require("@aws-sdk/s3-request-presigner");
/**
* S3Pilot class abstracts interactions with AWS S3 SDK to provide a cleaner API.
* @template Clients - Mapping of client names to their configurations.
*/
class S3Pilot {
/**
* Creates an instance of the S3Pilot class.
* @param config - The configuration object for setting up S3 clients.
*/
constructor(config) {
this.clientInstances = {};
for (const clientName in config) {
const { region, accessKeyId, secretAccessKey, buckets, additionalConfig, keyPrefix, enableDefaultAllowedExtensions, allowedExtensions, validateBucketOnInit = false, } = config[clientName];
const s3Client = new client_s3_1.S3Client(Object.assign({ region, credentials: {
accessKeyId,
secretAccessKey,
} }, additionalConfig));
// Validate each bucket
if (validateBucketOnInit) {
for (const bucket of buckets) {
this.validateBucketOnInit(s3Client, bucket);
}
}
this.clientInstances[clientName] = {
s3: s3Client,
buckets: new Set(buckets),
keyPrefix: keyPrefix,
region: region,
enableDefaultAllowedExtensions: enableDefaultAllowedExtensions,
allowedExtensions: allowedExtensions,
validateBucketOnInit: validateBucketOnInit,
};
}
}
/**
* Retrieves the URL for a given object key in a specific bucket of a client.
* @param clientName - The name of the client.
* @param bucket - The bucket object from the client.
* @param key - The object key.
* @returns A promise that resolves to the URL of the object.
*/
getUrl(clientName, bucket, key) {
return __awaiter(this, void 0, void 0, function* () {
this.validateBucket(clientName, bucket);
const client = this.getClient(clientName);
return `https://${bucket}.s3.${client.region}.amazonaws.com/${key}`;
});
}
/**
* Validates the specified bucket on initialization.
*
* @param client - The S3 client.
* @param bucket - The name of the bucket to validate.
* @throws An error if the bucket is not valid or the user does not have access to it.
*/
validateBucketOnInit(client, bucket) {
return __awaiter(this, void 0, void 0, function* () {
try {
yield client.send(new client_s3_1.HeadBucketCommand({ Bucket: bucket }));
}
catch (error) {
throw new Error(`Bucket ${bucket} is not valid or you do not have access to it.`);
}
});
}
/**
* Validates if a bucket is available for a given client.
*
* @template ClientName - The name of the client.
* @param clientName - The name of the client.
* @param bucket - The bucket to validate.
* @throws {Error} If the bucket is not available for the client.
*/
validateBucket(clientName, bucket) {
if (!this.clientInstances[clientName].buckets.has(bucket)) {
throw new Error(`Bucket ${bucket} is not available for client ${String(clientName)}`);
}
}
validateExtension(clientName, extension) {
const client = this.getClient(clientName);
let allowedExtensions = [];
if (client.enableDefaultAllowedExtensions) {
allowedExtensions = [
"pdf",
"jpg",
"jpeg",
"JEPG",
"png",
"heic",
"heif",
"gif",
"doc",
"docx",
"xls",
"xlsx",
"ppt",
"pptx",
"txt",
"csv",
"mp4",
"mov",
"avi",
"mkv",
"zip",
"rar",
"webp",
"svg",
"json",
"ts",
];
}
else {
allowedExtensions = client.allowedExtensions || [];
}
if (allowedExtensions.length > 0 && !allowedExtensions.includes(extension)) {
throw new Error(`Invalid file extension: ${extension}`);
}
return;
}
/**
* Retrieves the client instance for the specified client name.
*
* @template ClientName - The type of the client name.
* @param {ClientName} clientName - The name of the client.
* @returns {Object} - An object containing the S3 client, a set of buckets, and an optional key prefix.
*/
getClient(clientName) {
return this.clientInstances[clientName];
}
/**
* Uploads a file stream to the specified bucket for the given client.
*
* @template ClientName - The name of the client.
* @param {ClientName} clientName - The name of the client.
* @param {Clients[ClientName]["buckets"][number]} bucket - The bucket to upload the file to.
* @param {Object} params - The parameters for the file upload.
* @param {string} params.filename - The name of the file.
* @param {string} [params.folder] - The folder to upload the file to (optional).
* @param {Readable} params.stream - The readable stream to upload.
* @param {string} params.contentType - The content type of the file.
* @param {number} [params.contentLength] - The content length of the stream (optional, but recommended for large files).
* @param {Omit<PutObjectCommand, "Bucket" | "Key" | "Body">} [params.additionalParams] - Additional parameters for the PutObjectCommand (optional).
* @returns {Promise<UploadFileStreamResponse>} A promise that resolves to an object containing the URL, key, etag, and versionId of the uploaded file.
* @throws {Error} If the file extension is invalid or if the file fails to upload.
*/
uploadFileStream(clientName, bucket, params) {
return __awaiter(this, void 0, void 0, function* () {
this.validateBucket(clientName, bucket);
const client = this.getClient(clientName);
const { filename, folder, stream, contentType, contentLength, additionalParams } = params;
// check if the extension is valid
const extension = filename.split(".").pop();
if (!extension) {
throw new Error("Invalid file extension.");
}
this.validateExtension(clientName, extension);
const key = `${client.keyPrefix ? `${client.keyPrefix}/` : ""}${folder ? `${folder}/` : ""}${filename}`;
const command = new client_s3_1.PutObjectCommand(Object.assign(Object.assign(Object.assign({}, additionalParams), { Bucket: bucket, Key: key, Body: stream, ContentType: contentType }), (contentLength && { ContentLength: contentLength })));
const res = yield client.s3.send(command);
if (res.$metadata.httpStatusCode !== 200) {
throw new Error(`Failed to upload file stream to ${bucket}/${key}`);
}
// construct the URL of the uploaded file
const url = yield this.getUrl(clientName, bucket, key);
return {
url: url,
key: key,
etag: res.ETag,
versionId: res.VersionId,
};
});
}
/**
* Uploads a file to the specified bucket for the given client.
*
* @template ClientName - The name of the client.
* @param {ClientName} clientName - The name of the client.
* @param {Clients[ClientName]["buckets"][number]} bucket - The bucket to upload the file to.
* @param {Object} params - The parameters for the file upload.
* @param {string} params.filename - The name of the file.
* @param {string} [params.folder] - The folder to upload the file to (optional).
* @param {Buffer | Uint8Array | Blob | string} params.file - The file to upload.
* @param {string} params.contentType - The content type of the file.
* @param {Omit<PutObjectCommand, "Bucket" | "Key" | "Body">} [params.additionalParams] - Additional parameters for the PutObjectCommand (optional).
* @returns {Promise<UploadFileResponse>} A promise that resolves to an object containing the URL and key of the uploaded file.
* @throws {Error} If the file extension is invalid or if the file fails to upload.
*/
uploadFile(clientName, bucket, params) {
return __awaiter(this, void 0, void 0, function* () {
this.validateBucket(clientName, bucket);
const client = this.getClient(clientName);
const { filename, folder, file, contentType, additionalParams } = params;
// check if the extension is valid
const extension = filename.split(".").pop();
if (!extension) {
throw new Error("Invalid file extension.");
}
this.validateExtension(clientName, extension);
const key = `${client.keyPrefix ? `${client.keyPrefix}/` : ""}${folder ? `${folder}/` : ""}${filename}`;
const command = new client_s3_1.PutObjectCommand(Object.assign(Object.assign({}, additionalParams), { Bucket: bucket, Key: key, Body: file, ContentType: contentType }));
const res = yield client.s3.send(command);
if (res.$metadata.httpStatusCode !== 200) {
throw new Error(`Failed to upload file to ${bucket}/${key}`);
}
// construct the URL of the uploaded file
const url = yield this.getUrl(clientName, bucket, key);
return {
url: url,
key: key,
};
});
}
/**
* Deletes a file from the specified bucket.
*
* @param clientName - The name of the client.
* @param bucket - The bucket from which to delete the file.
* @param params - The parameters for deleting the file.
* @param params.key - The key of the file to delete.
* @returns A Promise that resolves when the file is successfully deleted.
*/
deleteFile(clientName, bucket, params) {
return __awaiter(this, void 0, void 0, function* () {
const { key } = params;
this.validateBucket(clientName, bucket);
const client = this.getClient(clientName);
const command = new client_s3_1.DeleteObjectCommand({
Bucket: bucket,
Key: key,
});
yield client.s3.send(command);
});
}
/**
* Renames a file in the specified bucket of a client.
*
* @template ClientName - The name of the client.
* @param {ClientName} clientName - The name of the client.
* @param {Clients[ClientName]["buckets"][number]} bucket - The bucket in which the file is located.
* @param {Object} params - The parameters for renaming the file.
* @param {string} params.oldKey - The key of the file to be renamed.
* @param {string} [params.newFolder] - The new folder for the renamed file (optional).
* @param {string} [params.newFilename] - The new filename for the renamed file (optional).
* @returns {Promise<{
* url: string;
* key: string;
* acl: ObjectCannedACL;
* }>} - A promise that resolves to an object containing the URL, key, and ACL of the renamed file.
* @deprecated This function is not stable and should not be used.
*/
renameFile(clientName, bucket, params) {
return __awaiter(this, void 0, void 0, function* () {
this.validateBucket(clientName, bucket);
const client = this.getClient(clientName);
const { oldKey, newFolder, newFilename } = params;
const extension = oldKey.split(".").pop();
const newKey = `${client.keyPrefix ? `${client.keyPrefix}/` : ""}${newFolder ? `${newFolder}/` : ""}${newFilename}.${extension}`;
const copyCommand = new client_s3_1.CopyObjectCommand({
Bucket: bucket,
CopySource: `${bucket}/${oldKey}`,
Key: newKey,
});
yield client.s3.send(copyCommand);
yield this.deleteFile(clientName, bucket, {
key: oldKey,
});
const url = yield this.getUrl(clientName, bucket, newKey);
return {
url: url,
key: newKey,
};
});
}
/**
* Downloads a file from S3 and returns its content as a Buffer.
* This method is useful for server-side file processing or when you need to
* stream file content to clients through your API server.
*
* @template ClientName - The name of the client.
* @param clientName - The name of the client.
* @param bucket - The bucket containing the file.
* @param params - The parameters for downloading the file.
* @param params.key - The key of the file to download.
* @returns A promise that resolves to an object containing the file buffer and metadata.
* @throws {Error} If the file cannot be downloaded or doesn't exist.
*/
getFile(clientName, bucket, params) {
return __awaiter(this, void 0, void 0, function* () {
var _a, e_1, _b, _c;
this.validateBucket(clientName, bucket);
const client = this.getClient(clientName);
const { key } = params;
const command = new client_s3_1.GetObjectCommand({
Bucket: bucket,
Key: key,
});
try {
const response = yield client.s3.send(command);
if (!response.Body) {
throw new Error(`File ${key} not found in bucket ${bucket}`);
}
// Convert the readable stream to a buffer
const chunks = [];
const stream = response.Body;
try {
for (var _d = true, stream_1 = __asyncValues(stream), stream_1_1; stream_1_1 = yield stream_1.next(), _a = stream_1_1.done, !_a; _d = true) {
_c = stream_1_1.value;
_d = false;
const chunk = _c;
chunks.push(Buffer.from(chunk));
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (!_d && !_a && (_b = stream_1.return)) yield _b.call(stream_1);
}
finally { if (e_1) throw e_1.error; }
}
const buffer = Buffer.concat(chunks);
return {
buffer,
contentType: response.ContentType,
contentLength: response.ContentLength,
etag: response.ETag,
lastModified: response.LastModified,
};
}
catch (error) {
if (error instanceof Error && error.name === "NoSuchKey") {
throw new Error(`File ${key} not found in bucket ${bucket}`);
}
throw error;
}
});
}
/**
* Downloads a file from S3 and returns its content as a readable stream.
* This method is useful for streaming large files efficiently without loading
* the entire file into memory.
*
* @template ClientName - The name of the client.
* @param clientName - The name of the client.
* @param bucket - The bucket containing the file.
* @param params - The parameters for downloading the file.
* @param params.key - The key of the file to download.
* @returns A promise that resolves to an object containing the file stream and metadata.
* @throws {Error} If the file cannot be downloaded or doesn't exist.
*/
getFileStream(clientName, bucket, params) {
return __awaiter(this, void 0, void 0, function* () {
this.validateBucket(clientName, bucket);
const client = this.getClient(clientName);
const { key } = params;
const command = new client_s3_1.GetObjectCommand({
Bucket: bucket,
Key: key,
});
try {
const response = yield client.s3.send(command);
if (!response.Body) {
throw new Error(`File ${key} not found in bucket ${bucket}`);
}
return {
stream: response.Body,
contentType: response.ContentType,
contentLength: response.ContentLength,
etag: response.ETag,
lastModified: response.LastModified,
};
}
catch (error) {
if (error instanceof Error && error.name === "NoSuchKey") {
throw new Error(`File ${key} not found in bucket ${bucket}`);
}
throw error;
}
});
}
/**
* Generates a signed URL for accessing an object in a specific bucket of a client.
* Enhanced version with support for download-specific headers to force browser download
* and avoid CORS issues.
*
* @param clientName - The name of the client.
* @param bucket - The bucket to generate the signed URL for.
* @param params - Additional parameters for generating the signed URL.
* @param params.key - The key of the object.
* @param params.expiresIn - The expiration time of the signed URL in seconds. Defaults to 1 hour (3600 seconds).
* @param params.responseContentDisposition - Optional content disposition header to force download behavior.
* @param params.responseContentType - Optional content type header.
* @param params.responseCacheControl - Optional cache control header.
* @param params.responseContentLanguage - Optional content language header.
* @param params.responseContentEncoding - Optional content encoding header.
* @param params.responseExpires - Optional expires header.
* @returns A promise that resolves to the signed URL.
*/
generateSignedUrl(clientName, bucket, params) {
return __awaiter(this, void 0, void 0, function* () {
this.validateBucket(clientName, bucket);
const client = this.getClient(clientName);
const { key, expiresIn = 3600, responseContentDisposition, responseContentType, responseCacheControl, responseContentLanguage, responseContentEncoding, responseExpires, } = params;
const command = new client_s3_1.GetObjectCommand({
Bucket: bucket,
Key: key,
ResponseContentDisposition: responseContentDisposition,
ResponseContentType: responseContentType,
ResponseCacheControl: responseCacheControl,
ResponseContentLanguage: responseContentLanguage,
ResponseContentEncoding: responseContentEncoding,
ResponseExpires: responseExpires,
});
return (0, s3_request_presigner_1.getSignedUrl)(client.s3, command, { expiresIn });
});
}
/**
* Extracts the key from the given URL.
* @param url - The URL from which to extract the key.
* @returns A Promise that resolves to the extracted key.
* @throws An error if the URL is invalid or the key cannot be extracted.
*/
getKeyFromUrl(url) {
return __awaiter(this, void 0, void 0, function* () {
const parsedUrl = new URL(url);
let key;
// Extract the bucket name and key from the hostname or pathname
if (parsedUrl.hostname.includes("s3") && parsedUrl.hostname.endsWith("amazonaws.com")) {
// For virtual-hosted–style URL (with or without region)
const bucketNameEndIndex = parsedUrl.hostname.indexOf(".s3");
const bucketName = parsedUrl.hostname.substring(0, bucketNameEndIndex);
key = decodeURIComponent(parsedUrl.pathname.substring(1)); // Decode and remove leading '/'
return key; // Return key
}
else if (parsedUrl.pathname.includes("/")) {
// For path-style URL
const pathSegments = parsedUrl.pathname.split("/");
if (pathSegments.length > 2) {
// Ensure there's at least a bucket and a key
const bucketName = pathSegments[1]; // Bucket name is the first segment
key = decodeURIComponent(pathSegments.slice(2).join("/")); // Decode and join the rest as the key
return key; // Return key
}
}
throw new Error(`Invalid URL provided. Unable to extract key from ${url}`);
});
}
/**
* Moves a file from one key to another, possibly across buckets, for the given client.
*
* @template ClientName - The name of the client.
* @param clientName - The name of the client.
* @param sourceBucket - The source bucket.
* @param destinationBucket - The destination bucket.
* @param params - The parameters for moving the file.
* @param params.sourceKey - The key of the file to move.
* @param params.destinationKey - The new key for the file.
* @returns A promise that resolves to an object containing the URL and key of the moved file.
*/
moveFile(clientName, sourceBucket, destinationBucket, params) {
return __awaiter(this, void 0, void 0, function* () {
this.validateBucket(clientName, sourceBucket);
this.validateBucket(clientName, destinationBucket);
const client = this.getClient(clientName);
const { sourceKey, destinationKey } = params;
const copyCommand = new client_s3_1.CopyObjectCommand({
Bucket: destinationBucket,
CopySource: `${sourceBucket}/${sourceKey}`,
Key: destinationKey,
});
yield client.s3.send(copyCommand);
yield this.deleteFile(clientName, sourceBucket, { key: sourceKey });
const url = yield this.getUrl(clientName, destinationBucket, destinationKey);
return {
url,
key: destinationKey,
};
});
}
/**
* Deletes a folder and all its contents from the specified bucket.
*
* @template ClientName - The name of the client.
* @param clientName - The name of the client.
* @param bucket - The bucket from which to delete the folder.
* @param params - The parameters for deleting the folder.
* @param params.folder - The folder to delete.
* @returns A promise that resolves when the folder and its contents are successfully deleted.
*/
deleteFolder(clientName, bucket, params) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
this.validateBucket(clientName, bucket);
const client = this.getClient(clientName);
const { folder } = params;
const prefix = `${client.keyPrefix ? `${client.keyPrefix}/` : ""}${folder}/`;
let isTruncated = true;
let continuationToken;
while (isTruncated) {
const command = new client_s3_1.ListObjectsV2Command({
Bucket: bucket,
Prefix: prefix,
ContinuationToken: continuationToken,
});
const res = yield client.s3.send(command);
if (res.Contents && res.Contents.length > 0) {
const deleteCommands = res.Contents.map((item) => ({
Key: item.Key,
}));
const deleteCommand = new client_s3_1.DeleteObjectsCommand({
Bucket: bucket,
Delete: {
Objects: deleteCommands,
Quiet: false,
},
});
yield client.s3.send(deleteCommand);
}
isTruncated = (_a = res.IsTruncated) !== null && _a !== void 0 ? _a : false;
continuationToken = res.NextContinuationToken;
}
});
}
}
exports.S3Pilot = S3Pilot;