Xóa scene con mà không xóa scene cha
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const sharp = require('sharp');
|
||||
|
||||
const Asset = require('../models/Asset');
|
||||
const Scene = require('../models/Scene');
|
||||
const Hotspot = require('../models/Hotspot');
|
||||
const { protect, optionalAuth } = require('../middlewares/authMiddleware');
|
||||
const { verifyReferer } = require('../middlewares/securityMiddleware');
|
||||
const { deleteSceneCascade } = require('../utils/sceneHelper');
|
||||
const { logActivity } = require('../utils/logger');
|
||||
|
||||
// Chuẩn hóa đường dẫn uploads (Giai đoạn 1)
|
||||
const uploadDir = process.env.UPLOAD_DIR
|
||||
? path.resolve(process.env.UPLOAD_DIR)
|
||||
: path.join(__dirname, '../uploads');
|
||||
|
||||
/**
|
||||
* @route GET /api/assets/view/:assetId
|
||||
* @desc Stream ảnh panorama (Có Referer & Token Verification)
|
||||
*/
|
||||
router.get('/assets/view/:assetId', verifyReferer, optionalAuth, async (req, res) => {
|
||||
try {
|
||||
const asset = await Asset.findById(req.params.assetId);
|
||||
if (!asset) return res.status(404).json({ message: 'Asset not found' });
|
||||
|
||||
// Kiểm tra quyền truy cập dựa trên Privacy của Scene liên kết
|
||||
const scene = await Scene.findOne({ assetId: asset._id });
|
||||
if (!scene) {
|
||||
// Asset mồ côi, chỉ chủ sở hữu được xem
|
||||
if (!req.user || req.user._id.toString() !== asset.uploadedBy.toString()) {
|
||||
return res.status(403).json({ message: 'Access denied' });
|
||||
}
|
||||
} else {
|
||||
const isTokenValid = scene.shareToken && (!scene.shareTokenExpires || new Date() < scene.shareTokenExpires);
|
||||
const userEmail = req.user ? req.user.email : null;
|
||||
const hasAccess = scene.privacy === 'public' ||
|
||||
(scene.privacy === 'member' && req.user && (scene.sharedWith.includes(req.user._id) || (userEmail && scene.sharedEmails.includes(userEmail)))) ||
|
||||
(req.user && scene.createdBy.toString() === req.user._id.toString()) ||
|
||||
(scene.privacy === 'shared' && req.query.token === scene.shareToken && isTokenValid);
|
||||
|
||||
if (!hasAccess) return res.status(403).json({ message: 'Access denied' });
|
||||
}
|
||||
|
||||
// Kiểm tra file vật lý (Giai đoạn 2 - Async)
|
||||
try {
|
||||
await fs.promises.access(asset.filePath);
|
||||
} catch (e) {
|
||||
return res.status(404).json({ message: 'Physical file not found on disk' });
|
||||
}
|
||||
|
||||
const resolvedPath = path.resolve(asset.filePath);
|
||||
|
||||
// Xử lý Watermark cho mạng xã hội hoặc yêu cầu thủ công
|
||||
const userAgent = req.headers['user-agent'] || '';
|
||||
const isSocialBot = /facebookexternalhit|Facebot|ZaloBot|Twitterbot|Slackbot|LinkedInBot|Embedly/i.test(userAgent);
|
||||
|
||||
if (isSocialBot || req.query.watermark === 'true') {
|
||||
const iconPath = path.join(__dirname, '../assets/static/360-badge.png');
|
||||
if (fs.existsSync(iconPath)) {
|
||||
try {
|
||||
const buffer = await sharp(resolvedPath)
|
||||
.resize(1200, 1200, { fit: 'cover' })
|
||||
.composite([{ input: iconPath, gravity: 'center' }])
|
||||
.jpeg({ quality: 90 })
|
||||
.toBuffer();
|
||||
res.set({
|
||||
'Content-Type': 'image/jpeg',
|
||||
'Cache-Control': 'public, max-age=2592000'
|
||||
});
|
||||
return res.send(buffer);
|
||||
} catch (e) {
|
||||
console.error("[Assets] Watermark processing failed:", e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stream file với caching tốt
|
||||
res.sendFile(resolvedPath, {
|
||||
maxAge: 2592000000, // 30 ngày
|
||||
headers: {
|
||||
'Content-Type': 'image/jpeg',
|
||||
'Cache-Control': 'public, max-age=2592000, immutable'
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route GET /api/assets/view_avatar/:filename
|
||||
* @desc Stream ảnh đại diện
|
||||
*/
|
||||
router.get('/assets/view_avatar/:filename', async (req, res) => {
|
||||
try {
|
||||
const avatarPath = path.join(uploadDir, req.params.filename);
|
||||
try {
|
||||
await fs.promises.access(avatarPath);
|
||||
} catch (e) {
|
||||
return res.status(404).json({ message: 'Avatar not found' });
|
||||
}
|
||||
res.sendFile(avatarPath, {
|
||||
maxAge: 2592000000,
|
||||
headers: { 'Content-Type': 'image/jpeg' }
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route GET /api/me/assets
|
||||
* @desc Kho ảnh của tôi (Dùng cho Dashboard Media Library)
|
||||
*/
|
||||
router.get('/me/assets', protect, async (req, res) => {
|
||||
try {
|
||||
const query = (req.user.role === 'admin' || req.user.role === 'Chủ sở hữu') ? {} : { uploadedBy: req.user._id };
|
||||
|
||||
const assets = await Asset.aggregate([
|
||||
{ $match: query },
|
||||
{ $lookup: { from: 'scenes', localField: '_id', foreignField: 'assetId', as: 'linkedScene' } },
|
||||
{ $unwind: { path: '$linkedScene', preserveNullAndEmptyArrays: true } },
|
||||
{ $lookup: { from: 'hotspots', localField: 'linkedScene._id', foreignField: 'target_scene_id', as: 'incomingHotspots' } },
|
||||
{ $lookup: { from: 'scenes', localField: 'incomingHotspots.parent_scene_id', foreignField: '_id', as: 'parentScenes' } },
|
||||
{ $project: { filePath: 0 } },
|
||||
{ $sort: { createdAt: -1 } }
|
||||
]);
|
||||
res.json(assets);
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route GET /api/me/assets/top-large
|
||||
* @desc Thống kê 5 file chiếm dung lượng lớn nhất
|
||||
*/
|
||||
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 });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route DELETE /api/assets/:id
|
||||
* @desc Xóa file vật lý và bản ghi (kèm Scene liên quan)
|
||||
*/
|
||||
router.delete('/assets/:id', protect, async (req, res) => {
|
||||
try {
|
||||
const asset = await Asset.findById(req.params.id);
|
||||
if (!asset) return res.status(404).json({ message: 'Asset not found' });
|
||||
|
||||
if (asset.uploadedBy.toString() !== req.user._id.toString() && req.user.role !== 'admin') {
|
||||
return res.status(403).json({ message: 'Bạn không có quyền xóa tập tin này' });
|
||||
}
|
||||
|
||||
const linkedScene = await Scene.findOne({ assetId: asset._id });
|
||||
if (linkedScene) {
|
||||
await deleteSceneCascade(linkedScene._id, req.user.username);
|
||||
} else {
|
||||
// Nếu là asset mồ côi (không gắn scene)
|
||||
if (asset.filePath) await fs.promises.unlink(asset.filePath).catch(() => {});
|
||||
await Asset.findByIdAndDelete(req.params.id);
|
||||
await logActivity('ORPHAN_ASSET_DELETE', { assetId: req.params.id }, req.user.username);
|
||||
}
|
||||
|
||||
res.json({ message: 'Đã xóa ảnh và dữ liệu liên quan thành công' });
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user