Files
twenty/packages/twenty-server/src/engine/integrations/file-storage/drivers/s3.driver.ts
martmull 7e03419c16 Serverless function improvements (#6769)
- add layer for lambda execution
- add layer for local execution
- add package resolve for the monaco editor
- add route to get installed package for serverless functions
- add layer versioning
2024-09-02 15:25:20 +02:00

299 lines
7.2 KiB
TypeScript

import { Readable } from 'stream';
import {
CopyObjectCommand,
CreateBucketCommandInput,
DeleteObjectCommand,
DeleteObjectsCommand,
GetObjectCommand,
HeadBucketCommandInput,
HeadObjectCommand,
ListObjectsV2Command,
NotFound,
PutObjectCommand,
S3,
S3ClientConfig,
} from '@aws-sdk/client-s3';
import {
FileStorageException,
FileStorageExceptionCode,
} from 'src/engine/integrations/file-storage/interfaces/file-storage-exception';
import { isDefined } from 'src/utils/is-defined';
import { StorageDriver } from './interfaces/storage-driver.interface';
export interface S3DriverOptions extends S3ClientConfig {
bucketName: string;
endpoint?: string;
region: string;
}
export class S3Driver implements StorageDriver {
private s3Client: S3;
private bucketName: string;
constructor(options: S3DriverOptions) {
const { bucketName, region, endpoint, ...s3Options } = options;
if (!bucketName || !region) {
return;
}
this.s3Client = new S3({ ...s3Options, region, endpoint });
this.bucketName = bucketName;
}
public get client(): S3 {
return this.s3Client;
}
async write(params: {
file: Buffer | Uint8Array | string;
name: string;
folder: string;
mimeType: string | undefined;
}): Promise<void> {
const command = new PutObjectCommand({
Key: `${params.folder}/${params.name}`,
Body: params.file,
ContentType: params.mimeType,
Bucket: this.bucketName,
});
await this.s3Client.send(command);
}
private async emptyS3Directory(folderPath) {
const listParams = {
Bucket: this.bucketName,
Prefix: folderPath,
};
const listObjectsCommand = new ListObjectsV2Command(listParams);
const listedObjects = await this.s3Client.send(listObjectsCommand);
if (listedObjects.Contents?.length === 0) return;
const deleteParams = {
Bucket: this.bucketName,
Delete: {
Objects: listedObjects.Contents?.map(({ Key }) => {
return { Key };
}),
},
};
const deleteObjectCommand = new DeleteObjectsCommand(deleteParams);
await this.s3Client.send(deleteObjectCommand);
if (listedObjects.IsTruncated) {
await this.emptyS3Directory(folderPath);
}
}
async delete(params: {
folderPath: string;
filename?: string;
}): Promise<void> {
if (params.filename) {
const deleteCommand = new DeleteObjectCommand({
Key: `${params.folderPath}/${params.filename}`,
Bucket: this.bucketName,
});
await this.s3Client.send(deleteCommand);
} else {
await this.emptyS3Directory(params.folderPath);
const deleteEmptyFolderCommand = new DeleteObjectCommand({
Key: `${params.folderPath}`,
Bucket: this.bucketName,
});
await this.s3Client.send(deleteEmptyFolderCommand);
}
}
async read(params: {
folderPath: string;
filename: string;
}): Promise<Readable> {
const command = new GetObjectCommand({
Key: `${params.folderPath}/${params.filename}`,
Bucket: this.bucketName,
});
try {
const file = await this.s3Client.send(command);
if (!file || !file.Body || !(file.Body instanceof Readable)) {
throw new Error('Unable to get file stream');
}
return Readable.from(file.Body);
} catch (error) {
if (error.name === 'NoSuchKey') {
throw new FileStorageException(
'File not found',
FileStorageExceptionCode.FILE_NOT_FOUND,
);
}
throw error;
}
}
async move(params: {
from: { folderPath: string; filename: string };
to: { folderPath: string; filename: string };
}): Promise<void> {
const fromKey = `${params.from.folderPath}/${params.from.filename}`;
const toKey = `${params.to.folderPath}/${params.to.filename}`;
try {
// Check if the source file exists
await this.s3Client.send(
new HeadObjectCommand({
Bucket: this.bucketName,
Key: fromKey,
}),
);
// Copy the object to the new location
await this.s3Client.send(
new CopyObjectCommand({
CopySource: `${this.bucketName}/${fromKey}`,
Bucket: this.bucketName,
Key: toKey,
}),
);
// Delete the original object
await this.s3Client.send(
new DeleteObjectCommand({
Bucket: this.bucketName,
Key: fromKey,
}),
);
} catch (error) {
if (error.name === 'NotFound') {
throw new FileStorageException(
'File not found',
FileStorageExceptionCode.FILE_NOT_FOUND,
);
}
// For other errors, throw the original error
throw error;
}
}
async copy(params: {
from: { folderPath: string; filename?: string };
to: { folderPath: string; filename?: string };
}): Promise<void> {
if (!params.from.filename && params.to.filename) {
throw new Error('Cannot copy folder to file');
}
const fromKey = `${params.from.folderPath}/${params.from.filename || ''}`;
const toKey = `${params.to.folderPath}/${params.to.filename || ''}`;
if (isDefined(params.from.filename)) {
try {
// Check if the source file exists
await this.s3Client.send(
new HeadObjectCommand({
Bucket: this.bucketName,
Key: fromKey,
}),
);
// Copy the object to the new location
await this.s3Client.send(
new CopyObjectCommand({
CopySource: `${this.bucketName}/${fromKey}`,
Bucket: this.bucketName,
Key: toKey,
}),
);
return;
} catch (error) {
if (error.name === 'NotFound') {
throw new FileStorageException(
'File not found',
FileStorageExceptionCode.FILE_NOT_FOUND,
);
}
// For other errors, throw the original error
throw error;
}
}
const listedObjects = await this.s3Client.send(
new ListObjectsV2Command({
Bucket: this.bucketName,
Prefix: fromKey,
}),
);
if (!listedObjects.Contents || listedObjects.Contents.length === 0) {
throw new Error('No objects found in the source folder.');
}
for (const object of listedObjects.Contents) {
const match = object.Key?.match(/(.*)\/(.*)/);
if (!isDefined(match)) {
continue;
}
const fromFolderPath = match[1];
const filename = match[2];
const toFolderPath = fromFolderPath.replace(
params.from.folderPath,
params.to.folderPath,
);
if (!isDefined(toFolderPath)) {
continue;
}
await this.copy({
from: {
folderPath: fromFolderPath,
filename,
},
to: { folderPath: toFolderPath, filename },
});
}
}
async checkBucketExists(args: HeadBucketCommandInput) {
try {
await this.s3Client.headBucket(args);
return true;
} catch (error) {
if (error instanceof NotFound) {
return false;
}
throw error;
}
}
async createBucket(args: CreateBucketCommandInput) {
const exist = await this.checkBucketExists({
Bucket: args.Bucket,
});
if (exist) {
return;
}
return this.s3Client.createBucket(args);
}
}