From 8c06cc9f041c252feaa8c1a72f7cd4f431806a04 Mon Sep 17 00:00:00 2001 From: anibilag Date: Sat, 6 Dec 2025 23:42:48 +0300 Subject: [PATCH] =?UTF-8?q?=D0=92=D0=B5=D1=80=D1=81=D0=B8=D1=8F=201.0.8=20?= =?UTF-8?q?-=20=D0=95=D1=81=D0=BB=D0=B8=20=D0=BF=D1=80=D0=B8=20=D0=B8?= =?UTF-8?q?=D0=BC=D0=BF=D0=BE=D1=80=D1=82=D0=B5=20=D1=81=D1=82=D0=B0=D1=82?= =?UTF-8?q?=D1=8C=D0=B8=20=D0=B8=D0=B7=D0=BE=D0=B1=D1=80=D0=B0=D0=B6=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D1=83=D0=B6=D0=B5=20=D0=BD=D0=B5=20=D1=81?= =?UTF-8?q?=D1=83=D1=89=D0=B5=D1=81=D1=82=D0=B2=D1=83=D0=B5=D1=82=20(?= =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D0=B0),=20=D1=82=D0=BE=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=B4=D1=81=D1=82=D0=B0=D0=B2=D0=BB=D1=8F=D1=82?= =?UTF-8?q?=D1=8C=20URL=20=D0=B4=D0=BB=D1=8F=20=D0=BA=D0=B0=D1=80=D1=82?= =?UTF-8?q?=D0=B8=D0=BD=D0=BA=D0=B8=20=D0=98=D0=B7=D0=BE=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D0=B6=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=83=D1=82=D0=B5=D1=80=D1=8F?= =?UTF-8?q?=D0=BD=D0=BE.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- package.json | 2 +- src/routes/articles/controllers/crud.ts | 173 ++++++++++++++++++------ 3 files changed, 132 insertions(+), 45 deletions(-) diff --git a/.gitignore b/.gitignore index bb69df6..e683871 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ node_modules dist logs -*.log + .idea diff --git a/package.json b/package.json index 37bba8e..7799f3a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "russcult_server", - "version": "1.0.8", + "version": "1.0.9", "main": "index.js", "scripts": { "build": "tsc", diff --git a/src/routes/articles/controllers/crud.ts b/src/routes/articles/controllers/crud.ts index 56bcac8..533ee3e 100644 --- a/src/routes/articles/controllers/crud.ts +++ b/src/routes/articles/controllers/crud.ts @@ -12,6 +12,7 @@ import { AuthorRole } from "@prisma/client"; const DEFAULT_COVER_IMAGE = '/images/cover-placeholder.webp'; +const FALLBACK_COVER_URL = '/images/lost-image.webp'; export async function getArticle(req: Request, res: Response) : Promise { try { @@ -368,6 +369,69 @@ export async function deleteArticle(req: AuthRequest, res: Response) : Promise { + // --- Попытка HEAD (не бросаем исключение при 4xx/5xx) --- + try { + const headRes = await axios.head(url, { + timeout: 5000, + validateStatus: () => true, // всегда возвращаем ответ, не бросаем + }); + + const status = headRes.status; + const contentType = headRes.headers['content-type'] ?? ''; + + // Если HEAD вернул успешный статус и content-type явно image -> ok + if (status >= 200 && status < 300 && contentType.startsWith('image/')) { + return true; + } + + // Если HEAD вернул 2xx, но content-type нет/не image -> попробуем GET (возможно сервер не указывает CT в HEAD) + const shouldTryGetOnHead = + (status >= 200 && status < 300) || + status === 403 || + status === 404 || + status === 405 || + status === 501 || + !status; + + if (!shouldTryGetOnHead) { + // Например 500/502/401 и т.п. — считаем, что изображение недоступно + return false; + } + } catch (headErr) { + // В случаe сетевой ошибки или таймаута — попробуем GET ниже + // (не выкидываем ошибку) + } + + // --- HEAD не дал уверенного ответа — пробуем GET (stream) --- + try { + const getRes = await axios.get(url, { + responseType: 'stream', + timeout: 7000, + validateStatus: () => true, // чтобы не бросало исключение на 4xx/5xx + }); + + const status = getRes.status; + const contentType = (getRes.headers && getRes.headers['content-type']) || ''; + + // Если статус 2xx и content-type image -> ok + const ok = status >= 200 && status < 300 && contentType.startsWith('image/'); + + // Закрываем поток — нам достаточно заголовков + try { + if (getRes.data && typeof getRes.data.destroy === 'function') { + getRes.data.destroy(); + } + } catch (_) { + // ignore + } + + return !!ok; + } catch { + return false; + } +} + export async function importArticles(req: AuthRequest, res: Response) : Promise { const articles: Article[] = req.body; @@ -448,38 +512,53 @@ export async function importArticles(req: AuthRequest, res: Response) : Promise< const imageUrl = article.coverImage; try { - // Извлечение изображение в виде буфера массива - const imageResponse = await axios.get(imageUrl, { responseType: 'arraybuffer' }); + // Проверяем, существует ли картинка по URL + const exists = await imageExists(imageUrl); - // Создание буфера из данных ответа - const imageBuffer = Buffer.from(imageResponse.data, 'binary'); + if (exists) { + // Извлечение изображение в виде буфера массива + const imageResponse = await axios.get(imageUrl, { responseType: 'arraybuffer' }); - // Получить тип контента и расширение - const contentType = imageResponse.headers['content-type']; - const fileExtension = path.extname(new URL(imageUrl).pathname) || - (contentType.includes('image/') ? `.${contentType.split('/')[1]}` : ''); + // Создание буфера из данных ответа + const imageBuffer = Buffer.from(imageResponse.data, 'binary'); - // Генерация имени файла - const fileName = `cover-${Date.now()}${fileExtension}`; - const webpFileName = path.basename(fileName, path.extname(fileName)) + '.webp'; + // Получить тип контента и расширение + const contentType = imageResponse.headers['content-type']; + const fileExtension = path.extname(new URL(imageUrl).pathname) || + (contentType.includes('image/') ? `.${contentType.split('/')[1]}` : ''); - // Загрузка буфера в S3 - const uploadedUrl = await uploadBufferToS3( - imageBuffer, - folder, - webpFileName, - contentType, - 'medium', - 80 - ); + // Генерация имени файла + const fileName = `cover-${Date.now()}${fileExtension}`; + const webpFileName = path.basename(fileName, path.extname(fileName)) + '.webp'; - // Добавить обложку к статье - await prisma.article.update({ - where: { id: newArticle.id }, - data: { - coverImage: uploadedUrl - }, - }); + // Загрузка буфера в S3 + const uploadedUrl = await uploadBufferToS3( + imageBuffer, + folder, + webpFileName, + contentType, + 'medium', + 80 + ); + + // Добавить обложку к статье + await prisma.article.update({ + where: { id: newArticle.id }, + data: { + coverImage: uploadedUrl + }, + }); + } + else { + console.warn(`Изображение coverImage отсутствует, устанавливаю fallback: ${imageUrl}`); + + await prisma.article.update({ + where: { id: newArticle.id }, + data: { + coverImage: FALLBACK_COVER_URL + }, + }); + } } catch (imageError) { console.error(`Ошибка загрузки coverImage для статьи ${article.id}:`, imageError); } @@ -496,23 +575,31 @@ export async function importArticles(req: AuthRequest, res: Response) : Promise< for (let i = 0; i < article.images.length; i++) { const imageUrl = article.images[i]; try { - const imageResponse = await axios.get(imageUrl, {responseType: 'arraybuffer'}); - const imageBuffer = Buffer.from(imageResponse.data, 'binary'); - const contentType = imageResponse.headers['content-type']; - const fileExtension = path.extname(new URL(imageUrl).pathname) || - (contentType.includes('image/') ? `.${contentType.split('/')[1]}` : ''); - const fileName = `image-${Date.now()}-${i}${fileExtension}`; - const webpFileName = path.basename(fileName, path.extname(fileName)) + '.webp'; + // Проверяем, существует ли картинка по URL + const exists = await imageExists(imageUrl); - const uploadedUrl = await uploadBufferToS3( - imageBuffer, - folder, - webpFileName, - contentType, - 'medium', - 80 - ); - uploadedImageUrls.push(uploadedUrl); + if (exists) { + const imageResponse = await axios.get(imageUrl, {responseType: 'arraybuffer'}); + const imageBuffer = Buffer.from(imageResponse.data, 'binary'); + const contentType = imageResponse.headers['content-type']; + const fileExtension = path.extname(new URL(imageUrl).pathname) || + (contentType.includes('image/') ? `.${contentType.split('/')[1]}` : ''); + const fileName = `image-${Date.now()}-${i}${fileExtension}`; + const webpFileName = path.basename(fileName, path.extname(fileName)) + '.webp'; + + const uploadedUrl = await uploadBufferToS3( + imageBuffer, + folder, + webpFileName, + contentType, + 'medium', + 80 + ); + uploadedImageUrls.push(uploadedUrl); + } + else { + uploadedImageUrls.push(FALLBACK_COVER_URL); + } } catch (imageError) { console.error(`Ошибка загрузки изображения ${imageUrl} для статьи ${newArticle.id}:`, imageError); uploadedImageUrls.push(''); // Добавляем пустую строку, чтобы сохранить порядок