/** * Универсальные хелперы для работы с S3 * Устраняют дублирование кода в routes */ const { s3, BUCKET_NAME } = require('../services/s3Service'); const { GetObjectCommand, HeadObjectCommand } = require('@aws-sdk/client-s3'); const { streamToString } = require('../services/s3Service'); const { sendError, checkIfNoneMatch } = require('../middleware/errorHandler'); /** * Универсальный GET для JSON объектов из S3 с заголовками * @param {string} s3Key - Ключ S3 * @param {object} req - Express request * @param {object} res - Express response * @param {object} options - Опции * @param {function} options.transform - Функция трансформации данных * @param {*} options.defaultValue - Значение по умолчанию при отсутствии файла */ async function getS3JsonWithHeaders(s3Key, req, res, options = {}) { const { transform = (data) => data, defaultValue = [] } = options; try { // Получаем HEAD для установки заголовков const head = await s3.send(new HeadObjectCommand({ Bucket: BUCKET_NAME, Key: s3Key })).catch(() => null); const etag = head?.ETag || null; // Устанавливаем заголовки if (etag) res.set('ETag', String(etag)); if (head?.LastModified) res.set('Last-Modified', new Date(head.LastModified).toUTCString()); if (typeof head?.ContentLength === 'number') res.set('Content-Length-Source', String(head.ContentLength)); // Проверяем If-None-Match if (checkIfNoneMatch(req, res, etag)) return; // Получаем объект const data = await s3.send(new GetObjectCommand({ Bucket: BUCKET_NAME, Key: s3Key })); const fileContent = await streamToString(data.Body); let parsed; try { parsed = JSON.parse(fileContent); if (!Array.isArray(parsed) && Array.isArray(defaultValue)) { parsed = defaultValue; } } catch (parseError) { console.error(`Error parsing ${s3Key}:`, parseError); parsed = defaultValue; } // Применяем трансформацию const result = transform(parsed); res.json(result); } catch (error) { if (error?.code === 'NoSuchKey' || error?.$metadata?.httpStatusCode === 404) { res.json(defaultValue); } else { console.error(error); return sendError(res, 500, 'Error reading from S3', 'E_S3'); } } } /** * Универсальный GET для текстовых объектов из S3 с заголовками * @param {string} s3Key - Ключ S3 * @param {object} req - Express request * @param {object} res - Express response * @param {object} options - Опции * @param {function} options.transform - Функция трансформации текста * @param {string} options.defaultValue - Значение по умолчанию при отсутствии файла */ async function getS3TextWithHeaders(s3Key, req, res, options = {}) { const { transform = (text) => ({ config: text }), defaultValue = '' } = options; try { const head = await s3.send(new HeadObjectCommand({ Bucket: BUCKET_NAME, Key: s3Key })).catch(() => null); const etag = head?.ETag || null; if (etag) res.set('ETag', String(etag)); if (head?.LastModified) res.set('Last-Modified', new Date(head.LastModified).toUTCString()); if (typeof head?.ContentLength === 'number') res.set('Content-Length-Source', String(head.ContentLength)); if (checkIfNoneMatch(req, res, etag)) return; const data = await s3.send(new GetObjectCommand({ Bucket: BUCKET_NAME, Key: s3Key })); const text = await streamToString(data.Body); const result = transform(text); res.json(result); } catch (error) { if (error?.code === 'NoSuchKey' || error?.$metadata?.httpStatusCode === 404) { res.json(transform(defaultValue)); } else { console.error(error); return sendError(res, 500, 'Error reading from S3', 'E_S3'); } } } module.exports = { getS3JsonWithHeaders, getS3TextWithHeaders, };