Tạo scripts backup dữ liệu
This commit is contained in:
Binary file not shown.
@@ -4,6 +4,7 @@ const path = require('path');
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const sharp = require('sharp');
|
const sharp = require('sharp');
|
||||||
|
const AdmZip = require('adm-zip');
|
||||||
|
|
||||||
const User = require('../models/User');
|
const User = require('../models/User');
|
||||||
const Asset = require('../models/Asset');
|
const Asset = require('../models/Asset');
|
||||||
@@ -53,6 +54,83 @@ const upload = multer({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route POST /api/admin/backup
|
||||||
|
* @desc Tạo bản sao lưu toàn bộ hệ thống (DB + Uploads)
|
||||||
|
* @access Private (Admin)
|
||||||
|
*/
|
||||||
|
router.post('/admin/backup', protect, async (req, res) => {
|
||||||
|
if (req.user.role !== 'admin' && req.user.role !== 'Chủ sở hữu') {
|
||||||
|
return res.status(403).json({ message: 'Forbidden' });
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const zip = new AdmZip();
|
||||||
|
// 1. Export Database
|
||||||
|
const dbData = {
|
||||||
|
users: await User.find().lean(),
|
||||||
|
assets: await Asset.find().lean(),
|
||||||
|
scenes: await Scene.find().lean(),
|
||||||
|
hotspots: await Hotspot.find().lean(),
|
||||||
|
settings: await Setting.find().lean()
|
||||||
|
};
|
||||||
|
zip.addFile("database.json", Buffer.from(JSON.stringify(dbData, null, 2), "utf8"));
|
||||||
|
|
||||||
|
// 2. Add Uploads folder
|
||||||
|
if (fs.existsSync(uploadDir)) {
|
||||||
|
zip.addLocalFolder(uploadDir, "uploads");
|
||||||
|
}
|
||||||
|
|
||||||
|
const buffer = zip.toBuffer();
|
||||||
|
res.set({
|
||||||
|
'Content-Type': 'application/zip',
|
||||||
|
'Content-Disposition': 'attachment; filename="backup_3dtour.zip"'
|
||||||
|
});
|
||||||
|
res.send(buffer);
|
||||||
|
} 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
|
||||||
|
* @access Private (Admin)
|
||||||
|
*/
|
||||||
|
router.post('/admin/restore', protect, upload.single('backupFile'), async (req, res) => {
|
||||||
|
if (req.user.role !== 'admin' && req.user.role !== 'Chủ sở hữu') {
|
||||||
|
if (req.file) fs.unlinkSync(req.file.path);
|
||||||
|
return res.status(403).json({ message: 'Forbidden' });
|
||||||
|
}
|
||||||
|
if (!req.file) return res.status(400).json({ message: 'Vui lòng upload file backup.zip' });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const zip = new AdmZip(req.file.path);
|
||||||
|
const dbEntry = zip.getEntry("database.json");
|
||||||
|
if (!dbEntry) throw new Error("File backup không hợp lệ (thiếu database.json)");
|
||||||
|
|
||||||
|
const dbData = JSON.parse(dbEntry.getData().toString('utf8'));
|
||||||
|
|
||||||
|
// Khôi phục Database (Xóa cũ - Ghi mới)
|
||||||
|
await Promise.all([
|
||||||
|
User.deleteMany({}), Asset.deleteMany({}), Scene.deleteMany({}),
|
||||||
|
Hotspot.deleteMany({}), Setting.deleteMany({})
|
||||||
|
]);
|
||||||
|
await Promise.all([
|
||||||
|
User.insertMany(dbData.users), Asset.insertMany(dbData.assets),
|
||||||
|
Scene.insertMany(dbData.scenes), Hotspot.insertMany(dbData.hotspots),
|
||||||
|
Setting.insertMany(dbData.settings)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Khôi phục Files
|
||||||
|
zip.extractEntryTo("uploads/", uploadDir, false, true);
|
||||||
|
fs.unlinkSync(req.file.path);
|
||||||
|
res.json({ message: 'Khôi phục dữ liệu thành công' });
|
||||||
|
} catch (error) {
|
||||||
|
if (req.file && fs.existsSync(req.file.path)) fs.unlinkSync(req.file.path);
|
||||||
|
res.status(500).json({ message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrapper for Multer middleware to catch "Request aborted" and other upload errors gracefully.
|
* Wrapper for Multer middleware to catch "Request aborted" and other upload errors gracefully.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const AdmZip = require('adm-zip');
|
||||||
|
const connectDB = require('../config/db');
|
||||||
|
const User = require('../models/User');
|
||||||
|
const Asset = require('../models/Asset');
|
||||||
|
const Scene = require('../models/Scene');
|
||||||
|
const Hotspot = require('../models/Hotspot');
|
||||||
|
const Setting = require('../models/Setting');
|
||||||
|
|
||||||
|
const backup = async () => {
|
||||||
|
await connectDB();
|
||||||
|
const zip = new AdmZip();
|
||||||
|
const uploadDir = path.join(__dirname, '../uploads');
|
||||||
|
const backupPath = path.join(__dirname, `../backups/backup_${Date.now()}.zip`);
|
||||||
|
|
||||||
|
if (!fs.existsSync(path.join(__dirname, '../backups'))) fs.mkdirSync(path.join(__dirname, '../backups'));
|
||||||
|
|
||||||
|
console.log('Exporting Database...');
|
||||||
|
const dbData = {
|
||||||
|
users: await User.find().lean(),
|
||||||
|
assets: await Asset.find().lean(),
|
||||||
|
scenes: await Scene.find().lean(),
|
||||||
|
hotspots: await Hotspot.find().lean(),
|
||||||
|
settings: await Setting.find().lean()
|
||||||
|
};
|
||||||
|
zip.addFile("database.json", Buffer.from(JSON.stringify(dbData, null, 2), "utf8"));
|
||||||
|
|
||||||
|
if (fs.existsSync(uploadDir)) {
|
||||||
|
console.log('Adding Uploads...');
|
||||||
|
zip.addLocalFolder(uploadDir, "uploads");
|
||||||
|
}
|
||||||
|
|
||||||
|
zip.writeZip(backupPath);
|
||||||
|
console.log(`Backup completed: ${backupPath}`);
|
||||||
|
mongoose.connection.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
backup();
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const AdmZip = require('adm-zip');
|
||||||
|
const connectDB = require('../config/db');
|
||||||
|
const User = require('../models/User');
|
||||||
|
const Asset = require('../models/Asset');
|
||||||
|
const Scene = require('../models/Scene');
|
||||||
|
const Hotspot = require('../models/Hotspot');
|
||||||
|
const Setting = require('../models/Setting');
|
||||||
|
|
||||||
|
const restore = async () => {
|
||||||
|
const zipPath = process.argv[2];
|
||||||
|
if (!zipPath) return console.error('Please provide zip file path: node restoreData.js <path>');
|
||||||
|
|
||||||
|
await connectDB();
|
||||||
|
const zip = new AdmZip(zipPath);
|
||||||
|
const dbEntry = zip.getEntry("database.json");
|
||||||
|
const uploadDir = path.join(__dirname, '../uploads');
|
||||||
|
|
||||||
|
console.log('Restoring Database...');
|
||||||
|
const dbData = JSON.parse(dbEntry.getData().toString('utf8'));
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
User.deleteMany({}), Asset.deleteMany({}), Scene.deleteMany({}),
|
||||||
|
Hotspot.deleteMany({}), Setting.deleteMany({})
|
||||||
|
]);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
User.insertMany(dbData.users), Asset.insertMany(dbData.assets),
|
||||||
|
Scene.insertMany(dbData.scenes), Hotspot.insertMany(dbData.hotspots),
|
||||||
|
Setting.insertMany(dbData.settings)
|
||||||
|
]);
|
||||||
|
|
||||||
|
console.log('Restoring Files...');
|
||||||
|
zip.extractEntryTo("uploads/", uploadDir, false, true);
|
||||||
|
|
||||||
|
console.log('Restore completed successfully!');
|
||||||
|
mongoose.connection.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
restore();
|
||||||
@@ -142,6 +142,23 @@
|
|||||||
</div>
|
</div>
|
||||||
<button type="submit" class="submit-btn">Lưu cấu hình</button>
|
<button type="submit" class="submit-btn">Lưu cấu hình</button>
|
||||||
</form>
|
</form>
|
||||||
|
<div class="form-group" style="border-top: 1px solid rgba(255,255,255,0.1); margin-top: 30px; padding-top: 20px;">
|
||||||
|
<label>Dữ liệu & Bảo trì</label>
|
||||||
|
<div class="backup-restore-actions" style="display: flex; flex-direction: column; gap: 10px; margin-top: 15px;">
|
||||||
|
<button type="button" class="edit-btn-large" onclick="handleBackup()" style="background: #17a2b8; width: 100%;">
|
||||||
|
<span class="icon">📥</span> Tạo bản sao lưu (Backup)
|
||||||
|
</button>
|
||||||
|
<div class="restore-container">
|
||||||
|
<input type="file" id="restore-file-input" accept=".zip" style="display:none" onchange="handleRestore(this)">
|
||||||
|
<button type="button" class="edit-btn-large" onclick="document.getElementById('restore-file-input').click()" style="background: #fd7e14; width: 100%;">
|
||||||
|
<span class="icon">📤</span> Khôi phục dữ liệu (Restore)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p style="font-size: 11px; color: #888; margin-top: 10px;">
|
||||||
|
* Backup bao gồm toàn bộ database và các tệp tin ảnh 360 trong thư mục uploads.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2026,6 +2026,58 @@ async function submitEditScene(e) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Xử lý tải xuống bản sao lưu
|
||||||
|
*/
|
||||||
|
async function handleBackup() {
|
||||||
|
const token = localStorage.getItem('jwt');
|
||||||
|
try {
|
||||||
|
showNotification("Đang chuẩn bị bản sao lưu...", "success");
|
||||||
|
const response = await fetch(`${API_BASE_URL}/admin/backup`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error("Lỗi khi tạo backup");
|
||||||
|
const blob = await response.blob();
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `backup_3dtour_${new Date().toISOString().slice(0,10)}.zip`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
showNotification("Đã tải xuống bản sao lưu!", "success");
|
||||||
|
} catch (e) { showNotification(e.message, "error"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Xử lý khôi phục dữ liệu từ file zip
|
||||||
|
*/
|
||||||
|
async function handleRestore(input) {
|
||||||
|
if (!input.files || !input.files[0]) return;
|
||||||
|
if (!confirm("CẢNH BÁO: Khôi phục dữ liệu sẽ xóa sạch dữ liệu hiện tại và thay thế bằng dữ liệu từ bản sao lưu. Bạn có chắc chắn?")) {
|
||||||
|
input.value = ''; return;
|
||||||
|
}
|
||||||
|
const token = localStorage.getItem('jwt');
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('backupFile', input.files[0]);
|
||||||
|
try {
|
||||||
|
showNotification("Đang khôi phục... Vui lòng không đóng trình duyệt.", "success");
|
||||||
|
const response = await fetch(`${API_BASE_URL}/admin/restore`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` },
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
if (!response.ok) throw new Error(data.message);
|
||||||
|
showNotification("Khôi phục thành công! Hệ thống sẽ tải lại.", "success");
|
||||||
|
setTimeout(() => location.reload(), 2000);
|
||||||
|
} catch (e) {
|
||||||
|
showNotification(e.message, "error");
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens a specific tab within the dashboard.
|
* Opens a specific tab within the dashboard.
|
||||||
* @param {string} tabName - The ID of the tab pane to open (e.g., 'profile', 'my-scenes').
|
* @param {string} tabName - The ID of the tab pane to open (e.g., 'profile', 'my-scenes').
|
||||||
|
|||||||
Reference in New Issue
Block a user