307 lines
6.9 KiB
JavaScript
307 lines
6.9 KiB
JavaScript
const mongoose = require('mongoose');
|
||
|
||
const fileSchema = new mongoose.Schema({
|
||
fileId: {
|
||
type: String,
|
||
required: true,
|
||
unique: true,
|
||
index: true
|
||
},
|
||
userId: {
|
||
type: String,
|
||
required: false, // 允许匿名用户上传
|
||
index: true
|
||
},
|
||
originalName: {
|
||
type: String,
|
||
required: true,
|
||
trim: true
|
||
},
|
||
fileName: {
|
||
type: String,
|
||
required: true,
|
||
unique: true
|
||
},
|
||
fileSize: {
|
||
type: Number,
|
||
required: true,
|
||
min: 0
|
||
},
|
||
mimeType: {
|
||
type: String,
|
||
required: true,
|
||
enum: [
|
||
'application/pdf',
|
||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||
'text/html',
|
||
'text/plain',
|
||
'image/png',
|
||
'image/jpeg'
|
||
]
|
||
},
|
||
filePath: {
|
||
type: String,
|
||
required: true
|
||
},
|
||
status: {
|
||
type: String,
|
||
enum: ['uploading', 'ready', 'processing', 'completed', 'failed', 'deleted'],
|
||
default: 'ready'
|
||
},
|
||
metadata: {
|
||
pageCount: Number,
|
||
width: Number,
|
||
height: Number,
|
||
hasImages: Boolean,
|
||
hasText: Boolean,
|
||
isEncrypted: Boolean,
|
||
pdfVersion: String,
|
||
creationDate: Date,
|
||
modificationDate: Date,
|
||
author: String,
|
||
title: String,
|
||
subject: String,
|
||
keywords: String
|
||
},
|
||
checksum: {
|
||
type: String,
|
||
index: true // 用于去重
|
||
},
|
||
downloadCount: {
|
||
type: Number,
|
||
default: 0
|
||
},
|
||
lastDownloaded: Date,
|
||
tags: [String],
|
||
isPublic: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
expiresAt: {
|
||
type: Date,
|
||
index: { expireAfterSeconds: 0 } // MongoDB TTL索引,自动删除过期文件
|
||
}
|
||
}, {
|
||
timestamps: true
|
||
});
|
||
|
||
// 复合索引
|
||
fileSchema.index({ userId: 1, createdAt: -1 });
|
||
fileSchema.index({ status: 1, createdAt: -1 });
|
||
fileSchema.index({ mimeType: 1, status: 1 });
|
||
fileSchema.index({ fileSize: 1 });
|
||
|
||
// 虚拟字段:格式化文件大小
|
||
fileSchema.virtual('formattedFileSize').get(function() {
|
||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||
if (this.fileSize === 0) return '0 Bytes';
|
||
|
||
const i = Math.floor(Math.log(this.fileSize) / Math.log(1024));
|
||
const size = (this.fileSize / Math.pow(1024, i)).toFixed(1);
|
||
return size + ' ' + sizes[i];
|
||
});
|
||
|
||
// 虚拟字段:文件扩展名
|
||
fileSchema.virtual('fileExtension').get(function() {
|
||
const mimeToExt = {
|
||
'application/pdf': 'pdf',
|
||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx',
|
||
'text/html': 'html',
|
||
'text/plain': 'txt',
|
||
'image/png': 'png',
|
||
'image/jpeg': 'jpg'
|
||
};
|
||
return mimeToExt[this.mimeType] || 'unknown';
|
||
});
|
||
|
||
// 虚拟字段:是否已过期
|
||
fileSchema.virtual('isExpired').get(function() {
|
||
if (!this.expiresAt) return false;
|
||
return new Date() > this.expiresAt;
|
||
});
|
||
|
||
// 实例方法:设置过期时间
|
||
fileSchema.methods.setExpiration = function(hours = 24) {
|
||
this.expiresAt = new Date(Date.now() + hours * 60 * 60 * 1000);
|
||
return this.save();
|
||
};
|
||
|
||
// 实例方法:增加下载次数
|
||
fileSchema.methods.incrementDownload = function() {
|
||
this.downloadCount += 1;
|
||
this.lastDownloaded = new Date();
|
||
return this.save();
|
||
};
|
||
|
||
// 实例方法:更新状态
|
||
fileSchema.methods.updateStatus = function(status) {
|
||
this.status = status;
|
||
return this.save();
|
||
};
|
||
|
||
// 静态方法:根据校验和查找重复文件
|
||
fileSchema.statics.findByChecksum = function(checksum) {
|
||
return this.findOne({ checksum, status: { $ne: 'deleted' } });
|
||
};
|
||
|
||
// 静态方法:获取用户文件
|
||
fileSchema.statics.getUserFiles = function(userId, options = {}) {
|
||
const {
|
||
page = 1,
|
||
limit = 10,
|
||
status,
|
||
mimeType,
|
||
sortBy = 'createdAt',
|
||
sortOrder = -1
|
||
} = options;
|
||
|
||
const query = { userId };
|
||
|
||
if (status) query.status = status;
|
||
if (mimeType) query.mimeType = mimeType;
|
||
|
||
const skip = (page - 1) * limit;
|
||
const sort = { [sortBy]: sortOrder };
|
||
|
||
return this.find(query)
|
||
.sort(sort)
|
||
.skip(skip)
|
||
.limit(limit)
|
||
.lean();
|
||
};
|
||
|
||
// 静态方法:清理过期文件
|
||
fileSchema.statics.cleanupExpiredFiles = async function() {
|
||
const expiredFiles = await this.find({
|
||
expiresAt: { $lt: new Date() },
|
||
status: { $ne: 'deleted' }
|
||
});
|
||
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
let cleanedCount = 0;
|
||
let freedSpace = 0;
|
||
|
||
for (const file of expiredFiles) {
|
||
try {
|
||
// 删除物理文件
|
||
if (fs.existsSync(file.filePath)) {
|
||
freedSpace += file.fileSize;
|
||
fs.unlinkSync(file.filePath);
|
||
}
|
||
|
||
// 更新数据库记录
|
||
await this.findByIdAndUpdate(file._id, { status: 'deleted' });
|
||
cleanedCount++;
|
||
} catch (error) {
|
||
console.error(`清理文件失败: ${file.fileName}`, error);
|
||
}
|
||
}
|
||
|
||
return { cleanedCount, freedSpace };
|
||
};
|
||
|
||
// 静态方法:获取文件统计
|
||
fileSchema.statics.getFileStats = async function() {
|
||
const pipeline = [
|
||
{
|
||
$group: {
|
||
_id: '$status',
|
||
count: { $sum: 1 },
|
||
totalSize: { $sum: '$fileSize' }
|
||
}
|
||
}
|
||
];
|
||
|
||
const statusStats = await this.aggregate(pipeline);
|
||
|
||
const typeStats = await this.aggregate([
|
||
{
|
||
$group: {
|
||
_id: '$mimeType',
|
||
count: { $sum: 1 },
|
||
totalSize: { $sum: '$fileSize' }
|
||
}
|
||
}
|
||
]);
|
||
|
||
const totalStats = await this.aggregate([
|
||
{
|
||
$group: {
|
||
_id: null,
|
||
totalFiles: { $sum: 1 },
|
||
totalSize: { $sum: '$fileSize' },
|
||
totalDownloads: { $sum: '$downloadCount' }
|
||
}
|
||
}
|
||
]);
|
||
|
||
return {
|
||
byStatus: statusStats,
|
||
byType: typeStats,
|
||
total: totalStats[0] || { totalFiles: 0, totalSize: 0, totalDownloads: 0 }
|
||
};
|
||
};
|
||
|
||
// 静态方法:搜索文件
|
||
fileSchema.statics.searchFiles = function(searchTerm, options = {}) {
|
||
const {
|
||
userId,
|
||
page = 1,
|
||
limit = 10,
|
||
status = { $ne: 'deleted' }
|
||
} = options;
|
||
|
||
const query = {
|
||
status,
|
||
$or: [
|
||
{ originalName: { $regex: searchTerm, $options: 'i' } },
|
||
{ tags: { $in: [new RegExp(searchTerm, 'i')] } },
|
||
{ 'metadata.title': { $regex: searchTerm, $options: 'i' } },
|
||
{ 'metadata.subject': { $regex: searchTerm, $options: 'i' } }
|
||
]
|
||
};
|
||
|
||
if (userId) query.userId = userId;
|
||
|
||
const skip = (page - 1) * limit;
|
||
|
||
return this.find(query)
|
||
.sort({ createdAt: -1 })
|
||
.skip(skip)
|
||
.limit(limit)
|
||
.lean();
|
||
};
|
||
|
||
// 中间件:删除前清理物理文件
|
||
fileSchema.pre('remove', async function(next) {
|
||
try {
|
||
const fs = require('fs');
|
||
if (fs.existsSync(this.filePath)) {
|
||
fs.unlinkSync(this.filePath);
|
||
}
|
||
next();
|
||
} catch (error) {
|
||
console.error('删除物理文件失败:', error);
|
||
next(error);
|
||
}
|
||
});
|
||
|
||
// 中间件:保存后更新用户统计
|
||
fileSchema.post('save', async function(doc) {
|
||
if (doc.userId && doc.isNew) {
|
||
try {
|
||
const User = require('./User');
|
||
await User.findOneAndUpdate(
|
||
{ userId: doc.userId },
|
||
{ $inc: { 'statistics.totalFileSize': doc.fileSize } }
|
||
);
|
||
} catch (error) {
|
||
console.error('更新用户统计失败:', error);
|
||
}
|
||
}
|
||
});
|
||
|
||
const File = mongoose.model('File', fileSchema);
|
||
|
||
module.exports = File; |