Introduce `getPublicUrl` in `S3Service` for generating public URLs. Replace custom file URL generation logic across services with the new method. Add media controller for file streaming and update related tests. Adjust frontend to display user roles instead of email in the sidebar. Update environment schema to include optional `API_URL`. Fix help page contact email.
175 lines
4.6 KiB
TypeScript
175 lines
4.6 KiB
TypeScript
import { Injectable, Logger, OnModuleInit } from "@nestjs/common";
|
|
import { ConfigService } from "@nestjs/config";
|
|
import * as Minio from "minio";
|
|
import type { IStorageService } from "../common/interfaces/storage.interface";
|
|
|
|
@Injectable()
|
|
export class S3Service implements OnModuleInit, IStorageService {
|
|
private readonly logger = new Logger(S3Service.name);
|
|
private minioClient: Minio.Client;
|
|
private readonly bucketName: string;
|
|
|
|
constructor(private readonly configService: ConfigService) {
|
|
this.minioClient = new Minio.Client({
|
|
endPoint: this.configService.get<string>("S3_ENDPOINT", "localhost"),
|
|
port: Number.parseInt(this.configService.get<string>("S3_PORT", "9000"), 10),
|
|
useSSL: this.configService.get<string>("S3_USE_SSL") === "true",
|
|
accessKey: this.configService.get<string>("S3_ACCESS_KEY", "minioadmin"),
|
|
secretKey: this.configService.get<string>("S3_SECRET_KEY", "minioadmin"),
|
|
});
|
|
this.bucketName = this.configService.get<string>(
|
|
"S3_BUCKET_NAME",
|
|
"memegoat",
|
|
);
|
|
}
|
|
|
|
async onModuleInit() {
|
|
await this.ensureBucketExists(this.bucketName);
|
|
}
|
|
|
|
async ensureBucketExists(bucketName: string) {
|
|
try {
|
|
const exists = await this.minioClient.bucketExists(bucketName);
|
|
if (!exists) {
|
|
await this.minioClient.makeBucket(bucketName);
|
|
this.logger.log(`Bucket "${bucketName}" created successfully.`);
|
|
}
|
|
} catch (error) {
|
|
this.logger.error(
|
|
`Error checking/creating bucket "${bucketName}": ${error.message}`,
|
|
);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async uploadFile(
|
|
fileName: string,
|
|
file: Buffer,
|
|
mimeType: string,
|
|
metaData: Minio.ItemBucketMetadata = {},
|
|
bucketName: string = this.bucketName,
|
|
) {
|
|
try {
|
|
await this.minioClient.putObject(bucketName, fileName, file, file.length, {
|
|
...metaData,
|
|
"Content-Type": mimeType,
|
|
});
|
|
return fileName;
|
|
} catch (error) {
|
|
this.logger.error(`Error uploading file to ${bucketName}: ${error.message}`);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async getFile(fileName: string, bucketName: string = this.bucketName) {
|
|
try {
|
|
return await this.minioClient.getObject(bucketName, fileName);
|
|
} catch (error) {
|
|
this.logger.error(`Error getting file from ${bucketName}: ${error.message}`);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async getFileUrl(
|
|
fileName: string,
|
|
expiry = 3600,
|
|
bucketName: string = this.bucketName,
|
|
) {
|
|
try {
|
|
return await this.minioClient.presignedUrl(
|
|
"GET",
|
|
bucketName,
|
|
fileName,
|
|
expiry,
|
|
);
|
|
} catch (error) {
|
|
this.logger.error(
|
|
`Error getting file URL from ${bucketName}: ${error.message}`,
|
|
);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async getUploadUrl(
|
|
fileName: string,
|
|
expiry = 3600,
|
|
bucketName: string = this.bucketName,
|
|
) {
|
|
try {
|
|
return await this.minioClient.presignedUrl(
|
|
"PUT",
|
|
bucketName,
|
|
fileName,
|
|
expiry,
|
|
);
|
|
} catch (error) {
|
|
this.logger.error(
|
|
`Error getting upload URL for ${bucketName}: ${error.message}`,
|
|
);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async deleteFile(fileName: string, bucketName: string = this.bucketName) {
|
|
try {
|
|
await this.minioClient.removeObject(bucketName, fileName);
|
|
} catch (error) {
|
|
this.logger.error(
|
|
`Error deleting file from ${bucketName}: ${error.message}`,
|
|
);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async getFileInfo(fileName: string, bucketName: string = this.bucketName) {
|
|
try {
|
|
return await this.minioClient.statObject(bucketName, fileName);
|
|
} catch (error) {
|
|
this.logger.error(
|
|
`Error getting file info from ${bucketName}: ${error.message}`,
|
|
);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async moveFile(
|
|
sourceFileName: string,
|
|
destinationFileName: string,
|
|
sourceBucketName: string = this.bucketName,
|
|
destinationBucketName: string = this.bucketName,
|
|
) {
|
|
try {
|
|
const conds = new Minio.CopyConditions();
|
|
await this.minioClient.copyObject(
|
|
destinationBucketName,
|
|
destinationFileName,
|
|
`/${sourceBucketName}/${sourceFileName}`,
|
|
conds,
|
|
);
|
|
await this.minioClient.removeObject(sourceBucketName, sourceFileName);
|
|
return destinationFileName;
|
|
} catch (error) {
|
|
this.logger.error(
|
|
`Error moving file from ${sourceBucketName}/${sourceFileName} to ${destinationBucketName}/${destinationFileName}: ${error.message}`,
|
|
);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
getPublicUrl(storageKey: string): string {
|
|
const apiUrl = this.configService.get<string>("API_URL");
|
|
if (apiUrl) {
|
|
return `${apiUrl.replace(/\/$/, "")}/media/${storageKey}`;
|
|
}
|
|
|
|
const domain = this.configService.get<string>("DOMAIN_NAME", "localhost");
|
|
const port = this.configService.get<number>("PORT", 3000);
|
|
|
|
if (domain === "localhost" || domain === "127.0.0.1") {
|
|
return `http://${domain}:${port}/media/${storageKey}`;
|
|
}
|
|
|
|
return `https://api.${domain}/media/${storageKey}`;
|
|
}
|
|
}
|