Fix: backup/restore now includes group_photos and entry_photos tables; increase AI request timeout to 120s (configurable via AI_REQUEST_TIMEOUT_MS)
This commit is contained in:
@@ -12,3 +12,5 @@ AI_MODEL=qwen2.5-1.5b-instruct-q4_k_m.gguf
|
||||
AI_MODEL_REPO=Qwen/Qwen2.5-1.5B-Instruct-GGUF
|
||||
# Промпт по умолчанию для автоисправления текстов (переопределяет встроенный, если в настройках не сохранён свой)
|
||||
AI_PROMPT=Ты — редактор текстов. Исправь ТОЛЬКО грамматические, орфографические и пунктуационные ошибки в тексте. Приведи к правильному регистру буквы. НЕ меняй слова, структуру предложений, стиль или смысл текста. Верни ТОЛЬКО исправленный текст без пояснений.
|
||||
# Таймаут запроса к AI (мс), по умолчанию 120000 (2 минуты)
|
||||
AI_REQUEST_TIMEOUT_MS=120000
|
||||
@@ -32,6 +32,7 @@ services:
|
||||
BACKUP_UPLOAD_LIMIT_MB: ${BACKUP_UPLOAD_LIMIT_MB:-500}
|
||||
AI_MODEL: ${AI_MODEL:-qwen2.5-1.5b-instruct-q4_k_m.gguf}
|
||||
AI_PROMPT: ${AI_PROMPT:-}
|
||||
AI_REQUEST_TIMEOUT_MS: ${AI_REQUEST_TIMEOUT_MS:-120000}
|
||||
NODE_ENV: production
|
||||
TZ: Europe/Moscow
|
||||
depends_on:
|
||||
|
||||
@@ -810,6 +810,17 @@ function optTime(v) {
|
||||
return v;
|
||||
}
|
||||
|
||||
function optDate(v) {
|
||||
if (v === null || v === undefined) return null;
|
||||
if (typeof v !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(v)) throw new Error('Invalid date');
|
||||
return v;
|
||||
}
|
||||
|
||||
function optBool(v) {
|
||||
if (v === null || v === undefined) return null;
|
||||
return !!v;
|
||||
}
|
||||
|
||||
function reqToken(v) {
|
||||
if (typeof v !== 'string' || !/^[0-9a-f]{16,64}$/.test(v)) throw new Error('Invalid token');
|
||||
return v;
|
||||
@@ -898,17 +909,52 @@ function normalizeRestoreData(data) {
|
||||
user_id: reqInt(x.user_id),
|
||||
branch_id: reqInt(x.branch_id),
|
||||
}));
|
||||
const entry_photos = (data.entry_photos || []).map(x => ({
|
||||
id: reqInt(x.id),
|
||||
entry_id: reqInt(x.entry_id),
|
||||
photo_path: reqUploadPath(x.photo_path, 255),
|
||||
caption: optStr(x.caption, 10000),
|
||||
sort_order: optInt(x.sort_order, -2147483648, 2147483647),
|
||||
created_at: optTs(x.created_at),
|
||||
}));
|
||||
const group_photos = (data.group_photos || []).map(x => ({
|
||||
id: reqInt(x.id),
|
||||
group_id: reqInt(x.group_id),
|
||||
photo_path: reqUploadPath(x.photo_path, 255),
|
||||
caption: optStr(x.caption, 10000),
|
||||
taken_at: optDate(x.taken_at),
|
||||
sort_order: optInt(x.sort_order, -2147483648, 2147483647),
|
||||
created_at: optTs(x.created_at),
|
||||
}));
|
||||
const share_links = (data.share_links || []).map(x => ({
|
||||
id: reqInt(x.id),
|
||||
token: optStr(x.token, 40),
|
||||
name: reqStr(x.name, 200),
|
||||
group_id: optInt(x.group_id, 0, 2147483647),
|
||||
student_name: optStr(x.student_name, 150),
|
||||
date_from: optDate(x.date_from),
|
||||
date_to: optDate(x.date_to),
|
||||
show_student_names: optBool(x.show_student_names),
|
||||
expires_at: optTs(x.expires_at),
|
||||
access_password_hash: optStr(x.access_password_hash, 255),
|
||||
message: optStr(x.message, 2000),
|
||||
link_url: optStr(x.link_url, 500),
|
||||
show_student_message: optBool(x.show_student_message),
|
||||
show_entry_date: optBool(x.show_entry_date),
|
||||
show_group_photos: optBool(x.show_group_photos),
|
||||
created_at: optTs(x.created_at),
|
||||
}));
|
||||
const settings = {};
|
||||
for (const [k, v] of Object.entries(data.settings || {})) {
|
||||
settings[reqStr(k, 100)] = reqStr(String(v), 10000);
|
||||
}
|
||||
return { groups, students, entries, project_files, settings, branches, users, user_branches };
|
||||
return { groups, students, entries, project_files, settings, branches, users, user_branches, entry_photos, group_photos, share_links };
|
||||
}
|
||||
|
||||
app.get('/api/backup', requireAdmin, async (req, res) => {
|
||||
const staging = fs.mkdtempSync(path.join(os.tmpdir(), 'wido-bk-'));
|
||||
try {
|
||||
const [g, s, e, st, pf, br, us, ub] = await Promise.all([
|
||||
const [g, s, e, st, pf, br, us, ub, gp, ep] = await Promise.all([
|
||||
pool.query('SELECT * FROM groups ORDER BY id'),
|
||||
pool.query('SELECT * FROM students ORDER BY id'),
|
||||
pool.query('SELECT * FROM entries ORDER BY id'),
|
||||
@@ -917,10 +963,12 @@ app.get('/api/backup', requireAdmin, async (req, res) => {
|
||||
pool.query('SELECT * FROM branches ORDER BY id'),
|
||||
pool.query('SELECT * FROM users ORDER BY id'),
|
||||
pool.query('SELECT * FROM user_branches ORDER BY user_id, branch_id'),
|
||||
pool.query('SELECT * FROM group_photos ORDER BY id'),
|
||||
pool.query('SELECT * FROM entry_photos ORDER BY id'),
|
||||
]);
|
||||
const settings = {};
|
||||
st.rows.forEach(r => { settings[r.key] = r.value; });
|
||||
const payload = { version: 1, created_at: new Date().toISOString(), groups: g.rows, students: s.rows, entries: e.rows, settings, project_files: pf.rows, branches: br.rows, users: us.rows, user_branches: ub.rows };
|
||||
const payload = { version: 1, created_at: new Date().toISOString(), groups: g.rows, students: s.rows, entries: e.rows, settings, project_files: pf.rows, branches: br.rows, users: us.rows, user_branches: ub.rows, group_photos: gp.rows, entry_photos: ep.rows };
|
||||
fs.writeFileSync(path.join(staging, 'data.json'), JSON.stringify(payload));
|
||||
fs.mkdirSync(path.join(staging, 'uploads'), { recursive: true });
|
||||
const dir = path.join(__dirname, 'uploads');
|
||||
@@ -1050,6 +1098,18 @@ app.post('/api/restore', requireAdmin, uploadBackup.single('backup'), async (req
|
||||
[x.id, x.entry_id, x.token, x.path, x.name, x.created_at]
|
||||
);
|
||||
}
|
||||
for (const x of ndata.group_photos) {
|
||||
await client.query(
|
||||
'INSERT INTO group_photos (id, group_id, photo_path, caption, taken_at, sort_order, created_at) VALUES ($1,$2,$3,$4,$5,$6,$7)',
|
||||
[x.id, x.group_id, x.photo_path, x.caption, x.taken_at, x.sort_order, x.created_at]
|
||||
);
|
||||
}
|
||||
for (const x of ndata.entry_photos) {
|
||||
await client.query(
|
||||
'INSERT INTO entry_photos (id, entry_id, photo_path, caption, sort_order, created_at) VALUES ($1,$2,$3,$4,$5,$6)',
|
||||
[x.id, x.entry_id, x.photo_path, x.caption, x.sort_order, x.created_at]
|
||||
);
|
||||
}
|
||||
for (const x of ndata.users) {
|
||||
await client.query(
|
||||
'INSERT INTO users (id, username, password_hash, name, role, is_active, created_at) VALUES ($1,$2,$3,$4,$5,$6,$7)',
|
||||
@@ -1068,7 +1128,7 @@ app.post('/api/restore', requireAdmin, uploadBackup.single('backup'), async (req
|
||||
[k, String(v ?? '')]
|
||||
);
|
||||
}
|
||||
for (const tbl of ['groups', 'students', 'entries', 'project_files', 'branches', 'users']) {
|
||||
for (const tbl of ['groups', 'students', 'entries', 'project_files', 'branches', 'users', 'group_photos', 'entry_photos']) {
|
||||
const r = await client.query('SELECT COALESCE(MAX(id), 1) AS m FROM ' + tbl);
|
||||
await client.query('SELECT setval(pg_get_serial_sequence($1, $2), $3)', [tbl, 'id', r.rows[0].m]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user