Cài đặt quota cho các thành viên
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
const Asset = require('../models/Asset');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Cấu hình Quota cho từng nhóm người dùng (đơn vị: Bytes)
|
||||
const ROLE_QUOTAS = {
|
||||
'Thành viên': 2 * 1024 * 1024 * 1024, // 2GB
|
||||
'editor': 10 * 1024 * 1024 * 1024, // 10GB
|
||||
'admin': 100 * 1024 * 1024 * 1024, // 100GB (hoặc Infinity)
|
||||
'Chủ sở hữu': Infinity // Không giới hạn
|
||||
};
|
||||
|
||||
/**
|
||||
* Middleware kiểm tra giới hạn lưu trữ của người dùng
|
||||
*/
|
||||
const checkQuota = async (req, res, next) => {
|
||||
if (!req.user) return res.status(401).json({ message: 'Unauthorized' });
|
||||
|
||||
const userRole = req.user.role || 'Thành viên';
|
||||
const quota = ROLE_QUOTAS[userRole] || ROLE_QUOTAS['Thành viên'];
|
||||
|
||||
// Nếu không giới hạn thì đi tiếp
|
||||
if (quota === Infinity) return next();
|
||||
|
||||
try {
|
||||
// Sử dụng MongoDB Aggregation để tính tổng dung lượng ngay trên database
|
||||
const usageResult = await Asset.aggregate([
|
||||
{ $match: { uploadedBy: req.user._id } },
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
totalUsage: { $sum: { $ifNull: ["$fileSize", 0] } }
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
const currentUsage = usageResult.length > 0 ? usageResult[0].totalUsage : 0;
|
||||
|
||||
const newFileSize = req.file ? req.file.size : 0;
|
||||
|
||||
if (currentUsage + newFileSize > quota) {
|
||||
// Xóa file tạm vừa upload lên nếu vượt định mức
|
||||
if (req.file && fs.existsSync(req.file.path)) fs.unlinkSync(req.file.path);
|
||||
|
||||
return res.status(403).json({
|
||||
message: `Vượt quá giới hạn lưu trữ. Định mức của bạn là ${(quota / (1024**3)).toFixed(1)}GB. Bạn đã sử dụng ${(currentUsage / (1024**3)).toFixed(2)}GB.`
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
console.error('[Quota Check Error]:', error);
|
||||
next(); // Cho phép đi tiếp nếu lỗi logic kiểm tra để tránh chặn người dùng oan
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { checkQuota, ROLE_QUOTAS };
|
||||
@@ -13,6 +13,7 @@ const Setting = require('../models/Setting');
|
||||
|
||||
const { protect, optionalAuth } = require('../middlewares/authMiddleware');
|
||||
const { verifyReferer, setNoCacheHeaders } = require('../middlewares/securityMiddleware');
|
||||
const { checkQuota, ROLE_QUOTAS } = require('../middlewares/quotaMiddleware');
|
||||
const { resizeTo8K } = require('../utils/imageHelper');
|
||||
const { getGPSCoordinates, injectGPSCoordinates } = require('../utils/exifHelper');
|
||||
const { imageQueue } = require('./imageQueue');
|
||||
@@ -78,7 +79,7 @@ const uploadSinglePanorama = (req, res, next) => {
|
||||
* @desc Create a new 3D scene (with 360 photo, 8K resize, EXIF injection)
|
||||
* @access Private (Registered Users)
|
||||
*/
|
||||
router.post('/scenes', protect, uploadSinglePanorama, async (req, res) => {
|
||||
router.post('/scenes', protect, uploadSinglePanorama, checkQuota, async (req, res) => {
|
||||
try {
|
||||
const { title, lat, lng, privacy, sharedWithUsers } = req.body;
|
||||
|
||||
@@ -107,6 +108,7 @@ router.post('/scenes', protect, uploadSinglePanorama, async (req, res) => {
|
||||
// 5. Save Asset to DB
|
||||
const asset = new Asset({
|
||||
filePath: tempFilePath, // Tạm thời dùng file gốc cho đến khi worker xử lý xong
|
||||
fileSize: req.file.size,
|
||||
uploadedBy: req.user._id,
|
||||
coordinates: originalGPS ? { lat: originalGPS.lat, lng: originalGPS.lng } : { lat: latitude, lng: longitude }
|
||||
});
|
||||
@@ -740,8 +742,65 @@ router.delete('/scenes/:id', protect, async (req, res) => {
|
||||
*/
|
||||
router.get('/me/profile', protect, async (req, res) => {
|
||||
try {
|
||||
const user = await User.findById(req.user._id).select('-password');
|
||||
res.json(user);
|
||||
const user = await User.findById(req.user._id).select('-password').lean();
|
||||
|
||||
// Tính toán dung lượng thực tế của người dùng
|
||||
const usageResult = await Asset.aggregate([
|
||||
{ $match: { uploadedBy: req.user._id } },
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
totalUsage: { $sum: { $ifNull: ["$fileSize", 0] } }
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
const currentUsage = usageResult.length > 0 ? usageResult[0].totalUsage : 0;
|
||||
const quota = ROLE_QUOTAS[user.role] || ROLE_QUOTAS['Thành viên'];
|
||||
|
||||
res.json({
|
||||
...user,
|
||||
storage: {
|
||||
used: currentUsage,
|
||||
quota: quota === Infinity ? -1 : quota // -1 đại diện cho không giới hạn
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route GET /api/me/assets/top-large
|
||||
* @desc Lấy danh sách 5 tệp tin chiếm dung lượng lớn nhất của người dùng
|
||||
* @access Private
|
||||
*/
|
||||
router.get('/me/assets/top-large', protect, async (req, res) => {
|
||||
try {
|
||||
const topAssets = await Asset.aggregate([
|
||||
{ $match: { uploadedBy: req.user._id } },
|
||||
{ $sort: { fileSize: -1 } },
|
||||
{ $limit: 5 },
|
||||
{
|
||||
$lookup: {
|
||||
from: 'scenes',
|
||||
localField: '_id',
|
||||
foreignField: 'assetId',
|
||||
as: 'scene'
|
||||
}
|
||||
},
|
||||
{ $unwind: { path: '$scene', preserveNullAndEmptyArrays: true } },
|
||||
{
|
||||
$project: {
|
||||
fileSize: 1,
|
||||
createdAt: 1,
|
||||
'scene.name': 1,
|
||||
'scene.title': 1,
|
||||
'scene._id': 1
|
||||
}
|
||||
}
|
||||
]);
|
||||
res.json(topAssets);
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: error.message });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
const mongoose = require('mongoose');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const connectDB = require('../config/db');
|
||||
const Asset = require('../models/Asset');
|
||||
|
||||
/**
|
||||
* Script cập nhật bổ sung trường fileSize cho các Asset cũ
|
||||
* Giúp tối ưu hóa việc kiểm tra Quota và thống kê dung lượng
|
||||
*/
|
||||
const fixAssetFileSize = async () => {
|
||||
try {
|
||||
console.log('=== BẮT ĐẦU CẬP NHẬT KÍCH THƯỚC FILE CHO ASSET ===');
|
||||
await connectDB();
|
||||
|
||||
// 1. Tìm các Asset chưa có trường fileSize hoặc fileSize bằng null/0
|
||||
const assetsToFix = await Asset.find({
|
||||
$or: [
|
||||
{ fileSize: { $exists: false } },
|
||||
{ fileSize: null },
|
||||
{ fileSize: 0 }
|
||||
]
|
||||
});
|
||||
|
||||
console.log(`- Tìm thấy ${assetsToFix.length} bản ghi cần cập nhật.`);
|
||||
|
||||
let successCount = 0;
|
||||
let errorCount = 0;
|
||||
let missingFileCount = 0;
|
||||
|
||||
for (const asset of assetsToFix) {
|
||||
if (asset.filePath && fs.existsSync(asset.filePath)) {
|
||||
try {
|
||||
const stats = fs.statSync(asset.filePath);
|
||||
asset.fileSize = stats.size;
|
||||
await asset.save();
|
||||
successCount++;
|
||||
|
||||
if (successCount % 10 === 0) {
|
||||
console.log(` [Progress] Đã cập nhật ${successCount} file...`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(` [Lỗi] Không thể đọc stats cho Asset ${asset._id}: ${err.message}`);
|
||||
errorCount++;
|
||||
}
|
||||
} else {
|
||||
console.warn(` [Cảnh báo] Không tìm thấy file vật lý cho Asset ${asset._id}: ${asset.filePath}`);
|
||||
missingFileCount++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n=== TỔNG KẾT QUÁ TRÌNH ===');
|
||||
console.log(`- Thành công: ${successCount}`);
|
||||
console.log(`- Lỗi đọc file: ${errorCount}`);
|
||||
console.log(`- File không tồn tại trên đĩa: ${missingFileCount}`);
|
||||
console.log('==========================================');
|
||||
|
||||
mongoose.connection.close();
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('Lỗi nghiêm trọng:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
fixAssetFileSize();
|
||||
@@ -50,10 +50,10 @@ const getStorageStats = async () => {
|
||||
for (const asset of assets) {
|
||||
const userId = asset.uploadedBy ? asset.uploadedBy.toString() : 'unknown';
|
||||
|
||||
if (asset.filePath && fs.existsSync(asset.filePath)) {
|
||||
const fileStat = fs.statSync(asset.filePath);
|
||||
if (asset.fileSize || (asset.filePath && fs.existsSync(asset.filePath))) {
|
||||
const size = asset.fileSize || fs.statSync(asset.filePath).size;
|
||||
if (stats[userId]) {
|
||||
stats[userId].totalBytes += fileStat.size;
|
||||
stats[userId].totalBytes += size;
|
||||
stats[userId].fileCount += 1;
|
||||
} else {
|
||||
stats['unknown'].totalBytes += fileStat.size;
|
||||
|
||||
Reference in New Issue
Block a user