Версия 1.0.8 - Если при импорте статьи изображение уже не существует (добавлена проверка), то подставлять URL для картинки Изображение утеряно.
This commit is contained in:
parent
5bae3e4f0e
commit
8c06cc9f04
2
.gitignore
vendored
2
.gitignore
vendored
@ -2,7 +2,7 @@ node_modules
|
|||||||
dist
|
dist
|
||||||
|
|
||||||
logs
|
logs
|
||||||
*.log
|
|
||||||
|
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "russcult_server",
|
"name": "russcult_server",
|
||||||
"version": "1.0.8",
|
"version": "1.0.9",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import { AuthorRole } from "@prisma/client";
|
|||||||
|
|
||||||
|
|
||||||
const DEFAULT_COVER_IMAGE = '/images/cover-placeholder.webp';
|
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> {
|
export async function getArticle(req: Request, res: Response) : Promise<void> {
|
||||||
try {
|
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> {
|
export async function importArticles(req: AuthRequest, res: Response) : Promise<void> {
|
||||||
const articles: Article[] = req.body;
|
const articles: Article[] = req.body;
|
||||||
|
|
||||||
@ -448,6 +512,10 @@ export async function importArticles(req: AuthRequest, res: Response) : Promise<
|
|||||||
const imageUrl = article.coverImage;
|
const imageUrl = article.coverImage;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Проверяем, существует ли картинка по URL
|
||||||
|
const exists = await imageExists(imageUrl);
|
||||||
|
|
||||||
|
if (exists) {
|
||||||
// Извлечение изображение в виде буфера массива
|
// Извлечение изображение в виде буфера массива
|
||||||
const imageResponse = await axios.get(imageUrl, { responseType: 'arraybuffer' });
|
const imageResponse = await axios.get(imageUrl, { responseType: 'arraybuffer' });
|
||||||
|
|
||||||
@ -480,6 +548,17 @@ export async function importArticles(req: AuthRequest, res: Response) : Promise<
|
|||||||
coverImage: uploadedUrl
|
coverImage: uploadedUrl
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.warn(`Изображение coverImage отсутствует, устанавливаю fallback: ${imageUrl}`);
|
||||||
|
|
||||||
|
await prisma.article.update({
|
||||||
|
where: { id: newArticle.id },
|
||||||
|
data: {
|
||||||
|
coverImage: FALLBACK_COVER_URL
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (imageError) {
|
} catch (imageError) {
|
||||||
console.error(`Ошибка загрузки coverImage для статьи ${article.id}:`, imageError);
|
console.error(`Ошибка загрузки coverImage для статьи ${article.id}:`, imageError);
|
||||||
}
|
}
|
||||||
@ -496,6 +575,10 @@ export async function importArticles(req: AuthRequest, res: Response) : Promise<
|
|||||||
for (let i = 0; i < article.images.length; i++) {
|
for (let i = 0; i < article.images.length; i++) {
|
||||||
const imageUrl = article.images[i];
|
const imageUrl = article.images[i];
|
||||||
try {
|
try {
|
||||||
|
// Проверяем, существует ли картинка по URL
|
||||||
|
const exists = await imageExists(imageUrl);
|
||||||
|
|
||||||
|
if (exists) {
|
||||||
const imageResponse = await axios.get(imageUrl, {responseType: 'arraybuffer'});
|
const imageResponse = await axios.get(imageUrl, {responseType: 'arraybuffer'});
|
||||||
const imageBuffer = Buffer.from(imageResponse.data, 'binary');
|
const imageBuffer = Buffer.from(imageResponse.data, 'binary');
|
||||||
const contentType = imageResponse.headers['content-type'];
|
const contentType = imageResponse.headers['content-type'];
|
||||||
@ -513,6 +596,10 @@ export async function importArticles(req: AuthRequest, res: Response) : Promise<
|
|||||||
80
|
80
|
||||||
);
|
);
|
||||||
uploadedImageUrls.push(uploadedUrl);
|
uploadedImageUrls.push(uploadedUrl);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
uploadedImageUrls.push(FALLBACK_COVER_URL);
|
||||||
|
}
|
||||||
} catch (imageError) {
|
} catch (imageError) {
|
||||||
console.error(`Ошибка загрузки изображения ${imageUrl} для статьи ${newArticle.id}:`, imageError);
|
console.error(`Ошибка загрузки изображения ${imageUrl} для статьи ${newArticle.id}:`, imageError);
|
||||||
uploadedImageUrls.push(''); // Добавляем пустую строку, чтобы сохранить порядок
|
uploadedImageUrls.push(''); // Добавляем пустую строку, чтобы сохранить порядок
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user