refactor(file-handling): replace multer with fastify multipart for file uploads

This commit removes the dependency on multer and replaces it with fastify's built-in multipart handling for file uploads. The changes include:
- Removing multer and related dependencies from package.json.
- Refactoring logo and avatar services to handle base64 encoded images directly.
- Updating docker-compose configurations to reflect changes in file storage.
- Adding new documentation for file upload processes.

The refactor simplifies the file upload process, reduces dependencies, and improves maintainability.
This commit is contained in:
Daniel Luiz Alves
2025-03-31 17:24:22 -03:00
parent 35d1c2d697
commit 44ce5b1f37
16 changed files with 212 additions and 531 deletions

View File

@@ -2,41 +2,6 @@ import { AvatarService } from "./avatar.service";
import { UpdateUserSchema, createRegisterUserSchema } from "./dto";
import { UserService } from "./service";
import { FastifyReply, FastifyRequest } from "fastify";
import multer from 'multer';
import path from 'path';
import { Request, Response } from 'express';
import fs from 'fs';
const uploadsDir = "/app/uploads/avatars";
if (!fs.existsSync(uploadsDir)) {
fs.mkdirSync(uploadsDir, { recursive: true });
}
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, uploadsDir);
},
filename: function (req, file, cb) {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
const ext = path.extname(file.originalname);
cb(null, `${uniqueSuffix}${ext}`);
}
});
const fileFilter = (req: any, file: any, cb: any) => {
if (!file.mimetype.startsWith('image/')) {
return cb(new Error('Only images are allowed'));
}
cb(null, true);
};
const upload = multer({
storage: storage,
fileFilter: fileFilter,
limits: {
fileSize: 5 * 1024 * 1024
}
}).single('file');
export class UserController {
private userService = new UserService();
@@ -125,51 +90,30 @@ export class UserController {
}
async uploadAvatar(request: FastifyRequest, reply: FastifyReply) {
if (!request.isMultipart()) {
return reply.status(400).send({ error: "Request must be multipart/form-data" });
try {
const userId = (request as any).user?.userId;
if (!userId) {
return reply.status(401).send({ error: "Unauthorized" });
}
const file = await request.file();
if (!file) {
return reply.status(400).send({ error: "No file uploaded" });
}
if (!file.mimetype.startsWith('image/')) {
return reply.status(400).send({ error: "Only images are allowed" });
}
const buffer = await file.toBuffer();
const base64Image = await this.avatarService.uploadAvatar(buffer);
const updatedUser = await this.userService.updateUserImage(userId, base64Image);
return reply.send(updatedUser);
} catch (error: any) {
console.error("Upload error:", error);
return reply.status(400).send({ error: error.message });
}
const userId = (request as any).user?.userId;
if (!userId) {
return reply.status(401).send({ error: "Unauthorized" });
}
return new Promise((resolve) => {
const rawRequest = request.raw;
rawRequest.headers = request.headers;
upload(rawRequest as Request, reply.raw as Response, async (err) => {
if (err) {
console.error("Upload error:", err);
if (err.code === 'LIMIT_FILE_SIZE') {
return resolve(reply.status(400).send({ error: "File size cannot be larger than 5MB" }));
}
return resolve(reply.status(400).send({ error: err.message }));
}
const file = (rawRequest as any).file;
if (!file) {
return resolve(reply.status(400).send({ error: "No file uploaded" }));
}
try {
const imageUrl = await this.avatarService.uploadAvatar(userId, file.path);
const updatedUser = await this.userService.updateUserImage(userId, imageUrl);
resolve(reply.send(updatedUser));
} catch (error: any) {
try {
if (fs.existsSync(file.path)) {
await fs.promises.unlink(file.path);
}
} catch (cleanupError) {
console.error("Error cleaning up temp file:", cleanupError);
}
console.error("Upload error:", error);
resolve(reply.status(400).send({ error: error.message }));
}
});
});
}
async removeAvatar(request: FastifyRequest, reply: FastifyReply) {
@@ -179,14 +123,9 @@ export class UserController {
return reply.status(401).send({ error: "Unauthorized" });
}
const user = await this.userService.getUserById(userId);
if (user.image) {
await this.avatarService.deleteAvatar(user.image);
const updatedUser = await this.userService.updateUserImage(userId, null);
return reply.send(updatedUser);
}
return reply.send(user);
await this.avatarService.deleteAvatar(userId);
const updatedUser = await this.userService.getUserById(userId);
return reply.send(updatedUser);
} catch (error: any) {
return reply.status(400).send({ error: error.message });
}