From 433aa38218ed97317ca09606588bd703dad9e428 Mon Sep 17 00:00:00 2001 From: anibilag Date: Wed, 26 Feb 2025 23:35:32 +0300 Subject: [PATCH] =?UTF-8?q?=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=D0=BE=D0=BD?= =?UTF-8?q?=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=D1=87=D0=B5?= =?UTF-8?q?=D1=80=D0=BD=D0=BE=D0=B2=D0=B8=D0=BA=D0=BE=D0=B2,=20=D0=B4?= =?UTF-8?q?=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=20=D0=B2=20=D1=82?= =?UTF-8?q?=D0=B0=D0=B1=D0=BB=D0=B8=D1=86=D1=83=20=D1=81=D1=82=D0=B0=D1=82?= =?UTF-8?q?=D0=B5=D0=B9=20id=20=D0=BA=D0=BE=D1=82=D0=BE=D1=80=D1=8B=D0=B9?= =?UTF-8?q?=20=D0=B1=D1=83=D0=B4=D0=B5=D1=82=20=D1=81=D0=BE=D1=85=D1=80?= =?UTF-8?q?=D0=B0=D0=BD=D1=8F=D1=82=D1=8C=20=D0=B8=D0=B4=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D0=B8=D1=84=D0=B8=D0=BA=D0=B0=D1=82=D0=BE=D1=80=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B8=20=D0=B8=D0=BC=D0=BF=D0=BE=D1=80=D1=82=D0=B5.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 110 +++++++++++++++++++++ package.json | 1 + prisma/schema.prisma | 2 + src/routes/articles/controllers/crud.ts | 84 +++++++++++++++- src/routes/articles/controllers/list.ts | 3 + src/routes/articles/index.ts | 3 +- src/routes/images/controllers/upload.ts | 100 ++++++++++++++++++++ src/routes/images/index.ts | 57 ++--------- src/services/s3Service.ts | 121 +++++++++++++++++++----- src/types/auth.ts | 16 ---- src/types/index.ts | 25 +---- 11 files changed, 408 insertions(+), 114 deletions(-) create mode 100644 src/routes/images/controllers/upload.ts diff --git a/package-lock.json b/package-lock.json index aa19d64..268716e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@aws-sdk/client-s3": "^3.734.0", "@aws-sdk/s3-request-presigner": "^3.734.0", "@prisma/client": "^6.3.1", + "axios": "^1.7.9", "bcryptjs": "^2.4.3", "cors": "^2.8.5", "dotenv": "^16.4.7", @@ -2456,6 +2457,23 @@ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "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": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -2646,6 +2664,18 @@ "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": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", @@ -2762,6 +2792,15 @@ "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": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -2886,6 +2925,21 @@ "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": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -3026,6 +3080,41 @@ "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", "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": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -3129,6 +3218,21 @@ "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": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -3661,6 +3765,12 @@ "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": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", diff --git a/package.json b/package.json index 439f770..54ffc50 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@aws-sdk/client-s3": "^3.734.0", "@aws-sdk/s3-request-presigner": "^3.734.0", "@prisma/client": "^6.3.1", + "axios": "^1.7.9", "bcryptjs": "^2.4.3", "cors": "^2.8.5", "dotenv": "^16.4.7", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index fde9ae4..73261b6 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -22,6 +22,7 @@ model User { model Article { id String @id @default(uuid()) + importId Int @default(0) title String excerpt String content String @@ -35,6 +36,7 @@ model Article { author User @relation(fields: [authorId], references: [id]) authorId String gallery GalleryImage[] + isActive Boolean @default(false) } model GalleryImage { diff --git a/src/routes/articles/controllers/crud.ts b/src/routes/articles/controllers/crud.ts index c09f3a4..12e1f14 100644 --- a/src/routes/articles/controllers/crud.ts +++ b/src/routes/articles/controllers/crud.ts @@ -1,11 +1,14 @@ import { Request, Response } from 'express'; +import axios from "axios"; import { prisma } from '../../../lib/prisma'; import { AuthRequest } from '../../../middleware/auth'; import { checkPermission } from '../../../utils/permissions'; import { logger } from '../../../config/logger'; 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 { try { @@ -140,6 +143,47 @@ export async function updateArticle(req: AuthRequest, res: Response) : Promise { + 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 { try { if (!req.user) { @@ -175,6 +219,11 @@ export async function deleteArticle(req: AuthRequest, res: Response) : Promise { const articles: Article[] = req.body; + if (!req.user) { + res.status(401).json({ error: 'Пользователь не вошел в систему' }); + return + } + if (!Array.isArray(articles) || articles.length === 0) { res.status(400).json({ message: 'Ожидается непустой массив статей в теле запроса' }); return @@ -205,6 +254,39 @@ export async function importArticles(req: AuthRequest, res: Response) : Promise< }); // Шаг 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++; diff --git a/src/routes/articles/controllers/list.ts b/src/routes/articles/controllers/list.ts index 75cf607..6ec1699 100644 --- a/src/routes/articles/controllers/list.ts +++ b/src/routes/articles/controllers/list.ts @@ -10,6 +10,8 @@ export async function listArticles(req: Request, res: Response) { const perPage = 6; + const isActiveParam = req.query.isDraft; + // Преобразование и проверка categoryId и cityId const catId = Number(req.query.categoryId); const citId = Number(req.query.cityId); @@ -17,6 +19,7 @@ export async function listArticles(req: Request, res: Response) { const where: Prisma.ArticleWhereInput = { ...(Number.isInteger(catId) && catId > 0 ? { categoryId: catId } : {}), ...(Number.isInteger(citId) && citId > 0 ? { cityId: citId } : {}), + ...(isActiveParam === "true" ? { isActive: false } : isActiveParam === "false" ? {} : { isActive: true }), }; // Рассчитываем пропуск записей diff --git a/src/routes/articles/index.ts b/src/routes/articles/index.ts index d66e758..0e02b58 100644 --- a/src/routes/articles/index.ts +++ b/src/routes/articles/index.ts @@ -2,7 +2,7 @@ import express from 'express'; import { auth } from '../../middleware/auth'; import { searchArticles } from './controllers/search'; 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(); @@ -19,6 +19,7 @@ router.post('/import', auth, importArticles); router.get('/:id', getArticle); router.post('/', auth, createArticle); router.put('/:id', auth, updateArticle); +router.put('/active/:id', auth, activeArticle); router.delete('/:id', auth, deleteArticle); export default router; \ No newline at end of file diff --git a/src/routes/images/controllers/upload.ts b/src/routes/images/controllers/upload.ts new file mode 100644 index 0000000..219b386 --- /dev/null +++ b/src/routes/images/controllers/upload.ts @@ -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 => { + 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 => { + 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 => { + 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' }); + } +}; \ No newline at end of file diff --git a/src/routes/images/index.ts b/src/routes/images/index.ts index c37f79f..c53b05f 100644 --- a/src/routes/images/index.ts +++ b/src/routes/images/index.ts @@ -1,58 +1,17 @@ import express from 'express'; import { auth } from '../../middleware/auth'; -import { createS3Client, getMulterS3Config, uploadToS3 } from '../../services/s3Service'; -import { logger } from '../../config/logger'; +import { multerUpload } from '../../services/s3Service'; +import { handleBufferUpload, handleFileUpload, handleUrlToImageUpload } from './controllers/upload'; const router = express.Router(); -// Конфигурация для подключения к S3 -const BUCKET_NAME = process.env.AWS_S3_BUCKET || ''; -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 || ''; +// Route for uploading image from a file +router.post('/upload-url', auth, multerUpload, handleFileUpload); -// Создаем клиент S3 -const s3Client = createS3Client(REGION, AWS_ENDPOINT, ACCESS_KEY_ID, SECRET_ACCESS_KEY); +// Route for uploading image from a URL +router.post('/upload-from-url', auth, handleUrlToImageUpload); -// Настройка Multer -const upload = getMulterS3Config(s3Client, BUCKET_NAME); - -// Роут для загрузки изображения -router.post('/upload-url', auth, upload, async (req, res): Promise => { - 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' }); - } -}); -*/ +// Route for uploading from a buffer +router.post('/upload-buffer', auth, handleBufferUpload); export default router; \ No newline at end of file diff --git a/src/services/s3Service.ts b/src/services/s3Service.ts index e89a744..a0c5d08 100644 --- a/src/services/s3Service.ts +++ b/src/services/s3Service.ts @@ -1,20 +1,30 @@ -import { S3Client, PutObjectCommand, GetObjectCommand} from '@aws-sdk/client-s3'; -import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; +import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; +import { v4 as uuidv4 } from 'uuid'; import multer from 'multer'; import sharp from 'sharp'; +import { logger } from '../config/logger'; import { imageResolutions } from '../config/imageResolutions'; -// Функция для создания клиента S3 -export const createS3Client = (region: string, endpoint: string, accessKeyId: string, secretAccessKey: string) => { - return new S3Client({ - region: region, - endpoint: endpoint, - credentials: { - accessKeyId: accessKeyId, - secretAccessKey: secretAccessKey, - } - }); -}; + +// Конфигурация для подключения к S3 +const BUCKET_NAME = process.env.AWS_S3_BUCKET || ''; +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 +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 => { @@ -27,7 +37,7 @@ const resizeAndOptimizeImage = async (buffer: Buffer, width: number, height: num }; // Функция для получения конфигурации Multer-S3 -export const getMulterS3Config = ( +const getMulterS3Config = ( s3Client: S3Client, bucketName: string ) => { @@ -42,6 +52,9 @@ export const getMulterS3Config = ( }).single('file'); // Обрабатываем один файл }; +// Default multer upload middleware using the default S3 client +export const multerUpload = getMulterS3Config(s3Client, bucketName); + // Функция для загрузки файла в S3 после оптимизации export const uploadToS3 = async ( 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 => { try { - const command = new GetObjectCommand({ - Bucket: bucketName, - Key: `uploads/${imageId}` - }); + // Generate filename if not provided + const finalFilename = filename || `${uuidv4()}${mimetype ? getExtensionFromMimeType(mimetype) : '.bin'}`; - const url = await getSignedUrl(s3Client, command, { expiresIn: 3600 }); - return { url }; + // Create the S3 key (path) + 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) { - 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 = { + '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 => { + return uploadToS3(s3Client, bucketName, folder, file, resolutionId, quality); +}; \ No newline at end of file diff --git a/src/types/auth.ts b/src/types/auth.ts index 6406a47..7247ca9 100644 --- a/src/types/auth.ts +++ b/src/types/auth.ts @@ -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 { categories: { [key: string]: { edit: boolean; create: boolean; delete: boolean }; diff --git a/src/types/index.ts b/src/types/index.ts index ad93e9f..1176faa 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,5 +1,6 @@ export interface Article { id: string; + importId: number; title: string; excerpt: string; content: string; @@ -13,6 +14,7 @@ export interface Article { likes: number; dislikes: number; userReaction?: 'like' | 'dislike' | null; + isActive: boolean; } export interface GalleryImage { @@ -28,26 +30,3 @@ export interface Author { avatar: 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 = { - 'Film': 1, - 'Theater': 2, - 'Music': 3, - 'Sports': 4, - 'Art': 5, - 'Legends': 6, - 'Anniversaries': 7, - 'Memory': 8, -}; -*/