feat: Initial commit of PDF Tools project

This commit is contained in:
2025-08-25 02:29:48 +08:00
parent af6827cd9e
commit 30180e50a2
48 changed files with 36364 additions and 1 deletions

307
server/models/File.js Normal file
View File

@@ -0,0 +1,307 @@
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;