s3-orm
Version:
Object-Relational Mapping (ORM) interface for Amazon S3, enabling model-based data operations with indexing and querying capabilities
277 lines (234 loc) • 8.02 kB
text/typescript
import {map} from "lodash";
import {
S3Client,
ListObjectsV2Command,
ListObjectsV2CommandOutput,
HeadObjectCommand,
GetObjectCommand,
GetObjectAclCommand,
PutObjectAclCommand,
DeleteObjectCommand,
DeleteObjectsCommand,
_Object as S3Object,
GetObjectCommandOutput,
ObjectIdentifier,
ObjectCannedACL
} from "@aws-sdk/client-s3";
import { Upload } from "@aws-sdk/lib-storage";
import { Readable } from 'stream';
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import AuthError from "../errors/AuthError";
export type S3Options = {
bucket: string;
prefix: string;
region?: string;
rootUrl?: string;
acl?: ObjectCannedACL;
accessKeyId: string;
secretAccessKey: string;
sessionToken?: string; // Optional, for temporary credentials
}
/**
* https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/s3/
* Class to simplify working with Amazon services
*/
export class S3Helper {
opts: S3Options;
authenticated: boolean = false;
s3: S3Client;
/**
*
* @param {*} opts {bucket,region,baseUrl,accessKeyId,secretAccessKey }
*/
constructor(opts: S3Options | S3Client){
if (!opts){
throw new Error('You must pass configuration settings!');
}
// If we have an S3Client instance, use it directly
if (opts instanceof S3Client) {
this.s3 = opts;
}
else {
// Make sure we have the settings we need
if (!opts.bucket){
throw new Error('No AWS Bucket specified!')
}
opts.region = (opts.region) ? opts.region : "us-east-1";
opts.rootUrl = (opts.rootUrl) ? opts.rootUrl : `https://${opts.bucket}.s3.amazonaws.com`;
opts.acl = (opts && opts.acl) ? opts.acl as ObjectCannedACL : 'private';
this.opts = opts;
this.authenticated = false;
const clientConfig: any = {
region: opts.region
};
// If we have the credentials, try to authenticate
if (opts.accessKeyId && opts.secretAccessKey){
clientConfig.credentials = {
accessKeyId: opts.accessKeyId,
secretAccessKey: opts.secretAccessKey,
sessionToken: (opts.sessionToken) ? opts.sessionToken : null // Optional, for temporary credentials
};
this.authenticated = true;
}
this.s3 = new S3Client(clientConfig);
}
}
getBucket(): string { return this.opts.bucket }
getRegion(): string { return this.opts.region }
getUrl(key: string): string {
key = key.replace(/^\//, '');
return `${this.opts.rootUrl}/${key}`
}
async _read(command: any): Promise<any> {
try {
const response = await this.s3.send(command);
return response;
} catch (err) {
//Logger.error('S3 read error:', err);
throw err;
}
}
async _write(command: any): Promise<any> {
if (!this.authenticated){
throw new AuthError(`You need to be authenticated for this operation`);
}
try {
const response = await this.s3.send(command);
return response;
} catch (err) {
//Logger.error('S3 write error:', err);
throw err;
}
}
/**
* @see https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/s3/command/ListObjectsV2Command/
*/
async list(directoryKey: string): Promise<S3Object[]> {
const command = new ListObjectsV2Command({
Delimiter: '/',
Prefix: directoryKey,
Bucket: this.opts.bucket,
});
const response = await this._read(command) as ListObjectsV2CommandOutput;
return response.Contents || [];
}
/**
* Check that a file exists
*/
async exists(key: string): Promise<boolean> {
const command = new HeadObjectCommand({
Bucket: this.opts.bucket,
Key: key
});
try {
await this._read(command);
return true;
} catch(err) {
return false;
}
}
/**
* Get a file
*/
async get(key: string): Promise<string> {
const command = new GetObjectCommand({
Bucket: this.opts.bucket,
Key: key
});
const response = await this._read(command) as GetObjectCommandOutput;
const streamToString = async (stream: Readable): Promise<string> => {
return new Promise((resolve, reject) => {
const chunks: Buffer[] = [];
stream.on("data", (chunk: Buffer) => chunks.push(chunk));
stream.on("error", reject);
stream.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
});
};
if (response.Body instanceof Readable) {
return await streamToString(response.Body);
}
throw new Error('Invalid response body type');
}
async getObjectACL(key: string): Promise<any> {
const command = new GetObjectAclCommand({
Bucket: this.opts.bucket,
Key: key
});
const response = await this._read(command);
return response.Grants;
}
/**
* Get a signed URL to a resource on S3
*/
async getSignedUrl(key: string): Promise<string> {
const command = new GetObjectCommand({
Bucket: this.opts.bucket,
Key: key
});
return await getSignedUrl(this.s3, command, { expiresIn: 3600 });
}
/**
* @param {string} key
* @param {ObjectCannedACL} acl private | public-read | public-read-write | authenticated-read | aws-exec-read | bucket-owner-read | bucket-owner-full-control
*/
async setObjectACL(key: string, acl: ObjectCannedACL): Promise<any> {
const command = new PutObjectAclCommand({
Bucket: this.opts.bucket,
Key: key,
ACL: acl
});
return await this._write(command);
}
/**
* Upload a file to AWS
* @param {string} content The string content
* @param {string} key The s3 key (e.g. the path on S3)
* @param {string} contentType Content type of the file
*/
async uploadString(content: string, key: string, contentType?: string): Promise<string> {
if (!this.authenticated){
throw new AuthError(`You need to be authenticated to call uploadString!`);
}
contentType = contentType || 'text/plain';
key = key.replace(/^\/|\/$/g, '');
const upload = new Upload({
client: this.s3,
params: {
Bucket: this.opts.bucket,
Key: key,
Body: content,
ContentType: contentType,
ACL: this.opts.acl as ObjectCannedACL
}
});
await upload.done();
return this.getUrl(key);
}
/**
* Delete a file on S3
*/
async delete(key: string): Promise<any> {
const command = new DeleteObjectCommand({
Bucket: this.opts.bucket,
Key: key
});
return await this._write(command);
}
/**
* Delete multiple objects from s3
* @param {S3Object[]} items Array of S3 objects from list() command
*/
async deleteAll(items: S3Object[]): Promise<any> {
const cleaned: ObjectIdentifier[] = map(items, (item) => {
const obj: ObjectIdentifier = { Key: item.Key || '' };
return obj;
});
const command = new DeleteObjectsCommand({
Bucket: this.opts.bucket,
Delete: {
Objects: cleaned
}
});
return await this._write(command);
}
}