Добавлена функциональность черновиков, добавлен в таблицу статей 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/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",
|
||||
|
@ -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",
|
||||
|
@ -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 {
|
||||
|
@ -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<void> {
|
||||
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> {
|
||||
try {
|
||||
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> {
|
||||
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++;
|
||||
|
@ -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 }),
|
||||
};
|
||||
|
||||
// Рассчитываем пропуск записей
|
||||
|
@ -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;
|
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 { 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<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' });
|
||||
}
|
||||
});
|
||||
*/
|
||||
// Route for uploading from a buffer
|
||||
router.post('/upload-buffer', auth, handleBufferUpload);
|
||||
|
||||
export default router;
|
@ -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<Buffer> => {
|
||||
@ -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<string> => {
|
||||
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<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 {
|
||||
categories: {
|
||||
[key: string]: { edit: boolean; create: boolean; delete: boolean };
|
||||
|
@ -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<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