Commit 4cd74144 authored by Phạm Quang Bảo's avatar Phạm Quang Bảo

fix: save file in local and add api upload management

parent cfeeda3b
import { v2 as cloudinary } from "cloudinary";
import "dotenv/config";
cloudinary.config({
cloud_name: process.env.CLOUD_NAME! as string,
api_key: process.env.API_KEY! as string,
api_secret: process.env.API_SECRET! as string,
});
export default cloudinary;
\ No newline at end of file
import multer from "multer";
import path from "node:path";
// save in memory
const memoryStorage = multer.memoryStorage();
const upload = multer({
const uploadMemory = multer({
storage: memoryStorage,
limits: {
fileSize: 2 * 1024 * 1024, // 2 MB
fileSize: 5 * 1024 * 1024, // 5 MB
},
});
export default upload;
\ No newline at end of file
// save in disk
const diskStorage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, 'uploads/');
},
filename: function (req, file, cb) {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
const ext = path.extname(file.originalname);
cb(null, file.fieldname + '-' + uniqueSuffix + ext);
}
});
const uploadDisk = multer({
storage: diskStorage,
limits: {
fileSize: 5 * 1024 * 1024, // 5 MB
},
});
export { uploadMemory, uploadDisk };
\ No newline at end of file
import { Application } from "express";
import { Readable } from 'stream';
import { Resource } from "express-automatic-routes";
import upload from "#config/multer.config";
import { uploadMemory } from "#config/multer.config";
import csv from 'csv-parser';
import { Req, Res } from "#interfaces/IApi";
import { CoursesProvider } from "#providers/CoursesProvider";
......@@ -29,7 +29,7 @@ export default (_express: Application) => {
* file:
* type: string
* format: binary
* description: File CSV (Giới hạn dưới 2MB)
* description: File CSV (Giới hạn dưới 5MB)
* responses:
* 201:
* description: Tạo khóa học mới thành công
......@@ -39,7 +39,7 @@ export default (_express: Application) => {
* $ref: "#/components/schemas/CourseResponse"
*/
post: {
middleware: [upload.single('file')],
middleware: [uploadMemory.single('file')],
handler: async (req: Req, res: Res) => {
try {
......
......@@ -3,13 +3,53 @@ import { Application } from "express";
import { Resource } from "express-automatic-routes";
import queryModifier from "#middlewares/request";
import { authMiddleware } from "#middlewares/authentication";
import upload from "#config/multer.config";
import { UploadService } from "#services/uploadService";
import { uploadDisk } from "#config/multer.config";
import { UploadProvider } from "#providers/UploadProvider";
export default (_express: Application) => {
const uploadService = new UploadService();
const uploadProvider = new UploadProvider();
return <Resource>{
/**
* @openapi
* /api/v1.0/upload:
* get:
* tags: [Upload]
* security:
* - bearerAuth: []
* description: Lấy danh sách file ảnh.
* responses:
* 200:
* description: Lấy danh sách file ảnh thành công
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/getResponse"
*/
get: {
middleware: [authMiddleware, queryModifier, uploadDisk.single("image")],
handler: async (req: Req, res: Res) => {
try {
const image = await uploadProvider.getImage();
res.sendOk({
data: image,
message: "Lấy danh sách ảnh thành công",
message_en: "Images retrieved successfully",
status: 200
});
} catch (error) {
console.error("Error retrieving images:", error);
res.sendError({
message: "Lấy danh sách ảnh thất bại",
message_en: "Failed to retrieve images",
status: 500
});
}
}
},
/**
* @openapi
* /api/v1.0/upload:
......@@ -37,10 +77,10 @@ export default (_express: Application) => {
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/UploadResponse"
* $ref: "#/components/schemas/postResponse"
*/
post: {
middleware: [authMiddleware, queryModifier, upload.single("image")],
middleware: [authMiddleware, queryModifier, uploadDisk.single("image")],
handler: async (req: Req, res: Res) => {
try {
......@@ -60,10 +100,16 @@ export default (_express: Application) => {
});
}
const result = await uploadService.uploadImage(req.file.buffer);
const image = await uploadProvider.uploadImage(req);
res.sendOk({
data: { width: result?.width, height: result?.height, bytes: result?.bytes, url: result?.url },
data: {
id: image.id,
url: image.url,
name: image.name,
size: image.size,
type: image.type
},
message: "Upload ảnh thành công",
message_en: "Image uploaded successfully",
status: 200
......
import { Req, Res } from "#interfaces/IApi";
import { authMiddleware } from "#middlewares/authentication";
import queryModifier from "#middlewares/request";
import { UploadProvider } from "#providers/UploadProvider";
import { Application } from "express";
import { Resource } from "express-automatic-routes";
export default (_express: Application) => {
const uploadProvider = new UploadProvider();
return <Resource>{
/**
* @openapi
* /api/v1.0/upload/{id}:
* get:
* tags: [Upload]
* security:
* - bearerAuth: []
* description: Lấy thông tin file ảnh theo ID.
* parameters:
* - name: id
* in: path
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Lấy thông tin file ảnh thành công
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/getIdResponse"
*/
get: {
middleware: [authMiddleware, queryModifier],
handler: async (req: Req, res: Res) => {
try {
if (!req.params.id) {
return res.sendError({
message: "ID ảnh không được để trống",
message_en: "Image ID cannot be empty",
status: 400
});
}
if (typeof req.params.id !== "string") {
return res.sendError({
message: "ID ảnh không hợp lệ",
message_en: "Image ID is invalid",
status: 400
});
}
const image = await uploadProvider.getImageById(req.params.id);
res.sendOk({
data: {
id: image.id,
url: image.url,
name: image.name,
size: image.size,
type: image.type
},
message: "Lấy thông tin ảnh thành công",
message_en: "Image information retrieved successfully",
status: 200
});
} catch (error) {
console.error("Error retrieving image by ID:", error);
res.sendError({
message: "Lấy thông tin ảnh thất bại",
message_en: "Failed to retrieve image information",
status: 500
});
}
}
},
/**
* @openapi
* /api/v1.0/upload/{id}:
* delete:
* tags: [Upload]
* security:
* - bearerAuth: []
* parameters:
* - name: id
* in: path
* required: true
* schema:
* type: string
* description: Xóa file ảnh theo ID.
* responses:
* 200:
* description: Xóa file ảnh thành công
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/deleteResponse"
*/
delete: {
middleware: [authMiddleware, queryModifier],
handler: async (req: Req, res: Res) => {
try {
if (!req.params.id) {
return res.sendError({
message: "ID ảnh không được để trống",
message_en: "Image ID cannot be empty",
status: 400
});
}
if (typeof req.params.id !== "string") {
return res.sendError({
message: "ID ảnh không hợp lệ",
message_en: "Image ID is invalid",
status: 400
});
}
const image = await uploadProvider.deleteImageById(req.params.id);
res.sendOk({
data: {
id: image.id,
url: image.url,
name: image.name,
size: image.size,
type: image.type
},
message: "Xóa ảnh thành công",
message_en: "Image deleted successfully",
status: 200
});
} catch (error) {
console.error("Error deleting image by ID:", error);
res.sendError({
message: "Xóa ảnh thất bại",
message_en: "Failed to delete image",
status: 500
});
}
}
}
};
}
\ No newline at end of file
......@@ -1010,7 +1010,20 @@
}
}
},
"UploadResponse": {
"postInput": {
"type": "object",
"example": {
"file": "image.jpg"
},
"properties": {
"file": {
"type": "string",
"format": "binary",
"example": "image.jpg"
}
}
},
"getResponse": {
"type": "object",
"properties": {
"message": {
......@@ -1021,24 +1034,129 @@
"type": "string",
"nullable": true
},
"responseData": {
"data": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"example": "123e4567-e89b-12d3-a456-426614174000"
},
"url": {
"type": "string",
"example": "uploads/image-1780543350715-29021218.png"
},
"name": {
"type": "string",
"example": "image-1780543350715-29021218.png"
},
"size": {
"type": "number",
"example": 150000
},
"type": {
"type": "string",
"example": "image/jpeg"
}
}
}
},
"status": {
"type": "string",
"example": "success"
},
"timeStamp": {
"type": "string",
"example": "2024-02-26 03:12:45"
},
"violations": {
"type": "array"
}
}
},
"getIdResponse": {
"type": "object",
"properties": {
"message": {
"type": "string",
"nullable": true
},
"message_en": {
"type": "string",
"nullable": true
},
"data": {
"type": "object",
"properties": {
"width": {
"type": "number",
"example": 800
"id": {
"type": "string",
"example": "123e4567-e89b-12d3-a456-426614174000"
},
"height": {
"type": "number",
"example": 600
"url": {
"type": "string",
"example": "uploads/image-1780543350715-29021218.png"
},
"bytes": {
"name": {
"type": "string",
"example": "image-1780543350715-29021218.png"
},
"size": {
"type": "number",
"example": 150000
},
"type": {
"type": "string",
"example": "image/jpeg"
}
}
},
"status": {
"type": "string",
"example": "success"
},
"timeStamp": {
"type": "string",
"example": "2024-02-26 03:12:45"
},
"violations": {
"type": "array"
}
}
},
"postResponse": {
"type": "object",
"properties": {
"message": {
"type": "string",
"nullable": true
},
"message_en": {
"type": "string",
"nullable": true
},
"responseData": {
"type": "object",
"properties": {
"id": {
"type": "string",
"example": "123e4567-e89b-12d3-a456-426614174000"
},
"url": {
"type": "string",
"example": "https://res.cloudinary.com/demo/image/upload/v1312461204/sample.jpg"
"example": "uploads/image-1780543350715-29021218.png"
},
"name": {
"type": "string",
"example": "image-1780543350715-29021218.png"
},
"size": {
"type": "number",
"example": 15078
},
"type": {
"type": "string",
"example": "image/png"
}
}
},
......@@ -1055,16 +1173,52 @@
}
}
},
"UploadInput": {
"deleteResponse": {
"type": "object",
"example": {
"file": "image.jpg"
},
"properties": {
"file": {
"message": {
"type": "string",
"format": "binary",
"example": "image.jpg"
"nullable": true
},
"message_en": {
"type": "string",
"nullable": true
},
"data": {
"type": "object",
"properties": {
"id": {
"type": "string",
"example": "123e4567-e89b-12d3-a456-426614174000"
},
"url": {
"type": "string",
"example": "uploads/image-1780543350715-29021218.png"
},
"name": {
"type": "string",
"example": "image-1780543350715-29021218.png"
},
"size": {
"type": "number",
"example": 150000
},
"type": {
"type": "string",
"example": "image/jpeg"
}
}
},
"status": {
"type": "string",
"example": "success"
},
"timeStamp": {
"type": "string",
"example": "2024-02-26 03:12:45"
},
"violations": {
"type": "array"
}
}
}
......@@ -1704,7 +1858,7 @@
"file": {
"type": "string",
"format": "binary",
"description": "File CSV (Giới hạn dưới 2MB)"
"description": "File CSV (Giới hạn dưới 5MB)"
}
}
}
......@@ -1966,7 +2120,98 @@
}
}
},
"/api/v1.0/upload/{id}": {
"get": {
"tags": [
"Upload"
],
"security": [
{
"bearerAuth": []
}
],
"description": "Lấy thông tin file ảnh theo ID.",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Lấy thông tin file ảnh thành công",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/getIdResponse"
}
}
}
}
}
},
"delete": {
"tags": [
"Upload"
],
"security": [
{
"bearerAuth": []
}
],
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"description": "Xóa file ảnh theo ID.",
"responses": {
"200": {
"description": "Xóa file ảnh thành công",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/deleteResponse"
}
}
}
}
}
}
},
"/api/v1.0/upload": {
"get": {
"tags": [
"Upload"
],
"security": [
{
"bearerAuth": []
}
],
"description": "Lấy danh sách file ảnh.",
"responses": {
"200": {
"description": "Lấy danh sách file ảnh thành công",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/getResponse"
}
}
}
}
}
},
"post": {
"tags": [
"Upload"
......@@ -2003,7 +2248,7 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UploadResponse"
"$ref": "#/components/schemas/postResponse"
}
}
}
......
import express from 'express';
import { resolve } from 'path';
import path, { resolve } from 'path';
import _autoroutes from 'express-automatic-routes';
import swaggerUi from 'swagger-ui-express';
import response from '#middlewares/response';
......@@ -21,6 +21,7 @@ mountRoutes(app, {
emailCronJob.start();
app.use('/uploads', express.static(path.join(process.cwd(), 'uploads')));
app.use('/swagger/index', swaggerUi.serve, swaggerUi.setup(swaggerFile));
app.listen(port, () => {
......
......@@ -9,6 +9,8 @@ import { refresh_token as _refresh_token } from "./refresh_token";
import type { refresh_tokenAttributes, refresh_tokenCreationAttributes } from "./refresh_token";
import { roles as _roles } from "./roles";
import type { rolesAttributes, rolesCreationAttributes } from "./roles";
import { upload as _upload } from "./upload";
import type { uploadAttributes, uploadCreationAttributes } from "./upload";
import { user_auth as _user_auth } from "./user_auth";
import type { user_authAttributes, user_authCreationAttributes } from "./user_auth";
import { users as _users } from "./users";
......@@ -20,6 +22,7 @@ export {
_enrollments as enrollments,
_refresh_token as refresh_token,
_roles as roles,
_upload as upload,
_user_auth as user_auth,
_users as users,
};
......@@ -35,6 +38,8 @@ export type {
refresh_tokenCreationAttributes,
rolesAttributes,
rolesCreationAttributes,
uploadAttributes,
uploadCreationAttributes,
user_authAttributes,
user_authCreationAttributes,
usersAttributes,
......@@ -47,6 +52,7 @@ export function initModels(sequelize: Sequelize) {
const enrollments = _enrollments.initModel(sequelize);
const refresh_token = _refresh_token.initModel(sequelize);
const roles = _roles.initModel(sequelize);
const upload = _upload.initModel(sequelize);
const user_auth = _user_auth.initModel(sequelize);
const users = _users.initModel(sequelize);
......@@ -69,6 +75,7 @@ export function initModels(sequelize: Sequelize) {
enrollments: enrollments,
refresh_token: refresh_token,
roles: roles,
upload: upload,
user_auth: user_auth,
users: users,
};
......
import * as Sequelize from 'sequelize';
import { DataTypes, Model, Optional } from 'sequelize';
export interface uploadAttributes {
url: string;
name?: string;
size?: number;
type?: string;
created_at: Date;
created_by?: string;
updated_at?: Date;
updated_by?: string;
id: string;
}
export type uploadPk = "id";
export type uploadId = upload[uploadPk];
export type uploadOptionalAttributes = "name" | "size" | "type" | "created_at" | "created_by" | "updated_at" | "updated_by" | "id";
export type uploadCreationAttributes = Optional<uploadAttributes, uploadOptionalAttributes>;
export class upload extends Model<uploadAttributes, uploadCreationAttributes> implements uploadAttributes {
url!: string;
name?: string;
size?: number;
type?: string;
created_at!: Date;
created_by?: string;
updated_at?: Date;
updated_by?: string;
id!: string;
static initModel(sequelize: Sequelize.Sequelize): typeof upload {
return sequelize.define('upload', {
url: {
type: DataTypes.TEXT,
allowNull: false,
unique: "upload_url_unique"
},
name: {
type: DataTypes.TEXT,
allowNull: true
},
size: {
type: DataTypes.INTEGER,
allowNull: true
},
type: {
type: DataTypes.TEXT,
allowNull: true
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: Sequelize.Sequelize.fn('now')
},
created_by: {
type: DataTypes.UUID,
allowNull: true
},
updated_at: {
type: DataTypes.DATE,
allowNull: true
},
updated_by: {
type: DataTypes.UUID,
allowNull: true
},
id: {
type: DataTypes.UUID,
allowNull: false,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
}
}, {
tableName: 'upload',
schema: 'public',
timestamps: false,
indexes: [
{
name: "upload_pkey",
unique: true,
fields: [
{ name: "id" },
]
},
{
name: "upload_url_unique",
unique: true,
fields: [
{ name: "url" },
]
},
]
}) as typeof upload;
}
}
import { Req } from "#interfaces/IApi";
import { models } from "#models/sequelize-config";
import path from "path/win32";
import fs from 'fs';
export class UploadProvider {
async getImage() {
const images = await models.upload.findAll({
attributes: ["id", "url", "name", "size", "type"],
order: [["created_at", "DESC"]]
});
return images;
}
async getImageById(id: string) {
const image = await models.upload.findOne({
where: { id },
attributes: ["id", "url", "name", "size", "type"]
});
if (!image) {
throw new Error("Image not found!");
}
return image;
}
async uploadImage(req: Req) {
if (!req.file) {
throw new Error("No file uploaded!");
}
if (!req.file.mimetype.startsWith("image/")) {
throw new Error("Only image uploads are allowed!");
}
const image = await models.upload.create({
url: req.file.path.replace(/\\/g, '/'),
name: req.file.filename,
size: req.file.size,
type: req.file.mimetype
});
return image;
}
async deleteImageById(id: string) {
const image = await models.upload.findOne({
where: { id }
});
if (!image) {
throw new Error("Image not found!");
}
const filePath = path.join(process.cwd(), image.url);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
await image.destroy();
} else {
console.warn(`File tồn tại trong DB nhưng không tìm thấy trên ổ cứng: ${filePath}`);
}
return image;
}
}
\ No newline at end of file
import { Readable } from "stream";
import cloudinary from "#config/cloudinary.config";
type UploadResult = {
width?: number;
height?: number;
bytes?: number;
url?: string;
[key: string]: any;
};
export class UploadService {
async uploadImage(buffer: Buffer): Promise<UploadResult | undefined> {
return new Promise((resolve, reject) => {
const stream = cloudinary.uploader.upload_stream(
{ folder: "images" },
(error, result) => {
if (error) return reject(error);
resolve(result as UploadResult);
}
);
Readable.from(buffer).pipe(stream);
});
}
}
\ No newline at end of file
const uploadSchemas = {
UploadResponse: {
postInput: {
type: 'object',
example: {
file: 'image.jpg',
},
properties: {
file: {
type: 'string',
format: 'binary',
example: 'image.jpg',
},
},
},
getResponse: {
type: 'object',
properties: {
message: {
......@@ -10,24 +24,81 @@ const uploadSchemas = {
type: 'string',
nullable: true,
},
responseData: {
data: {
type: 'array',
items: {
type: 'object',
properties: {
id: {
type: 'string',
example: '123e4567-e89b-12d3-a456-426614174000',
},
url: {
type: 'string',
example: 'uploads/image-1780543350715-29021218.png',
},
name: {
type: 'string',
example: 'image-1780543350715-29021218.png',
},
size: {
type: 'number',
example: 150000,
},
type: {
type: 'string',
example: 'image/jpeg',
},
},
},
},
status: {
type: 'string',
example: 'success',
},
timeStamp: {
type: 'string',
example: '2024-02-26 03:12:45',
},
violations: {
type: 'array',
},
},
},
getIdResponse: {
type: 'object',
properties: {
message: {
type: 'string',
nullable: true,
},
message_en: {
type: 'string',
nullable: true,
},
data: {
type: 'object',
properties: {
width: {
type: 'number',
example: 800,
id: {
type: 'string',
example: '123e4567-e89b-12d3-a456-426614174000',
},
height: {
type: 'number',
example: 600,
url: {
type: 'string',
example: 'uploads/image-1780543350715-29021218.png',
},
bytes: {
name: {
type: 'string',
example: 'image-1780543350715-29021218.png',
},
size: {
type: 'number',
example: 150000,
},
url: {
type: {
type: 'string',
example: 'https://res.cloudinary.com/demo/image/upload/v1312461204/sample.jpg',
example: 'image/jpeg',
},
},
},
......@@ -45,16 +116,102 @@ const uploadSchemas = {
},
},
UploadInput: {
postResponse: {
type: 'object',
example: {
file: 'image.jpg',
properties: {
message: {
type: 'string',
nullable: true,
},
message_en: {
type: 'string',
nullable: true,
},
responseData: {
type: 'object',
properties: {
id: {
type: 'string',
example: '123e4567-e89b-12d3-a456-426614174000',
},
url: {
type: 'string',
example: 'uploads/image-1780543350715-29021218.png'
},
name: {
type: 'string',
example: 'image-1780543350715-29021218.png'
},
size: {
type: 'number',
example: 15078
},
type: {
type: 'string',
example: 'image/png'
}
},
},
status: {
type: 'string',
example: 'success',
},
timeStamp: {
type: 'string',
example: '2024-02-26 03:12:45',
},
violations: {
type: 'array',
},
},
},
deleteResponse: {
type: 'object',
properties: {
file: {
message: {
type: 'string',
format: 'binary',
example: 'image.jpg',
nullable: true,
},
message_en: {
type: 'string',
nullable: true,
},
data: {
type: 'object',
properties: {
id: {
type: 'string',
example: '123e4567-e89b-12d3-a456-426614174000',
},
url: {
type: 'string',
example: 'uploads/image-1780543350715-29021218.png',
},
name: {
type: 'string',
example: 'image-1780543350715-29021218.png',
},
size: {
type: 'number',
example: 150000,
},
type: {
type: 'string',
example: 'image/jpeg',
},
},
},
status: {
type: 'string',
example: 'success',
},
timeStamp: {
type: 'string',
example: '2024-02-26 03:12:45',
},
violations: {
type: 'array',
},
},
},
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment