文件上传接口

This commit is contained in:
Jerry 2025-05-12 13:53:08 +08:00
parent ec4b97ec65
commit c1199e1649
5 changed files with 222 additions and 6 deletions

View File

@ -14,6 +14,7 @@
"express": "^4.18.0",
"ioredis": "^5.6.0",
"mkdirp": "^3.0.1",
"multer": "1.4.5-lts.2",
"simple-git": "^3.27.0",
"uuid": "^11.1.0",
"ws": "^8.18.1"
@ -22,6 +23,7 @@
"@types/compression": "^1.7.5",
"@types/express": "^4.17.0",
"@types/mkdirp": "^2.0.0",
"@types/multer": "^1.4.12",
"@types/node": "^18.0.0",
"@types/ws": "^8.18.1",
"prettier": "^3.5.3",

121
pnpm-lock.yaml generated
View File

@ -29,6 +29,9 @@ importers:
mkdirp:
specifier: ^3.0.1
version: 3.0.1
multer:
specifier: 1.4.5-lts.2
version: 1.4.5-lts.2
simple-git:
specifier: ^3.27.0
version: 3.27.0
@ -48,6 +51,9 @@ importers:
'@types/mkdirp':
specifier: ^2.0.0
version: 2.0.0
'@types/multer':
specifier: ^1.4.12
version: 1.4.12
'@types/node':
specifier: ^18.0.0
version: 18.19.86
@ -126,6 +132,9 @@ packages:
resolution: {integrity: sha512-c/iUqMymAlxLAyIK3u5SzrwkrkyOdv1XDc91T+b5FsY7Jr6ERhUD19jJHOhPW4GD6tmN6mFEorfSdks525pwdQ==}
deprecated: This is a stub types definition. mkdirp provides its own type definitions, so you do not need this installed.
'@types/multer@1.4.12':
resolution: {integrity: sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg==}
'@types/node@18.19.86':
resolution: {integrity: sha512-fifKayi175wLyKyc5qUfyENhQ1dCNI1UNjp653d8kuYcPQN5JhX3dGuP/XmvPTg/xRBn1VTLpbmi+H/Mr7tLfQ==}
@ -171,6 +180,9 @@ packages:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
append-field@1.0.0:
resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==}
arg@4.1.3:
resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
@ -204,6 +216,10 @@ packages:
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
busboy@1.6.0:
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
engines: {node: '>=10.16.0'}
bytes@3.1.2:
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
engines: {node: '>= 0.8'}
@ -250,6 +266,10 @@ packages:
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
concat-stream@1.6.2:
resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==}
engines: {'0': node >= 0.8}
content-disposition@0.5.4:
resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
engines: {node: '>= 0.6'}
@ -265,6 +285,9 @@ packages:
resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==}
engines: {node: '>= 0.6'}
core-util-is@1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
create-require@1.1.1:
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
@ -473,6 +496,9 @@ packages:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
isarray@1.0.0:
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
lodash.defaults@4.2.0:
resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==}
@ -516,6 +542,10 @@ packages:
minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
mkdirp@0.5.6:
resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
hasBin: true
mkdirp@1.0.4:
resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
engines: {node: '>=10'}
@ -532,6 +562,10 @@ packages:
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
multer@1.4.5-lts.2:
resolution: {integrity: sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==}
engines: {node: '>= 6.0.0'}
negotiator@0.6.3:
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
engines: {node: '>= 0.6'}
@ -544,6 +578,10 @@ packages:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
object-inspect@1.13.4:
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
engines: {node: '>= 0.4'}
@ -582,6 +620,9 @@ packages:
engines: {node: '>=14'}
hasBin: true
process-nextick-args@2.0.1:
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
proxy-addr@2.0.7:
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
engines: {node: '>= 0.10'}
@ -601,6 +642,9 @@ packages:
resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==}
engines: {node: '>= 0.8'}
readable-stream@2.3.8:
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
readdirp@3.6.0:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
@ -623,6 +667,9 @@ packages:
deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true
safe-buffer@5.1.2:
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
@ -673,6 +720,13 @@ packages:
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
engines: {node: '>= 0.8'}
streamsearch@1.1.0:
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
engines: {node: '>=10.0.0'}
string_decoder@1.1.1:
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
strip-bom@3.0.0:
resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
engines: {node: '>=4'}
@ -733,6 +787,9 @@ packages:
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
engines: {node: '>= 0.6'}
typedarray@0.0.6:
resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
typescript@5.8.2:
resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==}
engines: {node: '>=14.17'}
@ -745,6 +802,9 @@ packages:
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
engines: {node: '>= 0.8'}
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
utils-merge@1.0.1:
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
engines: {node: '>= 0.4.0'}
@ -851,6 +911,10 @@ snapshots:
dependencies:
mkdirp: 3.0.1
'@types/multer@1.4.12':
dependencies:
'@types/express': 4.17.21
'@types/node@18.19.86':
dependencies:
undici-types: 5.26.5
@ -898,6 +962,8 @@ snapshots:
normalize-path: 3.0.0
picomatch: 2.3.1
append-field@1.0.0: {}
arg@4.1.3: {}
array-flatten@1.1.1: {}
@ -944,6 +1010,10 @@ snapshots:
buffer-from@1.1.2: {}
busboy@1.6.0:
dependencies:
streamsearch: 1.1.0
bytes@3.1.2: {}
call-bind-apply-helpers@1.0.2:
@ -1003,6 +1073,13 @@ snapshots:
concat-map@0.0.1: {}
concat-stream@1.6.2:
dependencies:
buffer-from: 1.1.2
inherits: 2.0.4
readable-stream: 2.3.8
typedarray: 0.0.6
content-disposition@0.5.4:
dependencies:
safe-buffer: 5.2.1
@ -1013,6 +1090,8 @@ snapshots:
cookie@0.7.1: {}
core-util-is@1.0.3: {}
create-require@1.1.1: {}
debug@2.6.9:
@ -1238,6 +1317,8 @@ snapshots:
is-number@7.0.0: {}
isarray@1.0.0: {}
lodash.defaults@4.2.0: {}
lodash.isarguments@3.1.0: {}
@ -1266,6 +1347,10 @@ snapshots:
minimist@1.2.8: {}
mkdirp@0.5.6:
dependencies:
minimist: 1.2.8
mkdirp@1.0.4: {}
mkdirp@3.0.1: {}
@ -1274,12 +1359,24 @@ snapshots:
ms@2.1.3: {}
multer@1.4.5-lts.2:
dependencies:
append-field: 1.0.0
busboy: 1.6.0
concat-stream: 1.6.2
mkdirp: 0.5.6
object-assign: 4.1.1
type-is: 1.6.18
xtend: 4.0.2
negotiator@0.6.3: {}
negotiator@0.6.4: {}
normalize-path@3.0.0: {}
object-assign@4.1.1: {}
object-inspect@1.13.4: {}
on-finished@2.4.1:
@ -1304,6 +1401,8 @@ snapshots:
prettier@3.5.3: {}
process-nextick-args@2.0.1: {}
proxy-addr@2.0.7:
dependencies:
forwarded: 0.2.0
@ -1324,6 +1423,16 @@ snapshots:
iconv-lite: 0.4.24
unpipe: 1.0.0
readable-stream@2.3.8:
dependencies:
core-util-is: 1.0.3
inherits: 2.0.4
isarray: 1.0.0
process-nextick-args: 2.0.1
safe-buffer: 5.1.2
string_decoder: 1.1.1
util-deprecate: 1.0.2
readdirp@3.6.0:
dependencies:
picomatch: 2.3.1
@ -1344,6 +1453,8 @@ snapshots:
dependencies:
glob: 7.2.3
safe-buffer@5.1.2: {}
safe-buffer@5.2.1: {}
safer-buffer@2.1.2: {}
@ -1424,6 +1535,12 @@ snapshots:
statuses@2.0.1: {}
streamsearch@1.1.0: {}
string_decoder@1.1.1:
dependencies:
safe-buffer: 5.1.2
strip-bom@3.0.0: {}
strip-json-comments@2.0.1: {}
@ -1490,12 +1607,16 @@ snapshots:
media-typer: 0.3.0
mime-types: 2.1.35
typedarray@0.0.6: {}
typescript@5.8.2: {}
undici-types@5.26.5: {}
unpipe@1.0.0: {}
util-deprecate@1.0.2: {}
utils-merge@1.0.1: {}
uuid@11.1.0: {}

