Версия 1.0.8 - Если при импорте статьи изображение уже не существует (добавлена проверка), то подставлять URL для картинки Изображение утеряно.

This commit is contained in:
anibilag 2025-12-06 23:42:48 +03:00
parent 5bae3e4f0e
commit 8c06cc9f04
3 changed files with 132 additions and 45 deletions

2
.gitignore vendored
View File

@ -2,7 +2,7 @@ node_modules
dist
logs
*.log
.idea

View File

@ -1,6 +1,6 @@
{
"name": "russcult_server",
"version": "1.0.8",
"version": "1.0.9",
"main": "index.js",
"scripts": {
"build": "tsc",

View File

@ -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<void> {
try {
@ -368,6 +369,69 @@ export async function deleteArticle(req: AuthRequest, res: Response) : Promise<v
}
}
export async function imageExists(url: string): Promise<boolean> {
// --- Попытка 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<void> {
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(''); // Добавляем пустую строку, чтобы сохранить порядок