Chỉnh sửa phần upload ảnh
This commit is contained in:
+28
-17
@@ -15,6 +15,7 @@ const { protect, optionalAuth } = require('../middlewares/authMiddleware');
|
||||
const { verifyReferer, setNoCacheHeaders } = require('../middlewares/securityMiddleware');
|
||||
const { resizeTo8K } = require('../utils/imageHelper');
|
||||
const { getGPSCoordinates, injectGPSCoordinates } = require('../utils/exifHelper');
|
||||
const { imageQueue } = require('./imageQueue');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -37,11 +38,16 @@ const storage = multer.diskStorage({
|
||||
const upload = multer({
|
||||
storage: storage,
|
||||
fileFilter: (req, file, cb) => {
|
||||
// Only accept images
|
||||
if (file.mimetype.startsWith('image/')) {
|
||||
// Chỉ chấp nhận các định dạng ảnh phổ biến đã được xử lý (Stitched)
|
||||
const filetypes = /jpeg|jpg|png/;
|
||||
const extname = filetypes.test(path.extname(file.originalname).toLowerCase());
|
||||
const mimetype = filetypes.test(file.mimetype) ||
|
||||
file.mimetype === 'application/octet-stream'; // Đôi khi trình duyệt gửi JPG dưới dạng octet-stream
|
||||
|
||||
if (mimetype && extname) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('Only image files are allowed!'), false);
|
||||
cb(new Error('Chỉ chấp nhận các định dạng ảnh JPEG, PNG, DNG hoặc INSP!'), false);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -93,21 +99,14 @@ router.post('/scenes', protect, uploadSinglePanorama, async (req, res) => {
|
||||
const tempFilePath = req.file.path;
|
||||
const processedFileName = `processed_${req.file.filename}.jpg`;
|
||||
const processedFilePath = path.join(uploadDir, processedFileName);
|
||||
|
||||
// Lấy tọa độ GPS gốc từ ảnh vừa upload trước khi nén/xử lý
|
||||
|
||||
// Lấy tọa độ GPS gốc từ metadata
|
||||
const originalGPS = await getGPSCoordinates(tempFilePath);
|
||||
|
||||
// Thực hiện xử lý ảnh tuần tự để đảm bảo file được tạo thành công trước khi lưu DB
|
||||
// 1. Resize to 8K (Sẽ convert từ DNG sang JPG nếu sharp hỗ trợ libraw, nếu không sẽ báo lỗi)
|
||||
await resizeTo8K(tempFilePath, processedFilePath);
|
||||
// 2. Inject GPS vào file JPG vừa tạo
|
||||
await injectGPSCoordinates(processedFilePath, latitude, longitude);
|
||||
// 3. Cleanup temp file (file gốc dng)
|
||||
if (fs.existsSync(tempFilePath)) fs.unlinkSync(tempFilePath);
|
||||
const ext = path.extname(req.file.originalname).toLowerCase();
|
||||
|
||||
// 5. Save Asset to DB
|
||||
const asset = new Asset({
|
||||
filePath: processedFilePath,
|
||||
filePath: tempFilePath, // Tạm thời dùng file gốc cho đến khi worker xử lý xong
|
||||
uploadedBy: req.user._id,
|
||||
coordinates: originalGPS ? { lat: originalGPS.lat, lng: originalGPS.lng } : { lat: latitude, lng: longitude }
|
||||
});
|
||||
@@ -133,7 +132,7 @@ router.post('/scenes', protect, uploadSinglePanorama, async (req, res) => {
|
||||
const scene = new Scene({
|
||||
name: title,
|
||||
assetId: asset._id,
|
||||
scene_url: processedFilePath, // Lưu đường dẫn ảnh trực tiếp
|
||||
scene_url: tempFilePath, // Tạm thời
|
||||
gps: {
|
||||
lat: latitude,
|
||||
lng: longitude
|
||||
@@ -141,12 +140,24 @@ router.post('/scenes', protect, uploadSinglePanorama, async (req, res) => {
|
||||
createdBy: req.user._id,
|
||||
privacy: privacy || 'private',
|
||||
shareToken,
|
||||
sharedWith: parsedSharedWith
|
||||
sharedWith: parsedSharedWith,
|
||||
status: 'processing' // Đánh dấu đang xử lý
|
||||
});
|
||||
await scene.save();
|
||||
|
||||
// Đẩy tác vụ xử lý ảnh (Stitch + Resize) vào hàng đợi BullMQ
|
||||
// Loại bỏ needsStitch và rotation vì người dùng đã stitch ảnh thủ công
|
||||
await imageQueue.add('process-panorama', {
|
||||
tempFilePath,
|
||||
processedFilePath,
|
||||
latitude,
|
||||
longitude,
|
||||
assetId: asset._id,
|
||||
sceneId: scene._id
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
message: 'Scene created successfully',
|
||||
message: 'Scene đã được tạo! Ảnh đang được xử lý 8K ngầm...',
|
||||
scene
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
const { Queue } = require('bullmq');
|
||||
const IORedis = require('ioredis');
|
||||
|
||||
// Cấu hình kết nối Redis (Mặc định localhost:6379)
|
||||
const connection = new IORedis({
|
||||
maxRetriesPerRequest: null
|
||||
});
|
||||
|
||||
/**
|
||||
* Khởi tạo hàng đợi xử lý ảnh
|
||||
*/
|
||||
const imageQueue = new Queue('image-processing', {
|
||||
connection,
|
||||
defaultJobOptions: {
|
||||
attempts: 3, // Thử lại tối đa 3 lần nếu lỗi
|
||||
backoff: { type: 'exponential', delay: 5000 },
|
||||
// Tự động dọn dẹp Job để tối ưu bộ nhớ Redis
|
||||
removeOnComplete: {
|
||||
age: 3600, // Xóa các job hoàn thành sau 1 giờ (3600 giây)
|
||||
count: 100 // Hoặc giữ tối đa 100 job hoàn thành gần nhất
|
||||
},
|
||||
removeOnFail: {
|
||||
age: 24 * 3600, // Giữ lại job lỗi trong 24 giờ để admin kiểm tra
|
||||
count: 500 // Giữ tối đa 500 job lỗi
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = { imageQueue, connection };
|
||||
@@ -0,0 +1,54 @@
|
||||
const { Worker } = require('bullmq');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { connection } = require('./imageQueue');
|
||||
const { resizeTo8K } = require('../utils/imageHelper');
|
||||
const { injectGPSCoordinates } = require('../utils/exifHelper');
|
||||
const Asset = require('../models/Asset');
|
||||
const Scene = require('../models/Scene');
|
||||
|
||||
/**
|
||||
* Khởi tạo Worker để xử lý hàng đợi 'image-processing'
|
||||
*/
|
||||
const imageWorker = new Worker('image-processing', async (job) => {
|
||||
const { tempFilePath, processedFilePath, latitude, longitude, assetId, sceneId } = job.data;
|
||||
|
||||
console.log(`[Worker] Bắt đầu xử lý Job ${job.id} cho Scene: ${sceneId}`);
|
||||
|
||||
try {
|
||||
// Quy trình tối giản: Chỉ Resize ảnh sang 8K (Sharp)
|
||||
// Vì người dùng đã stitch ảnh từ Insta360 Studio, file upload đã là Equirectangular JPEG.
|
||||
await resizeTo8K(tempFilePath, processedFilePath);
|
||||
|
||||
// 3. Chèn GPS Metadata
|
||||
await injectGPSCoordinates(processedFilePath, latitude, longitude);
|
||||
|
||||
// 4. Cập nhật đường dẫn file thực tế vào Database
|
||||
// Lúc này ảnh đã sẵn sàng để phục vụ (8K)
|
||||
await Asset.findByIdAndUpdate(assetId, { filePath: processedFilePath });
|
||||
await Scene.findByIdAndUpdate(sceneId, {
|
||||
scene_url: processedFilePath,
|
||||
status: 'completed' // Xử lý xong
|
||||
});
|
||||
|
||||
// 5. Dọn dẹp file tạm
|
||||
if (fs.existsSync(tempFilePath)) fs.unlinkSync(tempFilePath);
|
||||
|
||||
console.log(`[Worker] Hoàn tất xử lý Job ${job.id}`);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error(`[Worker Error] Job ${job.id} thất bại:`, error.message);
|
||||
await Scene.findByIdAndUpdate(sceneId, { status: 'failed' }); // Đánh dấu lỗi
|
||||
throw error; // Đẩy lỗi để BullMQ thực hiện retry
|
||||
}
|
||||
}, { connection });
|
||||
|
||||
imageWorker.on('completed', (job) => {
|
||||
console.log(`Job ${job.id} đã hoàn thành thành công.`);
|
||||
});
|
||||
|
||||
imageWorker.on('failed', (job, err) => {
|
||||
console.error(`Job ${job.id} thất bại sau nhiều lần thử: ${err.message}`);
|
||||
});
|
||||
|
||||
module.exports = imageWorker;
|
||||
Reference in New Issue
Block a user