Cập nhật tính năng quản lí dữ liệu mồ côi
This commit is contained in:
@@ -27,6 +27,65 @@ const tempDir = path.join(uploadDir, 'temp');
|
||||
if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true });
|
||||
if (!fs.existsSync(tempDir)) fs.mkdirSync(tempDir, { recursive: true });
|
||||
|
||||
/**
|
||||
* Hàm bổ trợ: Dọn dẹp dữ liệu mồ côi
|
||||
* Được gọi tự động khi xóa user hoặc gọi thủ công từ Admin Dashboard
|
||||
*/
|
||||
const runOrphanedCleanup = async () => {
|
||||
const validUserIds = await User.distinct('_id');
|
||||
|
||||
// 1. Xử lý Scenes mồ côi
|
||||
const orphanedScenes = await Scene.find({ createdBy: { $nin: validUserIds } });
|
||||
const orphanedSceneIds = orphanedScenes.map(s => s._id);
|
||||
|
||||
if (orphanedSceneIds.length > 0) {
|
||||
// Xóa Hotspots liên quan
|
||||
await Hotspot.deleteMany({
|
||||
$or: [
|
||||
{ parent_scene_id: { $in: orphanedSceneIds } },
|
||||
{ target_scene_id: { $in: orphanedSceneIds } }
|
||||
]
|
||||
});
|
||||
// Xóa Scenes
|
||||
await Scene.deleteMany({ _id: { $in: orphanedSceneIds } });
|
||||
}
|
||||
|
||||
// 2. Xử lý Assets mồ côi (Không có owner hoặc không gắn vào Scene nào quá 2h)
|
||||
const usedAssetIds = await Scene.distinct('assetId');
|
||||
const safeDate = new Date(Date.now() - 2 * 3600 * 1000);
|
||||
|
||||
const orphanedAssets = await Asset.find({
|
||||
$or: [
|
||||
{ uploadedBy: { $nin: validUserIds } },
|
||||
{
|
||||
$and: [
|
||||
{ _id: { $nin: usedAssetIds } },
|
||||
{ createdAt: { $lt: safeDate } }
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
let deletedFilesCount = 0;
|
||||
for (const asset of orphanedAssets) {
|
||||
if (asset.filePath && fs.existsSync(asset.filePath)) {
|
||||
try {
|
||||
fs.unlinkSync(asset.filePath);
|
||||
deletedFilesCount++;
|
||||
} catch (e) {
|
||||
console.error(`[Cleanup Error] File: ${asset.filePath}`, e.message);
|
||||
}
|
||||
}
|
||||
await Asset.findByIdAndDelete(asset._id);
|
||||
}
|
||||
|
||||
return {
|
||||
scenesDeleted: orphanedSceneIds.length,
|
||||
assetsDeleted: orphanedAssets.length,
|
||||
filesRemoved: deletedFilesCount
|
||||
};
|
||||
};
|
||||
|
||||
// Configure Multer for temp uploads
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
@@ -91,6 +150,68 @@ router.post('/admin/backup', protect, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route GET /api/admin/maintenance/stray-files
|
||||
* @desc Kiểm tra các file trong thư mục uploads không có bản ghi DB trỏ tới
|
||||
* @access Private (Admin)
|
||||
*/
|
||||
router.get('/admin/maintenance/stray-files', protect, async (req, res) => {
|
||||
if (req.user.role !== 'admin' && req.user.role !== 'Chủ sở hữu') {
|
||||
return res.status(403).json({ message: 'Bạn không có quyền quản trị' });
|
||||
}
|
||||
try {
|
||||
// Đọc danh sách file thực tế (bỏ qua thư mục temp và file ẩn)
|
||||
const filesOnDisk = fs.readdirSync(uploadDir).filter(file => {
|
||||
const fullPath = path.join(uploadDir, file);
|
||||
return fs.lstatSync(fullPath).isFile() && !file.startsWith('.');
|
||||
});
|
||||
|
||||
// Lấy danh sách file trong DB
|
||||
const assets = await Asset.find().select('filePath').lean();
|
||||
const dbFileNames = new Set(assets.map(a => path.basename(a.filePath)));
|
||||
|
||||
// Lọc ra các file mồ côi trên đĩa
|
||||
const strayFiles = filesOnDisk.filter(file => !dbFileNames.has(file));
|
||||
|
||||
res.json({
|
||||
count: strayFiles.length,
|
||||
files: strayFiles
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route POST /api/admin/maintenance/cleanup
|
||||
* @desc Kích hoạt dọn dẹp dữ liệu mồ côi thủ công
|
||||
* @access Private (Admin)
|
||||
*/
|
||||
router.post('/admin/maintenance/cleanup', protect, async (req, res) => {
|
||||
if (req.user.role !== 'admin' && req.user.role !== 'Chủ sở hữu') {
|
||||
return res.status(403).json({ message: 'Forbidden' });
|
||||
}
|
||||
try {
|
||||
// Nếu có tham số deleteStray=true, xóa luôn các file không có trong DB
|
||||
if (req.query.deleteStray === 'true') {
|
||||
const assets = await Asset.find().select('filePath').lean();
|
||||
const dbFileNames = new Set(assets.map(a => path.basename(a.filePath)));
|
||||
const filesOnDisk = fs.readdirSync(uploadDir).filter(f => fs.lstatSync(path.join(uploadDir, f)).isFile() && !f.startsWith('.'));
|
||||
|
||||
filesOnDisk.forEach(file => {
|
||||
if (!dbFileNames.has(file)) {
|
||||
try { fs.unlinkSync(path.join(uploadDir, file)); } catch (e) {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const report = await runOrphanedCleanup();
|
||||
res.json({ message: 'Quy trình dọn dẹp hoàn tất', report });
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route POST /api/admin/restore
|
||||
* @desc Khôi phục hệ thống từ file backup.zip
|
||||
@@ -1140,6 +1261,10 @@ router.delete('/admin/users/:id', protect, async (req, res) => {
|
||||
|
||||
// Lưu ý: Trong thực tế bạn có thể muốn xóa cả các Scene của user này
|
||||
await User.findByIdAndDelete(req.params.id);
|
||||
|
||||
// Tự động dọn dẹp dữ liệu liên quan để tránh rác hệ thống
|
||||
await runOrphanedCleanup();
|
||||
|
||||
res.json({ message: 'Đã xóa người dùng vĩnh viễn' });
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: error.message });
|
||||
|
||||
Reference in New Issue
Block a user