feat(groups): расширенный экспорт фото — галерея и оригиналы
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user