mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-16 17:51:43 +10:00
refactor(v4.0.0-alpha): beginning of a new era
This commit is contained in:
32
apps/server/src/storage/storage.controller.ts
Normal file
32
apps/server/src/storage/storage.controller.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Controller,
|
||||
Put,
|
||||
UploadedFile,
|
||||
UseGuards,
|
||||
UseInterceptors,
|
||||
} from "@nestjs/common";
|
||||
import { FileInterceptor } from "@nestjs/platform-express";
|
||||
|
||||
import { TwoFactorGuard } from "@/server/auth/guards/two-factor.guard";
|
||||
import { User } from "@/server/user/decorators/user.decorator";
|
||||
|
||||
import { StorageService } from "./storage.service";
|
||||
|
||||
@Controller("storage")
|
||||
export class StorageController {
|
||||
constructor(private readonly storageService: StorageService) {}
|
||||
|
||||
@Put("image")
|
||||
@UseGuards(TwoFactorGuard)
|
||||
@UseInterceptors(FileInterceptor("file"))
|
||||
async uploadFile(@User("id") userId: string, @UploadedFile("file") file: Express.Multer.File) {
|
||||
if (!file.mimetype.startsWith("image")) {
|
||||
throw new BadRequestException(
|
||||
"The file you uploaded doesn't seem to be an image, please upload a file that ends in .jp(e)g or .png.",
|
||||
);
|
||||
}
|
||||
|
||||
return this.storageService.uploadObject(userId, "pictures", file.buffer, userId);
|
||||
}
|
||||
}
|
||||
28
apps/server/src/storage/storage.module.ts
Normal file
28
apps/server/src/storage/storage.module.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import type {} from "multer";
|
||||
import { MinioModule } from "nestjs-minio-client";
|
||||
|
||||
import { Config } from "../config/schema";
|
||||
import { StorageController } from "./storage.controller";
|
||||
import { StorageService } from "./storage.service";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
MinioModule.registerAsync({
|
||||
inject: [ConfigService],
|
||||
useFactory: (configService: ConfigService<Config>) => ({
|
||||
useSSL: false,
|
||||
endPoint: configService.getOrThrow<string>("STORAGE_ENDPOINT"),
|
||||
port: configService.getOrThrow<number>("STORAGE_PORT"),
|
||||
region: configService.get<string>("STORAGE_REGION"),
|
||||
accessKey: configService.getOrThrow<string>("STORAGE_ACCESS_KEY"),
|
||||
secretKey: configService.getOrThrow<string>("STORAGE_SECRET_KEY"),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
controllers: [StorageController],
|
||||
providers: [StorageService],
|
||||
exports: [StorageService],
|
||||
})
|
||||
export class StorageModule {}
|
||||
170
apps/server/src/storage/storage.service.ts
Normal file
170
apps/server/src/storage/storage.service.ts
Normal file
@ -0,0 +1,170 @@
|
||||
import { CACHE_MANAGER } from "@nestjs/cache-manager";
|
||||
import {
|
||||
Inject,
|
||||
Injectable,
|
||||
InternalServerErrorException,
|
||||
Logger,
|
||||
OnModuleInit,
|
||||
} from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { Cache } from "cache-manager";
|
||||
import { Client } from "minio";
|
||||
import { MinioService } from "nestjs-minio-client";
|
||||
import sharp from "sharp";
|
||||
|
||||
import { Config } from "../config/schema";
|
||||
|
||||
// Objects are stored under the following path in the bucket:
|
||||
// "<bucketName>/<userId>/<type>/<fileName>",
|
||||
// where `userId` is a unique identifier (cuid) for the user,
|
||||
// where `type` can either be "pictures", "previews" or "resumes",
|
||||
// and where `fileName` is a unique identifier (cuid) for the file.
|
||||
|
||||
type ImageUploadType = "pictures" | "previews";
|
||||
type DocumentUploadType = "resumes";
|
||||
type UploadType = ImageUploadType | DocumentUploadType;
|
||||
|
||||
const PUBLIC_ACCESS_POLICY = {
|
||||
Version: "2012-10-17",
|
||||
Statement: [
|
||||
{
|
||||
Sid: "PublicAccess",
|
||||
Effect: "Allow",
|
||||
Action: ["s3:GetObject"],
|
||||
Principal: { AWS: ["*"] },
|
||||
Resource: [
|
||||
"arn:aws:s3:::{{bucketName}}/*/pictures/*",
|
||||
"arn:aws:s3:::{{bucketName}}/*/previews/*",
|
||||
"arn:aws:s3:::{{bucketName}}/*/resumes/*",
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class StorageService implements OnModuleInit {
|
||||
private readonly logger = new Logger(StorageService.name);
|
||||
|
||||
private client: Client;
|
||||
private bucketName: string;
|
||||
|
||||
constructor(
|
||||
private readonly configService: ConfigService<Config>,
|
||||
private readonly minioService: MinioService,
|
||||
@Inject(CACHE_MANAGER) private readonly cache: Cache,
|
||||
) {}
|
||||
|
||||
async onModuleInit() {
|
||||
this.client = this.minioService.client;
|
||||
this.bucketName = this.configService.getOrThrow<string>("STORAGE_BUCKET");
|
||||
|
||||
try {
|
||||
// Create a storage bucket if it doesn't exist
|
||||
// if it exists, log that we were able to connect to the storage service
|
||||
const bucketExists = await this.client.bucketExists(this.bucketName);
|
||||
|
||||
if (!bucketExists) {
|
||||
const bucketPolicy = JSON.stringify(PUBLIC_ACCESS_POLICY).replace(
|
||||
/{{bucketName}}/g,
|
||||
this.bucketName,
|
||||
);
|
||||
|
||||
await this.client.makeBucket(this.bucketName);
|
||||
await this.client.setBucketPolicy(this.bucketName, bucketPolicy);
|
||||
|
||||
this.logger.log(
|
||||
"A new storage bucket has been created and the policy has been applied successfully.",
|
||||
);
|
||||
} else {
|
||||
this.logger.log("Successfully connected to the storage service.");
|
||||
}
|
||||
} catch (error) {
|
||||
throw new InternalServerErrorException(
|
||||
"There was an error while creating the storage bucket.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async bucketExists() {
|
||||
const exists = await this.client.bucketExists(this.bucketName);
|
||||
|
||||
if (!exists) {
|
||||
throw new InternalServerErrorException(
|
||||
"There was an error while checking if the storage bucket exists.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async uploadObject(
|
||||
userId: string,
|
||||
type: UploadType,
|
||||
buffer: Buffer,
|
||||
filename: string = createId(),
|
||||
) {
|
||||
const extension = type === "resumes" ? "pdf" : "jpg";
|
||||
const storageUrl = this.configService.get<string>("STORAGE_URL");
|
||||
const filepath = `${userId}/${type}/${filename}.${extension}`;
|
||||
const url = `${storageUrl}/${this.bucketName}/${filepath}`;
|
||||
const metadata =
|
||||
extension === "jpg"
|
||||
? { "Content-Type": "image/jpeg" }
|
||||
: {
|
||||
"Content-Type": "application/pdf",
|
||||
"Content-Disposition": `attachment; filename=${filename}.${extension}`,
|
||||
};
|
||||
|
||||
try {
|
||||
if (extension === "jpg") {
|
||||
// If the uploaded file is an image, use sharp to resize the image to a maximum width/height of 600px
|
||||
buffer = await sharp(buffer)
|
||||
.resize({ width: 600, height: 600, fit: sharp.fit.outside })
|
||||
.jpeg({ quality: 80 })
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
this.client.putObject(this.bucketName, filepath, buffer, metadata),
|
||||
this.cache.set(`user:${userId}:storage:${type}:${filename}`, url),
|
||||
]);
|
||||
|
||||
return url;
|
||||
} catch (error) {
|
||||
throw new InternalServerErrorException("There was an error while uploading the file.");
|
||||
}
|
||||
}
|
||||
|
||||
async deleteObject(userId: string, type: UploadType, filename: string) {
|
||||
const extension = type === "resumes" ? "pdf" : "jpg";
|
||||
const path = `${userId}/${type}/${filename}.${extension}`;
|
||||
|
||||
try {
|
||||
return Promise.all([
|
||||
this.cache.del(`user:${userId}:storage:${type}:${filename}`),
|
||||
this.client.removeObject(this.bucketName, path),
|
||||
]);
|
||||
} catch (error) {
|
||||
throw new InternalServerErrorException(
|
||||
`There was an error while deleting the document at the specified path: ${path}.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteFolder(prefix: string) {
|
||||
const objectsList = [];
|
||||
|
||||
const objectsStream = this.client.listObjectsV2(this.bucketName, prefix, true);
|
||||
|
||||
for await (const object of objectsStream) {
|
||||
objectsList.push(object.name);
|
||||
}
|
||||
|
||||
try {
|
||||
return this.client.removeObjects(this.bucketName, objectsList);
|
||||
} catch (error) {
|
||||
throw new InternalServerErrorException(
|
||||
`There was an error while deleting the folder at the specified path: ${this.bucketName}/${prefix}.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user