133 lines
3.8 KiB
TypeScript
133 lines
3.8 KiB
TypeScript
import { Readable } from "stream";
|
|
|
|
import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
|
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
import { Inject, Injectable, Logger } from "@nestjs/common";
|
|
|
|
import type { IS3Configs } from "src/config/s3.config";
|
|
import { S3_CONFIG } from "../constants";
|
|
import { FileType } from "../interfaces/file.interface";
|
|
import { S3UploadResult } from "../interfaces/s3.interface";
|
|
|
|
@Injectable()
|
|
export class S3Service {
|
|
private readonly logger = new Logger(S3Service.name);
|
|
private readonly s3Client: S3Client;
|
|
|
|
constructor(@Inject(S3_CONFIG) private readonly s3Configs: IS3Configs) {
|
|
this.s3Client = new S3Client({
|
|
region: this.s3Configs.region,
|
|
endpoint: this.s3Configs.endpoint,
|
|
credentials: {
|
|
accessKeyId: this.s3Configs.accessKeyId,
|
|
secretAccessKey: this.s3Configs.secretAccessKey,
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Upload file to S3
|
|
*/
|
|
async uploadFile(buffer: Buffer, key: string, contentType: string, metadata?: Record<string, string>): Promise<S3UploadResult> {
|
|
try {
|
|
const command = new PutObjectCommand({
|
|
Bucket: this.s3Configs.bucketName,
|
|
Key: key,
|
|
Body: buffer,
|
|
ACL: "public-read",
|
|
ContentType: contentType,
|
|
Metadata: metadata,
|
|
});
|
|
|
|
await this.s3Client.send(command);
|
|
|
|
const url = `${this.s3Configs.bucketUploadedUrl}/${key}`;
|
|
|
|
this.logger.log(`File uploaded to S3: ${key}`);
|
|
|
|
return {
|
|
key,
|
|
url,
|
|
bucket: this.s3Configs.bucketName,
|
|
};
|
|
} catch (error: unknown) {
|
|
this.logger.error(`Failed to upload file to S3: ${(error as Error).message}`, (error as Error).stack);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get file stream from S3
|
|
*/
|
|
async getFileStream(key: string): Promise<Readable> {
|
|
try {
|
|
const command = new GetObjectCommand({
|
|
Bucket: this.s3Configs.bucketName,
|
|
Key: key,
|
|
});
|
|
|
|
const response = await this.s3Client.send(command);
|
|
return response.Body as Readable;
|
|
} catch (error: unknown) {
|
|
this.logger.error(`Failed to get file from S3: ${(error as Error).message}`, (error as Error).stack);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate presigned URL for file download
|
|
*/
|
|
async getPresignedUrl(key: string, expiresIn: number = 3600): Promise<string> {
|
|
try {
|
|
const command = new GetObjectCommand({
|
|
Bucket: this.s3Configs.bucketName,
|
|
Key: key,
|
|
});
|
|
|
|
return await getSignedUrl(this.s3Client, command, { expiresIn });
|
|
} catch (error: unknown) {
|
|
this.logger.error(`Failed to generate presigned URL: ${(error as Error).message}`, (error as Error).stack);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete file from S3
|
|
*/
|
|
async deleteFile(key: string): Promise<void> {
|
|
try {
|
|
const command = new DeleteObjectCommand({
|
|
Bucket: this.s3Configs.bucketName,
|
|
Key: key,
|
|
});
|
|
|
|
await this.s3Client.send(command);
|
|
this.logger.log(`File deleted from S3: ${key}`);
|
|
} catch (error: unknown) {
|
|
this.logger.error(`Failed to delete file from S3: ${(error as Error).message}`, (error as Error).stack);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate S3 key for file
|
|
*/
|
|
generateFileKey(originalName: string, fileType: FileType): string {
|
|
const timestamp = Date.now();
|
|
const randomSuffix = Math.round(Math.random() * 1e9);
|
|
const extension = originalName.split(".").pop();
|
|
|
|
const basePathMap: Record<FileType, string> = {
|
|
image: "images",
|
|
video: "videos",
|
|
audio: "audio",
|
|
document: "documents",
|
|
text: "texts",
|
|
unknown: "unknowns",
|
|
};
|
|
|
|
const basePath = basePathMap[fileType];
|
|
return `${basePath}/${timestamp}-${randomSuffix}.${extension}`;
|
|
}
|
|
}
|