文件上传服务 #
一、上传概述 #
1.1 上传类型 #
| 类型 | 说明 |
|---|---|
| 单文件上传 | 上传单个文件 |
| 多文件上传 | 同时上传多个文件 |
| 表单混合 | 文件和表单数据混合 |
1.2 安装依赖 #
bash
npm install @hapi/hapi @hapi/inert @hapi/joi @hapi/boom
npm install uuid mime-types
二、文件存储 #
2.1 src/services/fileService.js #
javascript
const fs = require('fs');
const path = require('path');
const { v4: uuidv4 } = require('uuid');
const mime = require('mime-types');
const uploadDir = path.join(__dirname, '../../uploads');
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
const saveFile = async (file) => {
const ext = mime.extension(file.headers['content-type']);
const filename = `${uuidv4()}.${ext}`;
const filepath = path.join(uploadDir, filename);
return new Promise((resolve, reject) => {
const writeStream = fs.createWriteStream(filepath);
file.pipe(writeStream);
writeStream.on('finish', () => {
resolve({
filename,
originalName: file.hapi.filename,
mimetype: file.hapi.headers['content-type'],
size: file.bytes,
path: filepath
});
});
writeStream.on('error', reject);
});
};
const deleteFile = (filename) => {
const filepath = path.join(uploadDir, filename);
return new Promise((resolve, reject) => {
fs.unlink(filepath, (err) => {
if (err) {
reject(err);
} else {
resolve(true);
}
});
});
};
const fileExists = (filename) => {
const filepath = path.join(uploadDir, filename);
return fs.existsSync(filepath);
};
const getFileInfo = (filename) => {
const filepath = path.join(uploadDir, filename);
if (!fs.existsSync(filepath)) {
return null;
}
const stats = fs.statSync(filepath);
return {
filename,
size: stats.size,
createdAt: stats.birthtime,
modifiedAt: stats.mtime
};
};
module.exports = {
saveFile,
deleteFile,
fileExists,
getFileInfo
};
三、上传处理函数 #
3.1 src/handlers/upload.js #
javascript
const Boom = require('@hapi/boom');
const fileService = require('../services/fileService');
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'];
const maxFileSize = 10 * 1024 * 1024;
const uploadSingle = async (request, h) => {
const file = request.payload.file;
if (!file) {
throw Boom.badRequest('No file uploaded');
}
const mimetype = file.hapi.headers['content-type'];
if (!allowedTypes.includes(mimetype)) {
throw Boom.badRequest('File type not allowed');
}
const fileInfo = await fileService.saveFile(file);
return h.response({
message: 'File uploaded successfully',
file: fileInfo
}).code(201);
};
const uploadMultiple = async (request, h) => {
const files = request.payload.files;
if (!files || files.length === 0) {
throw Boom.badRequest('No files uploaded');
}
const results = [];
for (const file of files) {
const mimetype = file.hapi.headers['content-type'];
if (!allowedTypes.includes(mimetype)) {
continue;
}
const fileInfo = await fileService.saveFile(file);
results.push(fileInfo);
}
return h.response({
message: `${results.length} files uploaded successfully`,
files: results
}).code(201);
};
const download = async (request, h) => {
const { filename } = request.params;
if (!fileService.fileExists(filename)) {
throw Boom.notFound('File not found');
}
return h.file(`uploads/${filename}`, {
mode: 'attachment'
});
};
const view = async (request, h) => {
const { filename } = request.params;
if (!fileService.fileExists(filename)) {
throw Boom.notFound('File not found');
}
return h.file(`uploads/${filename}`);
};
const deleteFile = async (request, h) => {
const { filename } = request.params;
if (!fileService.fileExists(filename)) {
throw Boom.notFound('File not found');
}
await fileService.deleteFile(filename);
return h.response().code(204);
};
const getInfo = async (request, h) => {
const { filename } = request.params;
const info = fileService.getFileInfo(filename);
if (!info) {
throw Boom.notFound('File not found');
}
return info;
};
module.exports = {
uploadSingle,
uploadMultiple,
download,
view,
deleteFile,
getInfo
};
四、上传路由 #
4.1 src/routes/upload.js #
javascript
const Joi = require('joi');
const handlers = require('../handlers/upload');
module.exports = [
{
method: 'POST',
path: '/upload',
options: {
description: 'Upload single file',
tags: ['api'],
payload: {
output: 'stream',
parse: true,
maxBytes: 10 * 1024 * 1024,
multipart: true
}
},
handler: handlers.uploadSingle
},
{
method: 'POST',
path: '/upload/multiple',
options: {
description: 'Upload multiple files',
tags: ['api'],
payload: {
output: 'stream',
parse: true,
maxBytes: 50 * 1024 * 1024,
multipart: true
}
},
handler: handlers.uploadMultiple
},
{
method: 'GET',
path: '/files/{filename}',
options: {
description: 'Download file',
tags: ['api'],
validate: {
params: Joi.object({
filename: Joi.string().required()
})
}
},
handler: handlers.download
},
{
method: 'GET',
path: '/view/{filename}',
options: {
description: 'View file',
tags: ['api'],
validate: {
params: Joi.object({
filename: Joi.string().required()
})
}
},
handler: handlers.view
},
{
method: 'DELETE',
path: '/files/{filename}',
options: {
description: 'Delete file',
tags: ['api'],
validate: {
params: Joi.object({
filename: Joi.string().required()
})
}
},
handler: handlers.deleteFile
},
{
method: 'GET',
path: '/files/{filename}/info',
options: {
description: 'Get file info',
tags: ['api'],
validate: {
params: Joi.object({
filename: Joi.string().required()
})
}
},
handler: handlers.getInfo
}
];
五、静态文件服务 #
5.1 配置Inert #
javascript
const Inert = require('@hapi/inert');
await server.register(Inert);
server.route({
method: 'GET',
path: '/uploads/{param*}',
handler: {
directory: {
path: './uploads',
listing: true
}
}
});
六、图片处理 #
6.1 安装sharp #
bash
npm install sharp
6.2 图片缩略图 #
javascript
const sharp = require('sharp');
const fs = require('fs');
const path = require('path');
const createThumbnail = async (inputPath, outputPath, width = 200) => {
await sharp(inputPath)
.resize(width, null, {
fit: 'inside',
withoutEnlargement: true
})
.toFile(outputPath);
};
const getImageMetadata = async (filepath) => {
return await sharp(filepath).metadata();
};
module.exports = {
createThumbnail,
getImageMetadata
};
6.3 上传并生成缩略图 #
javascript
const uploadImage = async (request, h) => {
const file = request.payload.file;
const ext = path.extname(file.hapi.filename);
const filename = `${uuidv4()}${ext}`;
const filepath = path.join(uploadDir, filename);
const thumbPath = path.join(uploadDir, 'thumbs', filename);
await saveFile(file, filepath);
await createThumbnail(filepath, thumbPath);
return {
filename,
thumbnail: `/uploads/thumbs/${filename}`
};
};
七、文件验证 #
7.1 文件类型验证 #
javascript
const validateFileType = (file, allowedTypes) => {
const mimetype = file.hapi.headers['content-type'];
return allowedTypes.includes(mimetype);
};
const validateFileSize = (file, maxSize) => {
return file.bytes <= maxSize;
};
const validateFile = (file, options = {}) => {
const {
allowedTypes = ['image/jpeg', 'image/png'],
maxSize = 10 * 1024 * 1024
} = options;
if (!validateFileType(file, allowedTypes)) {
throw Boom.badRequest('File type not allowed');
}
if (!validateFileSize(file, maxSize)) {
throw Boom.badRequest('File size exceeds limit');
}
return true;
};
八、完整入口文件 #
8.1 src/index.js #
javascript
require('dotenv').config();
const Hapi = require('@hapi/hapi');
const Inert = require('@hapi/inert');
const config = require('./config');
const errorHandler = require('./plugins/errorHandler');
const uploadRoutes = require('./routes/upload');
const init = async () => {
const server = Hapi.server({
port: config.port,
host: config.host
});
await server.register(Inert);
await server.register(errorHandler);
server.route([
{
method: 'GET',
path: '/',
handler: (request, h) => {
return { message: 'File Upload Service' };
}
},
{
method: 'GET',
path: '/uploads/{param*}',
handler: {
directory: {
path: './uploads',
listing: true
}
}
},
...uploadRoutes
]);
await server.start();
console.log('Server running on %s', server.info.uri);
};
init();
九、API测试 #
9.1 上传单文件 #
bash
curl -X POST http://localhost:3000/upload \
-F "file=@/path/to/file.jpg"
9.2 上传多文件 #
bash
curl -X POST http://localhost:3000/upload/multiple \
-F "files=@/path/to/file1.jpg" \
-F "files=@/path/to/file2.jpg"
9.3 下载文件 #
bash
curl http://localhost:3000/files/filename.jpg -O
9.4 获取文件信息 #
bash
curl http://localhost:3000/files/filename.jpg/info
十、总结 #
文件上传要点:
| 功能 | 路径 | 方法 |
|---|---|---|
| 单文件上传 | /upload | POST |
| 多文件上传 | /upload/multiple | POST |
| 下载文件 | /files/ | GET |
| 预览文件 | /view/ | GET |
| 删除文件 | /files/ | DELETE |
下一步,让我们学习部署上线!
最后更新:2026-03-28