feat(groups): расширенный экспорт фото — галерея и оригиналы

This commit is contained in:
dev
2026-10-02 13:07:47 +03:00
parent 55c4b281c3
commit 449b86955e
3 changed files with 64 additions and 19 deletions
+29 -6
View File
@@ -2061,6 +2061,12 @@ function optPhotoRefPath(v, max) {
return reqPhotoRefPath(v, max);
}
function photoRefKey(p) {
if (typeof p !== 'string') return null;
if (isSafeUploadPath(p) || ORIGINALS_PATH_RE.test(p)) return p.slice('/uploads/'.length);
return null;
}
const AI_STATUSES = new Set(['pending', 'processing', 'done', 'skipped', 'error', 'reverted']);
function optAiText(v, max) {
@@ -4142,6 +4148,8 @@ app.get('/api/groups/:id/export/files', requireAuth, async (req, res) => {
const includeFiles = req.query.include_files !== '0';
const includePhotos = req.query.include_photos !== '0';
const photosMode = req.query.photos_mode === 'all' ? 'all' : 'latest';
const includeMain = includePhotos && req.query.include_main !== '0';
const includeOriginals = includePhotos && req.query.include_originals === '1';
if (!includeFiles && !includePhotos) {
return res.status(400).json({ error: 'Выберите хотя бы одну категорию: работы или фото записи' });
}
@@ -4171,7 +4179,7 @@ app.get('/api/groups/:id/export/files', requireAuth, async (req, res) => {
conds.push(`e.created_at < ($${params.length}::date + interval '1 day')`);
}
const condStr = conds.join(' AND ');
const [studentsRes, filesRes, photosRes, entryPhotosRes] = await Promise.all([
const [studentsRes, filesRes, photosRes, entryPhotosRes, originalsRes] = await Promise.all([
pool.query('SELECT id, name FROM students WHERE group_id = $1 ORDER BY name', [gid]),
includeFiles
? pool.query(
@@ -4183,7 +4191,7 @@ app.get('/api/groups/:id/export/files', requireAuth, async (req, res) => {
params
)
: null,
includePhotos
includePhotos && includeMain
? pool.query(
`SELECT e.photo_path, e.student_name, e.created_at
FROM entries e
@@ -4202,10 +4210,20 @@ app.get('/api/groups/:id/export/files', requireAuth, async (req, res) => {
params
)
: null,
includeOriginals
? pool.query(
`SELECT e.photo_original_path, e.student_name, e.created_at
FROM entries e
WHERE ${condStr} AND e.photo_original_path IS NOT NULL
ORDER BY e.student_name, e.created_at DESC`,
params
)
: null,
]);
const fileRows = filesRes ? filesRes.rows : [];
const photoRows = photosRes ? photosRes.rows : [];
const entryPhotoRows = entryPhotosRes ? entryPhotosRes.rows : [];
const originalRows = originalsRes ? originalsRes.rows : [];
const studentNames = new Set(studentsRes.rows.map(s => s.name));
const zip = createZipWriter();
const folderOf = name => String(name || '').replace(/[\\/:*?"<>|]/g, '_').replace(/^[\s.]+/, '').slice(0, 80) || 'Без имени';
@@ -4233,8 +4251,8 @@ app.get('/api/groups/:id/export/files', requireAuth, async (req, res) => {
const seenFiles = new Map();
const seenPaths = new Set();
async function addEntryFile(row, subDir) {
if (!isSafeUploadPath(row.path)) return false;
const stored = row.path.slice('/uploads/'.length);
const stored = photoRefKey(row.path);
if (!stored) return false;
if (seenPaths.has(stored)) return false;
const data = await storage.getBuffer(stored);
if (!data) return false;
@@ -4262,6 +4280,7 @@ app.get('/api/groups/:id/export/files', requireAuth, async (req, res) => {
}
let worksAdded = 0;
let photosAdded = 0;
let originalsAdded = 0;
for (const f of fileRows) if (await addEntryFile(f, null)) worksAdded++;
for (const p of photoRows) {
if (await addEntryFile({ name: `photo_${new Date(p.created_at || Date.now()).toISOString().slice(0, 10)}.jpg`, path: p.photo_path, student_name: p.student_name, created_at: p.created_at }, 'Фото записи')) photosAdded++;
@@ -4270,12 +4289,15 @@ app.get('/api/groups/:id/export/files', requireAuth, async (req, res) => {
const storedName = String(p.photo_path).split('/').pop();
if (await addEntryFile({ name: storedName || 'photo.jpg', path: p.photo_path, student_name: p.student_name, created_at: p.created_at }, 'Фото записи')) photosAdded++;
}
for (const p of originalRows) {
if (await addEntryFile({ name: `original_${new Date(p.created_at || Date.now()).toISOString().slice(0, 10)}.jpg`, path: p.photo_original_path, student_name: p.student_name, created_at: p.created_at }, 'Фото записи/Оригиналы')) originalsAdded++;
}
const meta = {
exported_at: new Date().toISOString(),
group: { id: gid, name: groupName },
period: { date_from: dateFrom, date_to: dateTo },
options: { files: includeFiles, photos: includePhotos, photos_mode: photosMode },
totals: { students: studentNames.size, files: filesBuilt.length, works: worksAdded, photos: photosAdded },
options: { files: includeFiles, photos: includePhotos, main_photo: includeMain, photos_mode: photosMode, originals: includeOriginals },
totals: { students: studentNames.size, files: filesBuilt.length, works: worksAdded, photos: photosAdded, originals: originalsAdded },
students: [...studentNames].sort(),
files: filesBuilt,
};
@@ -4288,6 +4310,7 @@ app.get('/api/groups/:id/export/files', requireAuth, async (req, res) => {
files: filesBuilt.length,
works: worksAdded,
photos: photosAdded,
originals: originalsAdded,
photos_mode: photosMode,
date_from: dateFrom,
date_to: dateTo,