Добавлена функциональность черновиков, добавлен в таблицу статей id который будет сохранять идентификатор при импорте.
This commit is contained in:
parent
3b1b702b00
commit
433aa38218
110
package-lock.json
generated
110
package-lock.json
generated
@ -12,6 +12,7 @@
|
|||||||
"@aws-sdk/client-s3": "^3.734.0",
|
"@aws-sdk/client-s3": "^3.734.0",
|
||||||
"@aws-sdk/s3-request-presigner": "^3.734.0",
|
"@aws-sdk/s3-request-presigner": "^3.734.0",
|
||||||
"@prisma/client": "^6.3.1",
|
"@prisma/client": "^6.3.1",
|
||||||
|
"axios": "^1.7.9",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
@ -2456,6 +2457,23 @@
|
|||||||
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
|
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/asynckit": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "1.7.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
|
||||||
|
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.15.6",
|
||||||
|
"form-data": "^4.0.0",
|
||||||
|
"proxy-from-env": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/base64-js": {
|
"node_modules/base64-js": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
@ -2646,6 +2664,18 @@
|
|||||||
"text-hex": "1.0.x"
|
"text-hex": "1.0.x"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/combined-stream": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"delayed-stream": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/concat-stream": {
|
"node_modules/concat-stream": {
|
||||||
"version": "1.6.2",
|
"version": "1.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
|
||||||
@ -2762,6 +2792,15 @@
|
|||||||
"ms": "2.0.0"
|
"ms": "2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/delayed-stream": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/depd": {
|
"node_modules/depd": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||||
@ -2886,6 +2925,21 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/es-set-tostringtag": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.6",
|
||||||
|
"has-tostringtag": "^1.0.2",
|
||||||
|
"hasown": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/escape-html": {
|
"node_modules/escape-html": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||||
@ -3026,6 +3080,41 @@
|
|||||||
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==",
|
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/follow-redirects": {
|
||||||
|
"version": "1.15.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||||
|
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"debug": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/form-data": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"es-set-tostringtag": "^2.1.0",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/forwarded": {
|
"node_modules/forwarded": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
@ -3129,6 +3218,21 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/has-tostringtag": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"has-symbols": "^1.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/hasown": {
|
"node_modules/hasown": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
@ -3661,6 +3765,12 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/qs": {
|
"node_modules/qs": {
|
||||||
"version": "6.13.0",
|
"version": "6.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
"@aws-sdk/client-s3": "^3.734.0",
|
"@aws-sdk/client-s3": "^3.734.0",
|
||||||
"@aws-sdk/s3-request-presigner": "^3.734.0",
|
"@aws-sdk/s3-request-presigner": "^3.734.0",
|
||||||
"@prisma/client": "^6.3.1",
|
"@prisma/client": "^6.3.1",
|
||||||
|
"axios": "^1.7.9",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
|
@ -22,6 +22,7 @@ model User {
|
|||||||
|
|
||||||
model Article {
|
model Article {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
|
importId Int @default(0)
|
||||||
title String
|
title String
|
||||||
excerpt String
|
excerpt String
|
||||||
content String
|
content String
|
||||||
@ -35,6 +36,7 @@ model Article {
|
|||||||
author User @relation(fields: [authorId], references: [id])
|
author User @relation(fields: [authorId], references: [id])
|
||||||
authorId String
|
authorId String
|
||||||
gallery GalleryImage[]
|
gallery GalleryImage[]
|
||||||
|
isActive Boolean @default(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
model GalleryImage {
|
model GalleryImage {
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
|
import axios from "axios";
|
||||||
import { prisma } from '../../../lib/prisma';
|
import { prisma } from '../../../lib/prisma';
|
||||||
import { AuthRequest } from '../../../middleware/auth';
|
import { AuthRequest } from '../../../middleware/auth';
|
||||||
import { checkPermission } from '../../../utils/permissions';
|
import { checkPermission } from '../../../utils/permissions';
|
||||||
import { logger } from '../../../config/logger';
|
import { logger } from '../../../config/logger';
|
||||||
import { Article } from "../../../types";
|
import { Article } from "../../../types";
|
||||||
|
import path from "path";
|
||||||
|
import {uploadBufferToS3} from "../../../services/s3Service";
|
||||||
|
|
||||||
const DEFAULT_COVER_IMAGE = 'https://images.unsplash.com/photo-1460661419201-fd4cecdf8a8b?auto=format&fit=crop&q=80&w=2070';
|
const DEFAULT_COVER_IMAGE = '/images/cover-placeholder.webp';
|
||||||
|
|
||||||
export async function getArticle(req: Request, res: Response) : Promise<void> {
|
export async function getArticle(req: Request, res: Response) : Promise<void> {
|
||||||
try {
|
try {
|
||||||
@ -140,6 +143,47 @@ export async function updateArticle(req: AuthRequest, res: Response) : Promise<v
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function activeArticle(req: AuthRequest, res: Response) : Promise<void> {
|
||||||
|
try {
|
||||||
|
const { isActive } = req.body;
|
||||||
|
|
||||||
|
if (!req.user) {
|
||||||
|
res.status(401).json({ error: 'Пользователь не вошел в систему' });
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const article = await prisma.article.findUnique({
|
||||||
|
where: { id: req.params.id }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!article) {
|
||||||
|
res.status(404).json({ error: 'Статья не найдена' });
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedArticle = await prisma.article.update({
|
||||||
|
where: { id: req.params.id },
|
||||||
|
data: {
|
||||||
|
isActive: !isActive
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
author: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
displayName: true,
|
||||||
|
email: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json(updatedArticle);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Ошибка активирования статьи:', error);
|
||||||
|
res.status(500).json({ error: 'Серверная ошибка' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function deleteArticle(req: AuthRequest, res: Response) : Promise<void> {
|
export async function deleteArticle(req: AuthRequest, res: Response) : Promise<void> {
|
||||||
try {
|
try {
|
||||||
if (!req.user) {
|
if (!req.user) {
|
||||||
@ -175,6 +219,11 @@ export async function deleteArticle(req: AuthRequest, res: Response) : Promise<v
|
|||||||
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;
|
||||||
|
|
||||||
|
if (!req.user) {
|
||||||
|
res.status(401).json({ error: 'Пользователь не вошел в систему' });
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!Array.isArray(articles) || articles.length === 0) {
|
if (!Array.isArray(articles) || articles.length === 0) {
|
||||||
res.status(400).json({ message: 'Ожидается непустой массив статей в теле запроса' });
|
res.status(400).json({ message: 'Ожидается непустой массив статей в теле запроса' });
|
||||||
return
|
return
|
||||||
@ -205,6 +254,39 @@ export async function importArticles(req: AuthRequest, res: Response) : Promise<
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Шаг 2: Обработка coverImage
|
// Шаг 2: Обработка coverImage
|
||||||
|
if (article.coverImage) {
|
||||||
|
const folder = `articles/${newArticle.id}`;
|
||||||
|
const imageUrl = article.coverImage;
|
||||||
|
|
||||||
|
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 = `cover-${Date.now()}${fileExtension}`;
|
||||||
|
|
||||||
|
// Загрузка буфера в S3
|
||||||
|
const uploadedUrl = await uploadBufferToS3(
|
||||||
|
imageBuffer,
|
||||||
|
folder,
|
||||||
|
filename,
|
||||||
|
contentType,
|
||||||
|
'original',
|
||||||
|
80
|
||||||
|
);
|
||||||
|
|
||||||
|
} catch (imageError) {
|
||||||
|
console.error(`Ошибка загрузки coverImage для статьи ${article.id}:`, imageError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
importedCount++;
|
importedCount++;
|
||||||
|
@ -10,6 +10,8 @@ export async function listArticles(req: Request, res: Response) {
|
|||||||
|
|
||||||
const perPage = 6;
|
const perPage = 6;
|
||||||
|
|
||||||
|
const isActiveParam = req.query.isDraft;
|
||||||
|
|
||||||
// Преобразование и проверка categoryId и cityId
|
// Преобразование и проверка categoryId и cityId
|
||||||
const catId = Number(req.query.categoryId);
|
const catId = Number(req.query.categoryId);
|
||||||
const citId = Number(req.query.cityId);
|
const citId = Number(req.query.cityId);
|
||||||
@ -17,6 +19,7 @@ export async function listArticles(req: Request, res: Response) {
|
|||||||
const where: Prisma.ArticleWhereInput = {
|
const where: Prisma.ArticleWhereInput = {
|
||||||
...(Number.isInteger(catId) && catId > 0 ? { categoryId: catId } : {}),
|
...(Number.isInteger(catId) && catId > 0 ? { categoryId: catId } : {}),
|
||||||
...(Number.isInteger(citId) && citId > 0 ? { cityId: citId } : {}),
|
...(Number.isInteger(citId) && citId > 0 ? { cityId: citId } : {}),
|
||||||
|
...(isActiveParam === "true" ? { isActive: false } : isActiveParam === "false" ? {} : { isActive: true }),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Рассчитываем пропуск записей
|
// Рассчитываем пропуск записей
|
||||||
|
@ -2,7 +2,7 @@ import express from 'express';
|
|||||||
import { auth } from '../../middleware/auth';
|
import { auth } from '../../middleware/auth';
|
||||||
import { searchArticles } from './controllers/search';
|
import { searchArticles } from './controllers/search';
|
||||||
import { listArticles } from './controllers/list';
|
import { listArticles } from './controllers/list';
|
||||||
import { getArticle, createArticle, updateArticle, deleteArticle, importArticles } from './controllers/crud';
|
import { getArticle, createArticle, updateArticle, deleteArticle, importArticles, activeArticle } from './controllers/crud';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
@ -19,6 +19,7 @@ router.post('/import', auth, importArticles);
|
|||||||
router.get('/:id', getArticle);
|
router.get('/:id', getArticle);
|
||||||
router.post('/', auth, createArticle);
|
router.post('/', auth, createArticle);
|
||||||
router.put('/:id', auth, updateArticle);
|
router.put('/:id', auth, updateArticle);
|
||||||
|
router.put('/active/:id', auth, activeArticle);
|
||||||
router.delete('/:id', auth, deleteArticle);
|
router.delete('/:id', auth, deleteArticle);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
100
src/routes/images/controllers/upload.ts
Normal file
100
src/routes/images/controllers/upload.ts
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import axios from "axios";
|
||||||
|
import { logger } from '../../../config/logger';
|
||||||
|
import { uploadBufferToS3, uploadFile } from '../../../services/s3Service';
|
||||||
|
|
||||||
|
|
||||||
|
// Обработчик загрузки файла в S3
|
||||||
|
export const handleFileUpload = async (req: Request, res: Response): Promise<void> => {
|
||||||
|
if (!req.file) {
|
||||||
|
res.status(400).json({ error: 'Нет файла для загрузки' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = req.file as Express.MulterS3.File;
|
||||||
|
const folder = req.body.folder as string;
|
||||||
|
const resolutionId = req.body.resolutionId as string;
|
||||||
|
const quality = Number(req.body.quality) || 80;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Upload file to S3
|
||||||
|
const fileUrl = await uploadFile(folder, file, resolutionId, quality);
|
||||||
|
|
||||||
|
res.json({ message: 'Файл успешно загружен', fileUrl });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Ошибка загрузки файла в S3: ${file.key}`);
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ error: 'Ошибка загрузки файла в S3' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle URL to image upload
|
||||||
|
export const handleUrlToImageUpload = async (req: Request, res: Response): Promise<void> => {
|
||||||
|
const { imageUrl, folder, resolutionId, quality, filename } = req.body;
|
||||||
|
|
||||||
|
if (!imageUrl) {
|
||||||
|
res.status(400).json({ error: 'No image URL provided' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch the image
|
||||||
|
const imageResponse = await axios.get(imageUrl, { responseType: 'arraybuffer' });
|
||||||
|
const imageBuffer = Buffer.from(imageResponse.data, 'binary');
|
||||||
|
|
||||||
|
// Upload to S3
|
||||||
|
const fileUrl = await uploadBufferToS3(
|
||||||
|
imageBuffer,
|
||||||
|
folder || 'images',
|
||||||
|
filename,
|
||||||
|
imageResponse.headers['content-type'],
|
||||||
|
resolutionId,
|
||||||
|
Number(quality) || 80
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({ message: 'Image successfully uploaded from URL', fileUrl });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error uploading image from URL: ${imageUrl}`);
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ error: 'Error uploading image from URL' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle buffer upload
|
||||||
|
export const handleBufferUpload = async (req: Request, res: Response): Promise<void> => {
|
||||||
|
const { buffer, folder, filename, mimetype, resolutionId, quality } = req.body;
|
||||||
|
|
||||||
|
if (!buffer) {
|
||||||
|
res.status(400).json({ error: 'No buffer provided' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Convert base64 buffer to actual buffer if needed
|
||||||
|
let actualBuffer: Buffer;
|
||||||
|
if (typeof buffer === 'string') {
|
||||||
|
// Assuming base64 encoded
|
||||||
|
actualBuffer = Buffer.from(buffer, 'base64');
|
||||||
|
} else if (Buffer.isBuffer(buffer)) {
|
||||||
|
actualBuffer = buffer;
|
||||||
|
} else {
|
||||||
|
throw new Error('Invalid buffer format');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload to S3
|
||||||
|
const fileUrl = await uploadBufferToS3(
|
||||||
|
actualBuffer,
|
||||||
|
folder || 'buffers',
|
||||||
|
filename,
|
||||||
|
mimetype,
|
||||||
|
resolutionId,
|
||||||
|
Number(quality) || 80
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({ message: 'Buffer successfully uploaded', fileUrl });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error uploading buffer to S3');
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ error: 'Error uploading buffer to S3' });
|
||||||
|
}
|
||||||
|
};
|
@ -1,58 +1,17 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { auth } from '../../middleware/auth';
|
import { auth } from '../../middleware/auth';
|
||||||
import { createS3Client, getMulterS3Config, uploadToS3 } from '../../services/s3Service';
|
import { multerUpload } from '../../services/s3Service';
|
||||||
import { logger } from '../../config/logger';
|
import { handleBufferUpload, handleFileUpload, handleUrlToImageUpload } from './controllers/upload';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// Конфигурация для подключения к S3
|
// Route for uploading image from a file
|
||||||
const BUCKET_NAME = process.env.AWS_S3_BUCKET || '';
|
router.post('/upload-url', auth, multerUpload, handleFileUpload);
|
||||||
const REGION = process.env.AWS_REGION || 'ru-central1'
|
|
||||||
const AWS_ENDPOINT = process.env.AWS_ENDPOINT || ''
|
|
||||||
const ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID || '';
|
|
||||||
const SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY || '';
|
|
||||||
|
|
||||||
// Создаем клиент S3
|
// Route for uploading image from a URL
|
||||||
const s3Client = createS3Client(REGION, AWS_ENDPOINT, ACCESS_KEY_ID, SECRET_ACCESS_KEY);
|
router.post('/upload-from-url', auth, handleUrlToImageUpload);
|
||||||
|
|
||||||
// Настройка Multer
|
// Route for uploading from a buffer
|
||||||
const upload = getMulterS3Config(s3Client, BUCKET_NAME);
|
router.post('/upload-buffer', auth, handleBufferUpload);
|
||||||
|
|
||||||
// Роут для загрузки изображения
|
|
||||||
router.post('/upload-url', auth, upload, async (req, res): Promise<void> => {
|
|
||||||
if (!req.file) {
|
|
||||||
res.status(400).json({ error: 'No file uploaded' });
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const file = req.file as Express.MulterS3.File;
|
|
||||||
const folder = req.body.folder as string;
|
|
||||||
const resolutionId = req.body.resolutionId as string;
|
|
||||||
const quality = 80;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Загружаем файл в S3 после оптимизации
|
|
||||||
const fileUrl = await uploadToS3(s3Client, BUCKET_NAME, folder, file, resolutionId, quality);
|
|
||||||
res.json({ message: 'Файл успешно загружен', fileUrl });
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Ошибка загрузки файла в S3: ${file.key}`);
|
|
||||||
console.error(error);
|
|
||||||
res.status(500).json({ error: 'Ошибка загрузки файла в S3' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
router.get('/:id', auth, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { id } = req.params;
|
|
||||||
const image = await getImage(id, BUCKET_NAME);
|
|
||||||
res.json(image);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error fetching image:', error);
|
|
||||||
res.status(500).json({ error: 'Failed to fetch image' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default router;
|
export default router;
|
@ -1,20 +1,30 @@
|
|||||||
import { S3Client, PutObjectCommand, GetObjectCommand} from '@aws-sdk/client-s3';
|
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
|
||||||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import multer from 'multer';
|
import multer from 'multer';
|
||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
|
import { logger } from '../config/logger';
|
||||||
import { imageResolutions } from '../config/imageResolutions';
|
import { imageResolutions } from '../config/imageResolutions';
|
||||||
|
|
||||||
// Функция для создания клиента S3
|
|
||||||
export const createS3Client = (region: string, endpoint: string, accessKeyId: string, secretAccessKey: string) => {
|
// Конфигурация для подключения к S3
|
||||||
return new S3Client({
|
const BUCKET_NAME = process.env.AWS_S3_BUCKET || '';
|
||||||
region: region,
|
const REGION = process.env.AWS_REGION || 'ru-central1';
|
||||||
endpoint: endpoint,
|
const AWS_ENDPOINT = process.env.AWS_ENDPOINT || '';
|
||||||
credentials: {
|
const ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID || '';
|
||||||
accessKeyId: accessKeyId,
|
const SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY || '';
|
||||||
secretAccessKey: secretAccessKey,
|
|
||||||
}
|
// Создаем клиент S3
|
||||||
});
|
export const s3Client = new S3Client({
|
||||||
};
|
region: REGION,
|
||||||
|
endpoint: AWS_ENDPOINT,
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: ACCESS_KEY_ID,
|
||||||
|
secretAccessKey: SECRET_ACCESS_KEY,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Export bucket name
|
||||||
|
export const bucketName = BUCKET_NAME;
|
||||||
|
|
||||||
// Функция для изменения размеров и оптимизации изображения
|
// Функция для изменения размеров и оптимизации изображения
|
||||||
const resizeAndOptimizeImage = async (buffer: Buffer, width: number, height: number, quality: number): Promise<Buffer> => {
|
const resizeAndOptimizeImage = async (buffer: Buffer, width: number, height: number, quality: number): Promise<Buffer> => {
|
||||||
@ -27,7 +37,7 @@ const resizeAndOptimizeImage = async (buffer: Buffer, width: number, height: num
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Функция для получения конфигурации Multer-S3
|
// Функция для получения конфигурации Multer-S3
|
||||||
export const getMulterS3Config = (
|
const getMulterS3Config = (
|
||||||
s3Client: S3Client,
|
s3Client: S3Client,
|
||||||
bucketName: string
|
bucketName: string
|
||||||
) => {
|
) => {
|
||||||
@ -42,6 +52,9 @@ export const getMulterS3Config = (
|
|||||||
}).single('file'); // Обрабатываем один файл
|
}).single('file'); // Обрабатываем один файл
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Default multer upload middleware using the default S3 client
|
||||||
|
export const multerUpload = getMulterS3Config(s3Client, bucketName);
|
||||||
|
|
||||||
// Функция для загрузки файла в S3 после оптимизации
|
// Функция для загрузки файла в S3 после оптимизации
|
||||||
export const uploadToS3 = async (
|
export const uploadToS3 = async (
|
||||||
s3Client: S3Client,
|
s3Client: S3Client,
|
||||||
@ -88,18 +101,78 @@ export const uploadToS3 = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/**
|
||||||
export const getImage = async (imageId: string, bucketName: string) => {
|
* Function to upload a buffer to S3
|
||||||
|
* @param buffer - The buffer to upload
|
||||||
|
* @param folder - The folder path in S3
|
||||||
|
* @param filename - Optional filename (if not provided, a UUID will be generated)
|
||||||
|
* @param mimetype - The MIME type of the buffer content
|
||||||
|
* @param resolutionId - Optional resolution identifier
|
||||||
|
* @param quality - Optional quality setting for image optimization
|
||||||
|
* @returns URL of the uploaded file
|
||||||
|
*/
|
||||||
|
export const uploadBufferToS3 = async (
|
||||||
|
buffer: Buffer,
|
||||||
|
folder: string,
|
||||||
|
filename?: string,
|
||||||
|
mimetype?: string,
|
||||||
|
resolutionId?: string,
|
||||||
|
quality: number = 80
|
||||||
|
): Promise<string> => {
|
||||||
try {
|
try {
|
||||||
const command = new GetObjectCommand({
|
// Generate filename if not provided
|
||||||
Bucket: bucketName,
|
const finalFilename = filename || `${uuidv4()}${mimetype ? getExtensionFromMimeType(mimetype) : '.bin'}`;
|
||||||
Key: `uploads/${imageId}`
|
|
||||||
});
|
|
||||||
|
|
||||||
const url = await getSignedUrl(s3Client, command, { expiresIn: 3600 });
|
// Create the S3 key (path)
|
||||||
return { url };
|
const key = folder ? `${folder}/${finalFilename}` : finalFilename;
|
||||||
|
|
||||||
|
// Here you could add image optimization for the buffer if needed
|
||||||
|
// For example, using Sharp library to resize/compress the image
|
||||||
|
|
||||||
|
// Upload buffer to S3
|
||||||
|
await s3Client.send(
|
||||||
|
new PutObjectCommand({
|
||||||
|
Bucket: bucketName,
|
||||||
|
Key: key,
|
||||||
|
Body: buffer,
|
||||||
|
ContentType: mimetype,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Return the file URL
|
||||||
|
const fileUrl = `https://${bucketName}.s3.regru.cloud/${finalFilename}`;
|
||||||
|
logger.info(`Buffer successfully uploaded to S3: ${key}`);
|
||||||
|
return fileUrl;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
logger.error('Error uploading buffer to S3');
|
||||||
|
console.error(error);
|
||||||
|
throw new Error('Error uploading buffer to S3');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
*/
|
|
||||||
|
// Helper function to get file extension from MIME type
|
||||||
|
function getExtensionFromMimeType(mimetype: string): string {
|
||||||
|
const mimeToExt: Record<string, string> = {
|
||||||
|
'image/jpeg': '.jpg',
|
||||||
|
'image/png': '.png',
|
||||||
|
'image/gif': '.gif',
|
||||||
|
'image/webp': '.webp',
|
||||||
|
'image/svg+xml': '.svg',
|
||||||
|
'application/pdf': '.pdf',
|
||||||
|
'text/plain': '.txt',
|
||||||
|
'text/html': '.html',
|
||||||
|
'application/json': '.json',
|
||||||
|
};
|
||||||
|
|
||||||
|
return mimeToExt[mimetype] || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simplified version of uploadToS3 that uses the default client and bucket
|
||||||
|
export const uploadFile = async (
|
||||||
|
folder: string,
|
||||||
|
file: Express.MulterS3.File,
|
||||||
|
resolutionId: string,
|
||||||
|
quality: number = 80
|
||||||
|
): Promise<string> => {
|
||||||
|
return uploadToS3(s3Client, bucketName, folder, file, resolutionId, quality);
|
||||||
|
};
|
@ -1,19 +1,3 @@
|
|||||||
import {CategoryName, CategoryIds} from './index';
|
|
||||||
|
|
||||||
/*
|
|
||||||
export interface UserPermissions {
|
|
||||||
categories: {
|
|
||||||
[key in CategoryName]: {
|
|
||||||
create: boolean;
|
|
||||||
edit: boolean;
|
|
||||||
delete: boolean;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
cities: number[];
|
|
||||||
isAdmin: boolean;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface UserPermissions {
|
export interface UserPermissions {
|
||||||
categories: {
|
categories: {
|
||||||
[key: string]: { edit: boolean; create: boolean; delete: boolean };
|
[key: string]: { edit: boolean; create: boolean; delete: boolean };
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
export interface Article {
|
export interface Article {
|
||||||
id: string;
|
id: string;
|
||||||
|
importId: number;
|
||||||
title: string;
|
title: string;
|
||||||
excerpt: string;
|
excerpt: string;
|
||||||
content: string;
|
content: string;
|
||||||
@ -13,6 +14,7 @@ export interface Article {
|
|||||||
likes: number;
|
likes: number;
|
||||||
dislikes: number;
|
dislikes: number;
|
||||||
userReaction?: 'like' | 'dislike' | null;
|
userReaction?: 'like' | 'dislike' | null;
|
||||||
|
isActive: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GalleryImage {
|
export interface GalleryImage {
|
||||||
@ -28,26 +30,3 @@ export interface Author {
|
|||||||
avatar: string;
|
avatar: string;
|
||||||
bio: string;
|
bio: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// export interface Category {
|
|
||||||
// id: number;
|
|
||||||
// name: CategoryName;
|
|
||||||
// }
|
|
||||||
|
|
||||||
export type CategoryName = 'Film' | 'Theater' | 'Music' | 'Sports' | 'Art' | 'Legends' | 'Anniversaries' | 'Memory';
|
|
||||||
export type CategoryIds = '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8';
|
|
||||||
|
|
||||||
export type City = 'New York' | 'London';
|
|
||||||
|
|
||||||
/*
|
|
||||||
export const CategoryMap: Record<string, number> = {
|
|
||||||
'Film': 1,
|
|
||||||
'Theater': 2,
|
|
||||||
'Music': 3,
|
|
||||||
'Sports': 4,
|
|
||||||
'Art': 5,
|
|
||||||
'Legends': 6,
|
|
||||||
'Anniversaries': 7,
|
|
||||||
'Memory': 8,
|
|
||||||
};
|
|
||||||
*/
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user