import { S3Client, PutObjectCommand, DeleteObjectCommand, GetObjectCommand, CreateBucketCommand, HeadBucketCommand, } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; import crypto from 'crypto'; import path from 'path'; import fssync from 'fs'; import fs from 'fs/promises'; export const isS3Enabled = !!(process.env.MINIO_ENDPOINT || process.env.S3_ENDPOINT); export const s3Client = isS3Enabled ? new S3Client({ endpoint: process.env.MINIO_ENDPOINT || process.env.S3_ENDPOINT, region: process.env.S3_REGION || 'us-east-1', credentials: { accessKeyId: process.env.MINIO_ACCESS_KEY || process.env.S3_ACCESS_KEY || '', secretAccessKey: process.env.MINIO_SECRET_KEY || process.env.S3_SECRET_KEY || '', }, forcePathStyle: true, }) : null; export const S3_BUCKET = process.env.MINIO_BUCKET || process.env.S3_BUCKET || 'files'; export async function ensureS3Bucket(): Promise { if (!s3Client) return; try { await s3Client.send(new HeadBucketCommand({ Bucket: S3_BUCKET })); } catch { try { await s3Client.send(new CreateBucketCommand({ Bucket: S3_BUCKET })); console.log(`[S3] Bucket "${S3_BUCKET}" создан`); } catch (createErr: unknown) { const msg = createErr instanceof Error ? createErr.message : String(createErr); if (!msg.includes('BucketAlreadyOwnedByYou') && !msg.includes('BucketAlreadyExists')) { console.error('[S3] Ошибка создания bucket:', createErr); } } } } export async function uploadToS3( buffer: Buffer, originalName: string, mimeType: string, ): Promise<{ url: string; key: string }> { if (!s3Client) throw new Error('S3 не настроен'); const ext = path.extname(originalName); const key = `${Date.now()}-${crypto.randomBytes(6).toString('hex')}${ext}`; await s3Client.send( new PutObjectCommand({ Bucket: S3_BUCKET, Key: key, Body: buffer, ContentType: mimeType, ContentDisposition: `attachment; filename*=UTF-8''${encodeURIComponent(originalName)}`, }), ); return { url: `/api/files/${key}`, key }; } // Загружает файл в S3 потоком с диска (используется для файлов > 5 МБ в S3-режиме) export async function uploadToS3FromFile( filePath: string, originalName: string, mimeType: string, ): Promise<{ url: string; key: string }> { if (!s3Client) throw new Error('S3 не настроен'); const ext = path.extname(originalName); const key = `${Date.now()}-${crypto.randomBytes(6).toString('hex')}${ext}`; const stat = await fs.stat(filePath); const fileStream = fssync.createReadStream(filePath); await s3Client.send( new PutObjectCommand({ Bucket: S3_BUCKET, Key: key, Body: fileStream, ContentLength: stat.size, ContentType: mimeType, ContentDisposition: `attachment; filename*=UTF-8''${encodeURIComponent(originalName)}`, }), ); return { url: `/api/files/${key}`, key }; } export async function deleteFromS3(key: string): Promise { if (!s3Client) return; try { await s3Client.send(new DeleteObjectCommand({ Bucket: S3_BUCKET, Key: key })); } catch (e: unknown) { const code = (e as any)?.name || (e as any)?.code; if (code !== 'NoSuchKey') { console.error('[S3] Ошибка удаления объекта:', e); } } } export async function streamFromS3(key: string): Promise<{ body: NodeJS.ReadableStream; contentType?: string; contentDisposition?: string; contentLength?: number; } | null> { if (!s3Client) return null; try { const result = await s3Client.send( new GetObjectCommand({ Bucket: S3_BUCKET, Key: key }), ); return { body: result.Body as NodeJS.ReadableStream, contentType: result.ContentType, contentDisposition: result.ContentDisposition, contentLength: result.ContentLength, }; } catch { return null; } } export async function getPresignedUrl( key: string, expiresInSeconds = 300, contentDisposition?: string, ): Promise { if (!s3Client) return null; try { const command = new GetObjectCommand({ Bucket: S3_BUCKET, Key: key, ResponseContentDisposition: contentDisposition, }); return await getSignedUrl(s3Client, command, { expiresIn: expiresInSeconds }); } catch (err) { console.error('[S3] Ошибка генерации presigned URL:', err); return null; } } export async function downloadFromS3(key: string, destPath: string): Promise { if (!s3Client) throw new Error('S3 не настроен'); const result = await s3Client.send(new GetObjectCommand({ Bucket: S3_BUCKET, Key: key })); if (!result.Body) throw new Error('Пустое тело файла из S3'); const writeStream = fssync.createWriteStream(destPath); const bodyStream = result.Body as NodeJS.ReadableStream; return new Promise((resolve, reject) => { bodyStream.pipe(writeStream); writeStream.on('finish', resolve); writeStream.on('error', reject); bodyStream.on('error', reject); }); }