View File

@ -2,14 +2,21 @@ import express from 'express';
import FileService from './file.service';
import logger from '../../utils/core/logger';
import response from '../../utils/core/response';
import paths from '../../utils/core/path';
import multer from 'multer';
const uploadDir = paths.get('uploads');
const upload = multer({
dest: uploadDir,
});
class FileController {
private readonly router: express.Router;
private readonly imageService: FileService;
private readonly FileService: FileService;
constructor() {
this.router = express.Router();
this.imageService = new FileService();
this.FileService = new FileService();
this.initializeRoutes();
}
@ -19,13 +26,19 @@ class FileController {
private initializeRoutes(): void {
this.router.get('*', this.handleGetFile);
this.router.post('/upload', upload.single('file'), this.handleUploadFile);
}
/**
* get文件
* @param req
* @param res
*/
private handleGetFile = async (req: express.Request, res: express.Response): Promise<void> => {
try {
const fullPath = req.params[0];
logger.debug(`有个小可爱正在请求${fullPath}噢..`);
const filePath = await this.imageService.getFile(fullPath);
const filePath = await this.FileService.getFile(fullPath);
if (!filePath) {
logger.warn(`${fullPath}:文件不存在..`);
await response.error(res, '文件不存在啦!', 404);
@ -39,6 +52,48 @@ class FileController {
logger.error('晶灵数据请求处理失败:', error);
}
};
/**
*
* @example
* ```js
* const form = new FormData();
* const fileStream = fs.createReadStream(filePath);
* form.append('file', fileStream);
* const uploadUrl = `http://localhost:4000/upload?dir=${uploadDir}`;
* const response = await axios.post(uploadUrl, form, {
* headers: {
* ...form.getHeaders(),
* },
* maxContentLength: Infinity,
* maxBodyLength: Infinity,
* });
* ```
* @param req
* @param res
*/
private handleUploadFile = async (req: express.Request, res: express.Response): Promise<void> => {
try {
if (!req.file) {
await response.error(res, `未检测到上传文件`, 400);
return;
}
const uploadDir = req.query.dir?.toString() || '';
const { fullPath, relativePath } = await this.FileService.saveUploadedFile(
req.file,
uploadDir
);
await response.success(res, {
message: '文件上传成功..',
filePath: fullPath,
url: relativePath,
});
} catch (e) {
await response.error(res, `文件上传失败..`, 500);
logger.error(e);
}
};
}
export default new FileController();

View File

@ -1,7 +1,8 @@
import path from 'path';
import fs from 'fs';
import fs from 'fs/promises';
import paths from '../../utils/core/path';
import logger from '../../utils/core/logger';
import { existsSync } from 'fs';
class FileService {
private readonly filePath: string;
@ -11,6 +12,10 @@ class FileService {
logger.info(`晶灵云图数据中心初始化..数据存储在: ${this.filePath}`);
}
/**
*
* @param relativePath
*/
public async getFile(relativePath: string): Promise<string | null> {
if (!this.isValidPath(relativePath) && !this.isValidFilename(path.basename(relativePath))) {
throw new Error('非法路径请求');
@ -19,9 +24,39 @@ class FileService {
const filePath = path.join(this.filePath, relativePath);
logger.debug(`尝试访问文件路径: ${filePath}`);
return fs.existsSync(filePath) ? filePath : null;
return existsSync(filePath) ? filePath : null;
}
/**
*
* @param file multer的file
* @param dir
*/
public async saveUploadedFile(
file: Express.Multer.File,
dir: string = ''
): Promise<{ fullPath: string; relativePath: string }> {
const baseDir = paths.get('uploads');
const targetDir = path.join(baseDir, dir);
if (!existsSync(targetDir)) {
await fs.mkdir(targetDir, { recursive: true });
logger.debug(`已创建上传目录: ${targetDir}`);
}
const fileName = `${Date.now()}-${file.originalname.replace(/\s+/g, '_')}`;
const finalPath = path.join(targetDir, fileName);
await fs.rename(file.path, finalPath);
logger.info(`保存上传文件: ${finalPath}`);
return {
fullPath: finalPath,
relativePath: `uploads/${dir}/${fileName}`,
};
}
/**
*
* @param relativePath
* @private
*/
private isValidPath(relativePath: string): boolean {
try {
const normalized = path.normalize(relativePath);

View File

@ -37,6 +37,7 @@ class PathManager {
media: path.join(this.baseDir, 'public/files/media'),
package: path.join(this.baseDir, 'package.json'),
modules: path.join(this.baseDir, 'src/modules'),
uploads: path.join(this.baseDir, 'public/files/uploads'),
};
return type ? mappings[type] : this.baseDir;
@ -65,6 +66,7 @@ class PathManager {
this.get('userData'),
this.get('media'),
this.get('temp'),
this.get('uploads'),
];
pathsToInit.forEach((dirPath) => {
@ -86,7 +88,8 @@ type PathType =
| 'files'
| 'package'
| 'media'
| 'modules';
| 'modules'
| 'uploads';
const paths = PathManager.getInstance();
export default paths;