fix(backup): accept /uploads/.originals paths in photo_jobs on restore
photo_jobs.before_path хранит путь к оригиналу фото (/uploads/.originals/<файл>), но normalizeRestoreData проверял это поле через optUploadPath/isSafeUploadPath, который запрещает "/" — при наличии завершённых улучшений фото весь импорт падал с 400 «Неверный формат бэкапа: Invalid upload path». - добавлены reqPhotoRefPath/optPhotoRefPath: допустимы /uploads/<файл> и /uploads/.originals/<файл> (та же ORIGINALS_PATH_RE, что и для entries.photo_original_path); применяются к photo_jobs.before_path/after_path - POST /api/restore: понятная ошибка, если загружен архив скрипта scripts/backup.sh (db.sql.gz + _uploads) вместо веб-архива - README: форматы скриптового и веб-архива не взаимозаменяемы
This commit is contained in:
@@ -1357,6 +1357,17 @@ function optOriginalsPath(v, max) {
|
||||
return v;
|
||||
}
|
||||
|
||||
function reqPhotoRefPath(v, max) {
|
||||
if (typeof v !== 'string' || v.length > max) throw new Error('Invalid photo path');
|
||||
if (isSafeUploadPath(v) || ORIGINALS_PATH_RE.test(v)) return v;
|
||||
throw new Error('Invalid photo path');
|
||||
}
|
||||
|
||||
function optPhotoRefPath(v, max) {
|
||||
if (v === null || v === undefined) return null;
|
||||
return reqPhotoRefPath(v, max);
|
||||
}
|
||||
|
||||
const AI_STATUSES = new Set(['pending', 'processing', 'done', 'skipped', 'error', 'reverted']);
|
||||
|
||||
function optAiText(v, max) {
|
||||
@@ -1597,8 +1608,8 @@ function normalizeRestoreData(data) {
|
||||
entry_id: reqInt(x.entry_id),
|
||||
action: (x.action && PHOTO_JOB_ACTIONS.has(x.action)) ? x.action : 'ai',
|
||||
params: (x.params === null || x.params === undefined) ? null : (typeof x.params === 'object' ? JSON.stringify(x.params) : String(x.params)),
|
||||
before_path: optUploadPath(x.before_path, 255),
|
||||
after_path: optUploadPath(x.after_path, 255),
|
||||
before_path: optPhotoRefPath(x.before_path, 255),
|
||||
after_path: optPhotoRefPath(x.after_path, 255),
|
||||
status: (x.status && PHOTO_JOB_STATUSES.has(x.status)) ? x.status : 'pending',
|
||||
applied: !!x.applied,
|
||||
attempts: optInt(x.attempts, 0, 2147483647) ?? 0,
|
||||
@@ -1787,6 +1798,11 @@ app.post('/api/restore', requireAdmin, uploadBackup.single('backup'), async (req
|
||||
legacyPhotos = data.photos || [];
|
||||
} else {
|
||||
await tar.x({ file: req.file.path, cwd: staging });
|
||||
if (!fs.existsSync(path.join(staging, 'data.json')) && fs.existsSync(path.join(staging, 'db.sql.gz'))) {
|
||||
fs.rmSync(staging, { recursive: true, force: true });
|
||||
cleanupUpload(req);
|
||||
return res.status(400).json({ error: 'Это архив скрипта scripts/backup.sh (db.sql.gz + _uploads) — восстанавливайте его через scripts/restore.sh. Для веб-восстановления скачайте архив в Настройках админки.' });
|
||||
}
|
||||
data = JSON.parse(fs.readFileSync(path.join(staging, 'data.json'), 'utf8'));
|
||||
}
|
||||
} catch {
|
||||
|
||||
Reference in New Issue
Block a user