Files
pdf-tools/server/models/File.js

307 lines
6.9 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;