From d3ef4f6f4551e96c95aaf36316a6c8ed2a0aa391 Mon Sep 17 00:00:00 2001 From: anibilag Date: Mon, 14 Apr 2025 22:05:01 +0300 Subject: [PATCH] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=BF=D1=80=D0=BE=D0=B1=D0=BB=D0=B5=D0=BC?= =?UTF-8?q?=D0=B0=20=D0=BF=D0=B0=D0=B3=D0=B8=D0=BD=D0=B0=D1=86=D0=B8=D0=B8?= =?UTF-8?q?=20(=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20?= =?UTF-8?q?=D1=81=D0=BE=D1=80=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=BF=D0=BE=20id)=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=BB=D1=83=D1=87=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B4?= =?UTF-8?q?=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D0=BF=D0=BE=20=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=D1=8F=D0=BC=20=D0=B4=D0=BB=D1=8F=20sitemap?= =?UTF-8?q?=20=D0=B4=D0=BB=D1=8F=20SEO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/routes/articles/controllers/crud.ts | 80 ++++++++++++++++++++-- src/routes/articles/controllers/list.ts | 5 +- src/routes/articles/controllers/sitemap.ts | 26 +++++++ src/routes/articles/index.ts | 2 + src/services/s3Service.ts | 24 +++---- src/types/index.ts | 1 + 6 files changed, 118 insertions(+), 20 deletions(-) create mode 100644 src/routes/articles/controllers/sitemap.ts diff --git a/src/routes/articles/controllers/crud.ts b/src/routes/articles/controllers/crud.ts index 2bf7f0e..206877e 100644 --- a/src/routes/articles/controllers/crud.ts +++ b/src/routes/articles/controllers/crud.ts @@ -7,7 +7,7 @@ import { AuthRequest } from '../../../middleware/auth'; import { checkPermission } from '../../../utils/permissions'; import { logger } from '../../../config/logger'; import { Article } from "../../../types"; -import { uploadBufferToS3 } from "../../../services/s3Service"; +import { bucketName, uploadBufferToS3 } from "../../../services/s3Service"; const DEFAULT_COVER_IMAGE = '/images/cover-placeholder.webp'; @@ -286,12 +286,12 @@ export async function importArticles(req: AuthRequest, res: Response) : Promise< if (!req.user) { res.status(401).json({ error: 'Пользователь не вошел в систему' }); - return + return; } if (!Array.isArray(articles) || articles.length === 0) { res.status(400).json({ message: 'Ожидается непустой массив статей в теле запроса' }); - return + return; } let importedCount = 0; @@ -302,6 +302,7 @@ export async function importArticles(req: AuthRequest, res: Response) : Promise< // Шаг 1: Создание статьи const newArticle = await prisma.article.create({ data: { + importId: article.importId, title: article.title, excerpt: article.excerpt, content: article.content, @@ -336,23 +337,89 @@ export async function importArticles(req: AuthRequest, res: Response) : Promise< (contentType.includes('image/') ? `.${contentType.split('/')[1]}` : ''); // Генерация имени файла - const filename = `cover-${Date.now()}${fileExtension}`; + const fileName = `cover-${Date.now()}${fileExtension}`; + const webpFileName = path.basename(fileName, path.extname(fileName)) + '.webp'; // Загрузка буфера в S3 const uploadedUrl = await uploadBufferToS3( imageBuffer, folder, - filename, + webpFileName, contentType, - 'original', + 'medium', 80 ); + // Добавить обложку к статье + await prisma.article.update({ + where: { id: newArticle.id }, + data: { + coverImage: uploadedUrl + }, + }); } catch (imageError) { console.error(`Ошибка загрузки coverImage для статьи ${article.id}:`, imageError); } } + // Шаг 3: Обработка массива images + let updatedContent = newArticle.content; + if (article.images && Array.isArray(article.images) && article.images.length > 0) { + const folder = `articles/${newArticle.id}/images`; + const uploadedImageUrls: string[] = []; + + // Загружаем каждое изображение в S3 + 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'; + + const uploadedUrl = await uploadBufferToS3( + imageBuffer, + folder, + webpFileName, + contentType, + 'medium', + 80 + ); + uploadedImageUrls.push(uploadedUrl); + } catch (imageError) { + console.error(`Ошибка загрузки изображения ${imageUrl} для статьи ${newArticle.id}:`, imageError); + uploadedImageUrls.push(''); // Добавляем пустую строку, чтобы сохранить порядок + } + } + + // Заменяем плейсхолдеры {{image1}}, {{image2}} и т.д. на uploadedUrl + updatedContent = newArticle.content; + const hasPlaceholders = uploadedImageUrls.some((_, index) => updatedContent.includes(`{{image${index + 1}}}`)); + if (!hasPlaceholders) { + console.warn(`В content статьи ${newArticle.id} отсутствуют плейсхолдеры для изображений`); + } + + uploadedImageUrls.forEach((uploadedUrl, index) => { + if (uploadedUrl) { + const placeholder = `{{image${index + 1}}}`; + const imgTag = ``; + const regex = new RegExp(placeholder, 'g'); + updatedContent = updatedContent.replace(regex, imgTag); + } + }); + + // Обновляем поле content в базе данных + await prisma.article.update({ + where: {id: newArticle.id}, + data: { + content: updatedContent, + }, + }); + } + importedCount++; } catch (articleError) { console.error(`Ошибка импорта статьи ${article.id}:`, articleError); @@ -369,4 +436,3 @@ export async function importArticles(req: AuthRequest, res: Response) : Promise< await prisma.$disconnect(); } } - diff --git a/src/routes/articles/controllers/list.ts b/src/routes/articles/controllers/list.ts index 8b7ffac..795d7d4 100644 --- a/src/routes/articles/controllers/list.ts +++ b/src/routes/articles/controllers/list.ts @@ -45,7 +45,10 @@ export async function listArticles(req: Request, res: Response) { }, skip, take: perPage, - orderBy: { publishedAt: 'desc' }, + orderBy: [ + { publishedAt: 'desc' }, + { id: 'desc' }, // вторичный критерий сортировки + ] }), prisma.article.count({ where }), ]); diff --git a/src/routes/articles/controllers/sitemap.ts b/src/routes/articles/controllers/sitemap.ts new file mode 100644 index 0000000..35f3a9b --- /dev/null +++ b/src/routes/articles/controllers/sitemap.ts @@ -0,0 +1,26 @@ +import { Request, Response } from 'express'; +import { prisma } from '../../../lib/prisma'; + + +export async function sitemapArticles(req: Request, res: Response) { + try { + const articles = await prisma.article.findMany({ + select: { + id: true, + publishedAt: true, + }, + where: { + isActive: true, + }, + take: 10000, // запасной лимит + }); + + // Формируем ответ + res.json({ + articles, + }); + } catch (error) { + console.error('Ошибка получения статей для карты сайта:', error); + res.status(500).json({ error: 'Серверная ошибка' }); + } +} \ No newline at end of file diff --git a/src/routes/articles/index.ts b/src/routes/articles/index.ts index 399e70a..b36dba2 100644 --- a/src/routes/articles/index.ts +++ b/src/routes/articles/index.ts @@ -1,6 +1,7 @@ import express from 'express'; import { auth } from '../../middleware/auth'; import { searchArticles } from './controllers/search'; +import { sitemapArticles } from './controllers/sitemap'; import { listArticles } from './controllers/list'; import { getArticle, @@ -16,6 +17,7 @@ const router = express.Router(); // Поиск и список routes router.get('/search', searchArticles); +router.get('/sitemap', sitemapArticles); router.get('/', listArticles); // Импорт route diff --git a/src/services/s3Service.ts b/src/services/s3Service.ts index b6b7af5..afce76b 100644 --- a/src/services/s3Service.ts +++ b/src/services/s3Service.ts @@ -1,4 +1,4 @@ -import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; +import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3'; import { v4 as uuidv4 } from 'uuid'; import multer from 'multer'; import sharp from 'sharp'; @@ -103,12 +103,12 @@ export const uploadToS3 = async ( /** * Функция для загрузки буфера в S3 - * @param buffer - Буфер для загрузки - * @param folder - Путь к папке в S3 - * @param filename - Опциональное имя файла (если пусто, то будет сгенерирован UUID как имя) - * @param mimetype - MIME тип контента буфера + * @param buffer - Буфер для загрузки + * @param folder - Путь к папке в S3 + * @param filename - Опциональное имя файла (если пусто, то будет сгенерирован UUID как имя) + * @param mimetype - MIME тип контента буфера * @param resolutionId - Опциональный идентификатор разрешения изображения - * @param quality - Отцтональное значение качества при оптимизации + * @param quality - Опциональное значение качества при оптимизации * @returns URL загрущенного файла */ export const uploadBufferToS3 = async ( @@ -149,18 +149,18 @@ export const uploadBufferToS3 = async ( }) ); - // Возвращаем URL файла - const fileUrl = `https://${bucketName}.s3.regru.cloud/${finalFilename}`; - logger.info(`Buffer successfully uploaded to S3: ${key}`); + // Возвращаем URL файла с заменой расширения + const fileUrl = `https://${bucketName}.s3.regru.cloud/${key}`; + logger.info(`Буффер успешно загружен в S3: ${key}`); return fileUrl; } catch (error) { - logger.error('Error uploading buffer to S3'); + logger.error('Ошибка заргузки буффера в S3'); console.error(error); - throw new Error('Error uploading buffer to S3'); + throw new Error('Ошибка заргузки буффера в S3'); } }; -// Функция получения пасширения файла из MIME типа +// Функция получения расширения файла из MIME типа function getExtensionFromMimeType(mimetype: string): string { const mimeToExt: Record = { 'image/jpeg': '.jpg', diff --git a/src/types/index.ts b/src/types/index.ts index 1176faa..f178e71 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -8,6 +8,7 @@ export interface Article { cityId: number; author: Author; coverImage: string; + images?: string[]; gallery?: GalleryImage[]; publishedAt: string; readTime: number;