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;