update router getType
All checks were successful
Deploy to Production / deploy (push) Successful in 21s
All checks were successful
Deploy to Production / deploy (push) Successful in 21s
This commit is contained in:
126
UPLOAD_GUIDE.md
Normal file
126
UPLOAD_GUIDE.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# Upload API
|
||||
|
||||
API endpoint để upload các loại file khác nhau.
|
||||
|
||||
## Quy tắc phân loại file
|
||||
|
||||
- **Images** (jpg, jpeg, png, gif, bmp, webp, svg, ico) → `public/images/`
|
||||
- **Audio** (mp3, wav, ogg, m4a, aac, flac, wma) → `public/media/`
|
||||
- **Video** (mp4, avi, mov, wmv, flv, mkv, webm) → `public/media/`
|
||||
- **Các file khác** → `public/files/`
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### 1. Upload single file
|
||||
```
|
||||
POST /api/upload/single
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
Body:
|
||||
- file: (binary file)
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "File uploaded successfully",
|
||||
"data": {
|
||||
"filename": "example-1234567890.jpg",
|
||||
"originalname": "example.jpg",
|
||||
"mimetype": "image/jpeg",
|
||||
"size": 12345,
|
||||
"type": "image",
|
||||
"path": "public/images/example-1234567890.jpg",
|
||||
"url": "/public/images/example-1234567890.jpg"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Upload multiple files
|
||||
```
|
||||
POST /api/upload/multiple
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
Body:
|
||||
- files: (array of binary files, max 10)
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "2 files uploaded successfully",
|
||||
"data": [
|
||||
{
|
||||
"filename": "photo1-1234567890.jpg",
|
||||
"originalname": "photo1.jpg",
|
||||
"mimetype": "image/jpeg",
|
||||
"size": 12345,
|
||||
"type": "image",
|
||||
"path": "public/images/photo1-1234567890.jpg",
|
||||
"url": "/public/images/photo1-1234567890.jpg"
|
||||
},
|
||||
{
|
||||
"filename": "audio-1234567891.mp3",
|
||||
"originalname": "audio.mp3",
|
||||
"mimetype": "audio/mpeg",
|
||||
"size": 54321,
|
||||
"type": "audio",
|
||||
"path": "public/media/audio-1234567891.mp3",
|
||||
"url": "/public/media/audio-1234567891.mp3"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Delete file
|
||||
```
|
||||
DELETE /api/upload/file
|
||||
Content-Type: application/json
|
||||
|
||||
Body:
|
||||
{
|
||||
"filepath": "public/images/example-1234567890.jpg"
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Get file info
|
||||
```
|
||||
GET /api/upload/info?filepath=public/images/example-1234567890.jpg
|
||||
```
|
||||
|
||||
## Giới hạn
|
||||
|
||||
- **Max file size:** 50MB
|
||||
- **Max files per upload (multiple):** 10 files
|
||||
|
||||
## Sử dụng với Postman/cURL
|
||||
|
||||
### Postman:
|
||||
1. Chọn POST method
|
||||
2. URL: `http://localhost:10001/api/upload/single`
|
||||
3. Body → form-data
|
||||
4. Key: `file`, Type: File
|
||||
5. Chọn file cần upload
|
||||
|
||||
### cURL:
|
||||
```bash
|
||||
# Upload single file
|
||||
curl -X POST http://localhost:10001/api/upload/single \
|
||||
-F "file=@/path/to/your/file.jpg"
|
||||
|
||||
# Upload multiple files
|
||||
curl -X POST http://localhost:10001/api/upload/multiple \
|
||||
-F "files=@/path/to/file1.jpg" \
|
||||
-F "files=@/path/to/file2.mp3"
|
||||
```
|
||||
|
||||
## Truy cập file đã upload
|
||||
|
||||
Sau khi upload thành công, file có thể truy cập qua URL:
|
||||
```
|
||||
http://localhost:10001/public/images/example-1234567890.jpg
|
||||
http://localhost:10001/public/media/audio-1234567890.mp3
|
||||
http://localhost:10001/public/files/document-1234567890.pdf
|
||||
```
|
||||
3
app.js
3
app.js
@@ -39,6 +39,7 @@ const vocabRoutes = require('./routes/vocabRoutes');
|
||||
const grammarRoutes = require('./routes/grammarRoutes');
|
||||
const storyRoutes = require('./routes/storyRoutes');
|
||||
const learningContentRoutes = require('./routes/learningContentRoutes');
|
||||
const uploadRoutes = require('./routes/uploadRoutes');
|
||||
|
||||
/**
|
||||
* Initialize Express Application
|
||||
@@ -153,6 +154,7 @@ app.get('/api', (req, res) => {
|
||||
games: '/api/games',
|
||||
gameTypes: '/api/game-types',
|
||||
vocab: '/api/vocab',
|
||||
upload: '/api/upload',
|
||||
},
|
||||
documentation: '/api-docs',
|
||||
});
|
||||
@@ -210,6 +212,7 @@ app.use('/api/vocab', vocabRoutes);
|
||||
app.use('/api/grammar', grammarRoutes);
|
||||
app.use('/api/stories', storyRoutes);
|
||||
app.use('/api/learning-content', learningContentRoutes);
|
||||
app.use('/api/upload', uploadRoutes);
|
||||
|
||||
/**
|
||||
* Queue Status Endpoint
|
||||
|
||||
@@ -16,6 +16,7 @@ class GameTypeController {
|
||||
const cacheKey = `game_types:list:${page}:${limit}:${is_active || 'all'}:${is_premium || 'all'}:${difficulty_level || 'all'}`;
|
||||
|
||||
const cached = await cacheUtils.get(cacheKey);
|
||||
/*
|
||||
if (cached) {
|
||||
return res.json({
|
||||
success: true,
|
||||
@@ -23,6 +24,7 @@ class GameTypeController {
|
||||
cached: true,
|
||||
});
|
||||
}
|
||||
*/
|
||||
|
||||
const where = {};
|
||||
if (is_active !== undefined) where.is_active = is_active === 'true';
|
||||
|
||||
@@ -263,12 +263,6 @@ class TeacherController {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
jobId: job.id,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get teacher datatypes
|
||||
|
||||
276
controllers/uploadController.js
Normal file
276
controllers/uploadController.js
Normal file
@@ -0,0 +1,276 @@
|
||||
const multer = require('multer');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
/**
|
||||
* Upload Controller - Quản lý upload file
|
||||
*/
|
||||
class UploadController {
|
||||
constructor() {
|
||||
// Danh sách các extension cho từng loại file
|
||||
this.imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg', '.ico'];
|
||||
this.audioExtensions = ['.mp3', '.wav', '.ogg', '.m4a', '.aac', '.flac', '.wma'];
|
||||
this.videoExtensions = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv', '.webm'];
|
||||
|
||||
// Tạo các thư mục nếu chưa tồn tại
|
||||
this.ensureDirectories();
|
||||
|
||||
// Cấu hình multer storage
|
||||
this.storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
const fileExt = path.extname(file.originalname).toLowerCase();
|
||||
let uploadPath = 'public/files';
|
||||
|
||||
if (this.imageExtensions.includes(fileExt)) {
|
||||
uploadPath = 'public/images';
|
||||
} else if (this.audioExtensions.includes(fileExt) || this.videoExtensions.includes(fileExt)) {
|
||||
uploadPath = 'public/media';
|
||||
}
|
||||
|
||||
cb(null, uploadPath);
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
// Tạo tên file unique: timestamp-random-originalname
|
||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
||||
const ext = path.extname(file.originalname);
|
||||
const nameWithoutExt = path.basename(file.originalname, ext);
|
||||
const sanitizedName = nameWithoutExt.replace(/[^a-zA-Z0-9]/g, '_');
|
||||
cb(null, `${sanitizedName}-${uniqueSuffix}${ext}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Cấu hình multer với giới hạn kích thước
|
||||
this.upload = multer({
|
||||
storage: this.storage,
|
||||
limits: {
|
||||
fileSize: 50 * 1024 * 1024, // 50MB max file size
|
||||
},
|
||||
fileFilter: (req, file, cb) => {
|
||||
// Cho phép tất cả các loại file
|
||||
cb(null, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Đảm bảo các thư mục upload tồn tại
|
||||
*/
|
||||
ensureDirectories() {
|
||||
const directories = [
|
||||
'public/images',
|
||||
'public/media',
|
||||
'public/files'
|
||||
];
|
||||
|
||||
directories.forEach(dir => {
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload single file
|
||||
*/
|
||||
uploadSingle(req, res, next) {
|
||||
const uploadHandler = this.upload.single('file');
|
||||
|
||||
uploadHandler(req, res, (err) => {
|
||||
if (err instanceof multer.MulterError) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Upload error',
|
||||
error: err.message,
|
||||
});
|
||||
} else if (err) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error during upload',
|
||||
error: err.message,
|
||||
});
|
||||
}
|
||||
|
||||
if (!req.file) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'No file uploaded',
|
||||
});
|
||||
}
|
||||
|
||||
// Xác định loại file
|
||||
const fileExt = path.extname(req.file.originalname).toLowerCase();
|
||||
let fileType = 'file';
|
||||
|
||||
if (this.imageExtensions.includes(fileExt)) {
|
||||
fileType = 'image';
|
||||
} else if (this.audioExtensions.includes(fileExt)) {
|
||||
fileType = 'audio';
|
||||
} else if (this.videoExtensions.includes(fileExt)) {
|
||||
fileType = 'video';
|
||||
}
|
||||
|
||||
// Tạo URL truy cập file
|
||||
const fileUrl = `/${req.file.path.replace(/\\/g, '/')}`;
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: 'File uploaded successfully',
|
||||
data: {
|
||||
filename: req.file.filename,
|
||||
originalname: req.file.originalname,
|
||||
mimetype: req.file.mimetype,
|
||||
size: req.file.size,
|
||||
type: fileType,
|
||||
path: req.file.path,
|
||||
url: fileUrl,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload multiple files
|
||||
*/
|
||||
uploadMultiple(req, res, next) {
|
||||
const uploadHandler = this.upload.array('files', 10); // Max 10 files
|
||||
|
||||
uploadHandler(req, res, (err) => {
|
||||
if (err instanceof multer.MulterError) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Upload error',
|
||||
error: err.message,
|
||||
});
|
||||
} else if (err) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error during upload',
|
||||
error: err.message,
|
||||
});
|
||||
}
|
||||
|
||||
if (!req.files || req.files.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'No files uploaded',
|
||||
});
|
||||
}
|
||||
|
||||
const uploadedFiles = req.files.map(file => {
|
||||
const fileExt = path.extname(file.originalname).toLowerCase();
|
||||
let fileType = 'file';
|
||||
|
||||
if (this.imageExtensions.includes(fileExt)) {
|
||||
fileType = 'image';
|
||||
} else if (this.audioExtensions.includes(fileExt)) {
|
||||
fileType = 'audio';
|
||||
} else if (this.videoExtensions.includes(fileExt)) {
|
||||
fileType = 'video';
|
||||
}
|
||||
|
||||
const fileUrl = `/${file.path.replace(/\\/g, '/')}`;
|
||||
|
||||
return {
|
||||
filename: file.filename,
|
||||
originalname: file.originalname,
|
||||
mimetype: file.mimetype,
|
||||
size: file.size,
|
||||
type: fileType,
|
||||
path: file.path,
|
||||
url: fileUrl,
|
||||
};
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: `${uploadedFiles.length} files uploaded successfully`,
|
||||
data: uploadedFiles,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete file
|
||||
*/
|
||||
async deleteFile(req, res, next) {
|
||||
try {
|
||||
const { filepath } = req.body;
|
||||
|
||||
if (!filepath) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Filepath is required',
|
||||
});
|
||||
}
|
||||
|
||||
// Kiểm tra file có tồn tại không
|
||||
if (!fs.existsSync(filepath)) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'File not found',
|
||||
});
|
||||
}
|
||||
|
||||
// Xóa file
|
||||
fs.unlinkSync(filepath);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'File deleted successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file info
|
||||
*/
|
||||
async getFileInfo(req, res, next) {
|
||||
try {
|
||||
const { filepath } = req.query;
|
||||
|
||||
if (!filepath) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Filepath is required',
|
||||
});
|
||||
}
|
||||
|
||||
// Kiểm tra file có tồn tại không
|
||||
if (!fs.existsSync(filepath)) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'File not found',
|
||||
});
|
||||
}
|
||||
|
||||
const stats = fs.statSync(filepath);
|
||||
const fileExt = path.extname(filepath).toLowerCase();
|
||||
let fileType = 'file';
|
||||
|
||||
if (this.imageExtensions.includes(fileExt)) {
|
||||
fileType = 'image';
|
||||
} else if (this.audioExtensions.includes(fileExt)) {
|
||||
fileType = 'audio';
|
||||
} else if (this.videoExtensions.includes(fileExt)) {
|
||||
fileType = 'video';
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
path: filepath,
|
||||
size: stats.size,
|
||||
type: fileType,
|
||||
created: stats.birthtime,
|
||||
modified: stats.mtime,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new UploadController();
|
||||
@@ -31,7 +31,7 @@ const GameType = sequelize.define('game_types', {
|
||||
comment: 'Mô tả loại game (tiếng Việt)'
|
||||
},
|
||||
type: {
|
||||
type: DataTypes.TEXT,
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
comment: 'Mã định danh loại game: counting_quiz, math_game, word_puzzle, etc.'
|
||||
|
||||
96
package-lock.json
generated
96
package-lock.json
generated
@@ -22,6 +22,7 @@
|
||||
"joi": "^17.11.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "^2.0.2",
|
||||
"mysql2": "^3.6.5",
|
||||
"sequelize": "^6.35.2",
|
||||
"swagger-jsdoc": "^6.2.8",
|
||||
@@ -1656,6 +1657,12 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/append-field": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
|
||||
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/aproba": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz",
|
||||
@@ -2019,7 +2026,6 @@
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bullmq": {
|
||||
@@ -2037,6 +2043,17 @@
|
||||
"uuid": "11.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/busboy": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
|
||||
"dependencies": {
|
||||
"streamsearch": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
@@ -2422,6 +2439,21 @@
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/concat-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
|
||||
"integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
|
||||
"engines": [
|
||||
"node >= 6.0"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^3.0.2",
|
||||
"typedarray": "^0.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
|
||||
@@ -5150,6 +5182,15 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
|
||||
@@ -5288,6 +5329,36 @@
|
||||
"@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/multer": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz",
|
||||
"integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"append-field": "^1.0.0",
|
||||
"busboy": "^1.6.0",
|
||||
"concat-stream": "^2.0.0",
|
||||
"mkdirp": "^0.5.6",
|
||||
"object-assign": "^4.1.1",
|
||||
"type-is": "^1.6.18",
|
||||
"xtend": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/multer/node_modules/mkdirp": {
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
|
||||
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.6"
|
||||
},
|
||||
"bin": {
|
||||
"mkdirp": "bin/cmd.js"
|
||||
}
|
||||
},
|
||||
"node_modules/mysql2": {
|
||||
"version": "3.16.0",
|
||||
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.16.0.tgz",
|
||||
@@ -6494,6 +6565,14 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/streamsearch": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
|
||||
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
@@ -6889,6 +6968,12 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/typedarray": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
||||
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undefsafe": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
|
||||
@@ -7157,6 +7242,15 @@
|
||||
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
"joi": "^17.11.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "^2.0.2",
|
||||
"mysql2": "^3.6.5",
|
||||
"sequelize": "^6.35.2",
|
||||
"swagger-jsdoc": "^6.2.8",
|
||||
|
||||
3
public/files/.gitkeep
Normal file
3
public/files/.gitkeep
Normal file
@@ -0,0 +1,3 @@
|
||||
# Thư mục lưu trữ các file khác
|
||||
*
|
||||
!.gitkeep
|
||||
10
public/images/.gitkeep
Normal file
10
public/images/.gitkeep
Normal file
@@ -0,0 +1,10 @@
|
||||
# Thư mục lưu trữ hình ảnh
|
||||
*.jpg
|
||||
*.jpeg
|
||||
*.png
|
||||
*.gif
|
||||
*.bmp
|
||||
*.webp
|
||||
*.svg
|
||||
*.ico
|
||||
!.gitkeep
|
||||
BIN
public/images/quiz01.png
Normal file
BIN
public/images/quiz01.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 120 KiB |
BIN
public/images/quiz02.png
Normal file
BIN
public/images/quiz02.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 97 KiB |
BIN
public/images/quiz03.png
Normal file
BIN
public/images/quiz03.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
BIN
public/images/quiz04.png
Normal file
BIN
public/images/quiz04.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 112 KiB |
BIN
public/images/quiz05.png
Normal file
BIN
public/images/quiz05.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
16
public/media/.gitkeep
Normal file
16
public/media/.gitkeep
Normal file
@@ -0,0 +1,16 @@
|
||||
# Thư mục lưu trữ media (audio/video)
|
||||
*.mp3
|
||||
*.wav
|
||||
*.ogg
|
||||
*.m4a
|
||||
*.aac
|
||||
*.flac
|
||||
*.wma
|
||||
*.mp4
|
||||
*.avi
|
||||
*.mov
|
||||
*.wmv
|
||||
*.flv
|
||||
*.mkv
|
||||
*.webm
|
||||
!.gitkeep
|
||||
119
routes/uploadRoutes.js
Normal file
119
routes/uploadRoutes.js
Normal file
@@ -0,0 +1,119 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const uploadController = require('../controllers/uploadController');
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* tags:
|
||||
* name: Upload
|
||||
* description: File upload management
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/upload/single:
|
||||
* post:
|
||||
* summary: Upload single file
|
||||
* tags: [Upload]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* multipart/form-data:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* file:
|
||||
* type: string
|
||||
* format: binary
|
||||
* description: File to upload (images → public/images, audio/video → public/media, others → public/files)
|
||||
* responses:
|
||||
* 201:
|
||||
* description: File uploaded successfully
|
||||
* 400:
|
||||
* description: No file uploaded or upload error
|
||||
* 500:
|
||||
* description: Server error
|
||||
*/
|
||||
router.post('/single', (req, res, next) => {
|
||||
uploadController.uploadSingle(req, res, next);
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/upload/multiple:
|
||||
* post:
|
||||
* summary: Upload multiple files (max 10)
|
||||
* tags: [Upload]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* multipart/form-data:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* files:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* format: binary
|
||||
* description: Files to upload (max 10 files)
|
||||
* responses:
|
||||
* 201:
|
||||
* description: Files uploaded successfully
|
||||
* 400:
|
||||
* description: No files uploaded or upload error
|
||||
* 500:
|
||||
* description: Server error
|
||||
*/
|
||||
router.post('/multiple', (req, res, next) => {
|
||||
uploadController.uploadMultiple(req, res, next);
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/upload/file:
|
||||
* delete:
|
||||
* summary: Delete uploaded file
|
||||
* tags: [Upload]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - filepath
|
||||
* properties:
|
||||
* filepath:
|
||||
* type: string
|
||||
* example: "public/images/example-1234567890.jpg"
|
||||
* responses:
|
||||
* 200:
|
||||
* description: File deleted successfully
|
||||
* 404:
|
||||
* description: File not found
|
||||
*/
|
||||
router.delete('/file', uploadController.deleteFile);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/upload/info:
|
||||
* get:
|
||||
* summary: Get file information
|
||||
* tags: [Upload]
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: filepath
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Path to the file
|
||||
* responses:
|
||||
* 200:
|
||||
* description: File info retrieved successfully
|
||||
* 404:
|
||||
* description: File not found
|
||||
*/
|
||||
router.get('/info', uploadController.getFileInfo);
|
||||
|
||||
module.exports = router;
|
||||
@@ -5,7 +5,7 @@ const { closeQueues } = require('./config/bullmq');
|
||||
const config = require('./config/config.json');
|
||||
|
||||
const PORT = config.server.port || 3000;
|
||||
const HOST = '0.0.0.0';
|
||||
const HOST = 'localhost';
|
||||
|
||||
let server;
|
||||
|
||||
|
||||
66
yarn.lock
66
yarn.lock
@@ -939,6 +939,11 @@ anymatch@^3.0.3, anymatch@~3.1.2:
|
||||
normalize-path "^3.0.0"
|
||||
picomatch "^2.0.4"
|
||||
|
||||
append-field@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz"
|
||||
integrity sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==
|
||||
|
||||
"aproba@^1.0.3 || ^2.0.0":
|
||||
version "2.1.0"
|
||||
resolved "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz"
|
||||
@@ -1156,6 +1161,13 @@ bullmq@^5.1.0:
|
||||
tslib "2.8.1"
|
||||
uuid "11.1.0"
|
||||
|
||||
busboy@^1.6.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz"
|
||||
integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==
|
||||
dependencies:
|
||||
streamsearch "^1.1.0"
|
||||
|
||||
bytes@~3.1.2, bytes@3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz"
|
||||
@@ -1360,6 +1372,16 @@ concat-map@0.0.1:
|
||||
resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz"
|
||||
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
|
||||
|
||||
concat-stream@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz"
|
||||
integrity sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==
|
||||
dependencies:
|
||||
buffer-from "^1.0.0"
|
||||
inherits "^2.0.3"
|
||||
readable-stream "^3.0.2"
|
||||
typedarray "^0.0.6"
|
||||
|
||||
console-control-strings@^1.0.0, console-control-strings@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz"
|
||||
@@ -3029,6 +3051,11 @@ minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2:
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimist@^1.2.6:
|
||||
version "1.2.8"
|
||||
resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz"
|
||||
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
|
||||
|
||||
minipass@^3.0.0:
|
||||
version "3.3.6"
|
||||
resolved "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz"
|
||||
@@ -3049,6 +3076,13 @@ minizlib@^2.1.1:
|
||||
minipass "^3.0.0"
|
||||
yallist "^4.0.0"
|
||||
|
||||
mkdirp@^0.5.6:
|
||||
version "0.5.6"
|
||||
resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz"
|
||||
integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==
|
||||
dependencies:
|
||||
minimist "^1.2.6"
|
||||
|
||||
mkdirp@^1.0.3:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz"
|
||||
@@ -3108,6 +3142,19 @@ msgpackr@1.11.5:
|
||||
optionalDependencies:
|
||||
msgpackr-extract "^3.0.2"
|
||||
|
||||
multer@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz"
|
||||
integrity sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==
|
||||
dependencies:
|
||||
append-field "^1.0.0"
|
||||
busboy "^1.6.0"
|
||||
concat-stream "^2.0.0"
|
||||
mkdirp "^0.5.6"
|
||||
object-assign "^4.1.1"
|
||||
type-is "^1.6.18"
|
||||
xtend "^4.0.2"
|
||||
|
||||
mysql2@^3.6.5:
|
||||
version "3.16.0"
|
||||
resolved "https://registry.npmjs.org/mysql2/-/mysql2-3.16.0.tgz"
|
||||
@@ -3470,7 +3517,7 @@ react-is@^18.0.0:
|
||||
resolved "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz"
|
||||
integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==
|
||||
|
||||
readable-stream@^3.4.0, readable-stream@^3.6.0, readable-stream@^3.6.2:
|
||||
readable-stream@^3.0.2, readable-stream@^3.4.0, readable-stream@^3.6.0, readable-stream@^3.6.2:
|
||||
version "3.6.2"
|
||||
resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz"
|
||||
integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==
|
||||
@@ -3776,6 +3823,11 @@ statuses@~2.0.1, statuses@~2.0.2:
|
||||
resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz"
|
||||
integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==
|
||||
|
||||
streamsearch@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz"
|
||||
integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==
|
||||
|
||||
string_decoder@^1.1.1:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz"
|
||||
@@ -3989,7 +4041,7 @@ type-fest@^0.21.3:
|
||||
resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz"
|
||||
integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
|
||||
|
||||
type-is@~1.6.18:
|
||||
type-is@^1.6.18, type-is@~1.6.18:
|
||||
version "1.6.18"
|
||||
resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz"
|
||||
integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
|
||||
@@ -3997,6 +4049,11 @@ type-is@~1.6.18:
|
||||
media-typer "0.3.0"
|
||||
mime-types "~2.1.24"
|
||||
|
||||
typedarray@^0.0.6:
|
||||
version "0.0.6"
|
||||
resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz"
|
||||
integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==
|
||||
|
||||
undefsafe@^2.0.5:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz"
|
||||
@@ -4160,6 +4217,11 @@ write-file-atomic@^4.0.2:
|
||||
imurmurhash "^0.1.4"
|
||||
signal-exit "^3.0.7"
|
||||
|
||||
xtend@^4.0.2:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz"
|
||||
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
|
||||
|
||||
y18n@^5.0.5:
|
||||
version "5.0.8"
|
||||
resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz"
|
||||
|
||||
Reference in New Issue
Block a user