使用nodejs实现简单的文件管理模块
使用nodejs实现简单的文件管理模块
2025-05-31|1 个赞•257 次阅读
服务器#服务器#nodejs#文件上传
2025-05-31|1 个赞•257 次阅读
服务器#服务器#nodejs#文件上传
前言
服务器的附件管理曾一度让我十分头疼,看着本就不富裕的服务器磁盘空间,以及十几二十个G的附件,一时不知如何下手。。。
于是,痛定思痛决定为系统添加一个附件的管理模块。
本文主要介绍实现思路,具体的实现只做参考,方式有很多种。
首先是文件的上传
这里其实可用的库有很多,本文介绍使用multer实现文件的上传
javascriptconst multer = require("multer") const path = require("path") const fs = require("fs") // 配置存储 const storage = multer.diskStorage({ destination: function (req, file, cb) { // 使用统一的文件存储目录 const uploadDir = path.join(__dirname, `../../uploads`) // 确保目录存在 if (!fs.existsSync(uploadDir)) { fs.mkdirSync(uploadDir, { recursive: true }) } cb(null, uploadDir) }, filename: function (req, file, cb) { // 生成唯一文件名 const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9) cb(null, uniqueSuffix + path.extname(file.originalname)) } }) // 文件过滤器 const fileFilter = (req, file, cb) => { // 允许的文件类型 const allowedTypes = [ 'image/jpeg', 'image/png', 'image/gif', 'application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ] if (allowedTypes.includes(file.mimetype)) { cb(null, true) } else { cb(new Error('不支持的文件类型'), false) } } // 创建上传实例 const upload = multer({ storage: storage, fileFilter: fileFilter, limits: { fileSize: 2 * 1024 * 1024 // 限制2MB } })已折叠
经过上述代码,基本配置好附件上传服务了。 一般到这里,就写接口返回文件的保存路径完事,我之前就是这么简单粗暴的去实现的,然后,,,痛不欲生,哈哈哈
实现附件上传接口
我们先定义附件的数据模型,这里我以我在使用的mongoose为例 一般的,为了提高查询效率,我们可以给关键属性添加索引
Javascriptconst mongoose = require("mongoose") const Schema = mongoose.Schema const FileSchema = new Schema({ name: { type: String, required: true, description: JSON.stringify({ label: "文件名称", render: "input" }) }, size: { type: Number, required: true, description: JSON.stringify({ label: "文件大小(字节)", render: "input" }) }, path: { type: String, required: true, description: JSON.stringify({ label: "文件路径", render: "input" }) }, type: { type: String, required: true, description: JSON.stringify({ label: "文件类型", render: "input" }) }, md5: { type: String, required: true, description: JSON.stringify({ label: "文件MD5", render: "input" }) }, // 引用关系数组 references: [{ module: { type: String, required: true, description: JSON.stringify({ label: "引用模块", render: "input" }) }, field: { type: String, required: true, description: JSON.stringify({ label: "引用字段", render: "input" }) }, documentId: { type: Schema.Types.ObjectId, required: true, description: JSON.stringify({ label: "引用文档ID", render: "input" }) }, createTime: { type: Date, default: Date.now, description: JSON.stringify({ label: "引用时间", render: "datetime" }) } }], isEnabled: { type: Boolean, default: true, description: JSON.stringify({ label: "是否启用", render: "switch", options: ['启用', '禁用'] }) }, uploadTime: { type: Date, default: Date.now, description: JSON.stringify({ label: "上传时间", render: "datetime" }) }, updateTime: { type: Date, default: Date.now, description: JSON.stringify({ label: "更新时间", render: "datetime" }) } }) // 创建索引 FileSchema.index({ name: 1 }) FileSchema.index({ md5: 1 }) FileSchema.index({ uploadTime: -1 }) FileSchema.index({ 'references.module': 1, 'references.field': 1, 'references.documentId': 1 }) const FileModel = mongoose.model("File", FileSchema) module.exports = { FileModel }已折叠
然后开始写上传接口了,需要注意文件路径以及文件名称可能会乱码,反正我是遇到了乱码的情况,加个解码器完事 还有md5值的计算其实比较耗时间,这里不考虑大的文件。因为前面限制了文件大小,所以直接计算整个文件也是没问题的。
Javascriptconst { FileModel } = require("../models/file.model") const iconv = require('iconv-lite') const crypto = require("crypto") // 计算文件MD5 const calculateMD5 = (filePath) => { return new Promise((resolve, reject) => { const hash = crypto.createHash('md5') const stream = fs.createReadStream(filePath) stream.on('data', data => hash.update(data)) stream.on('end', () => resolve(hash.digest('hex'))) stream.on('error', err => reject(err)) }) } const uploadFile = async (req, res) => { try { if (!req.file) { return res.send({ code: 400, message: "请选择要上传的文件" }) } let fileName = req.file.originalname // 检查是否包含乱码字符,不是纯英文,可能需要转码 fileName = iconv.decode(Buffer.from(fileName, 'latin1'), 'utf8') // 计算文件MD5 const md5 = await calculateMD5(req.file.path) // 检查文件是否已存在 let fileRecord = await FileModel.findOne({ md5 }) if (fileRecord) { // 如果文件已存在,删除新上传的文件 fs.unlinkSync(req.file.path) } else { // 创建文件记录 const relativePath = path.relative(path.join(__dirname, '../../'), req.file.path) fileRecord = new FileModel({ name: fileName, size: req.file.size, path: relativePath.replace(/\\/g, '/'), type: req.file.mimetype, md5, uploadTime: new Date(), updateTime: new Date() }) await fileRecord.save() } res.send({ code: 0, data: { fileId: fileRecord._id }, message: "上传成功" }) } catch (error) { // 如果出错,删除已上传的文件 if (req.file) { fs.unlinkSync(req.file.path) } res.send({ code: 500, message: error.message }) } }已折叠
到这里,一个附件完整的上传流程算是走完了。 接下来需要将附件跟具体的业务进行软链接,方便我们后续的统一管理(尤其是删除!!!)
文件与业务的软链接
业务通常是根据模块和业务id来区分的,这里也是一样,使用这个两个属性来定位具体的业务。 附件则是用业务字段来进行软链接,这里也可以是一对多的关系
javascrpit// 绑定文件 // files: [{fileId: 附件id, field: 业务字段}] const bindFile = async (files, module, documentId) => { // 先解绑该documentId下所有的文件引用 await FileModel.updateMany( { 'references.documentId': documentId }, { $pull: { references: { documentId } }, $set: { updateTime: new Date() } } ) if (!files || !Array.isArray(files) || !module || !documentId) { return } // 批量添加新的文件引用 const bulkOps = files.map(file => ({ updateOne: { filter: { _id: file.fileId }, update: { $push: { references: { module, field: file.field, documentId, createTime: new Date() } }, $set: { updateTime: new Date() } } } })) if (bulkOps.length > 0) { await FileModel.bulkWrite(bulkOps) } }已折叠
接口算是写好了,业务中如何使用呢,接下来介绍业务中的用法,其实也很简单。。。
业务中的运用
这里以博客的文章创建为例,只需要在文章成功写入数据库后,调用bindFile()函数,然后文章中的所有附件便可以跟附件系统绑定,将来更新还是删除都可以动态的去更新附件系统中的记录数据。
- 服务器端
javascrpit// 创建文章 const createArticle = async (req, res, next) => { // 创建文章,不包含 bid 字段,让中间件自动生成 const { bid, ...articleData } = req.body const article = await BlogArticleModel.create(articleData) // 绑定文件 await bindFile(req.body.files, 'blogArticle', article._id) // 创建标签关系 await createTagRelations(article) res.send({ code: 0, data: article, message: "创建成功" }) }已折叠
- web客户端
javascrpitconst handleSubmit = (formEl) => { if (!formEl) return formEl.validate((valid) => { if (valid) { form.files = [ { fileId: form.cover, field: "cover" }, ...articleFileIds.value.map(fileId => ({ fileId, field: "content" })) ] emit("submit",form) } }) }
至此,基本实现了附件跟业务的软链接,还提高了附件的使用以及管理效率
路由
javascript// 上传文件 router.post("/upload", upload.single('file'), uploadFile)
评论区