import { S3Client, HeadBucketCommand, CreateBucketCommand, PutBucketPolicyCommand, PutObjectCommand, DeleteObjectCommand, GetObjectCommand, ListObjectsV2Command } from '@aws-sdk/client-s3'; // Prioriza as novas variáveis S3_*, mas mantém MINIO_* para compatibilidade const endpointHost = process.env.S3_ENDPOINT || process.env.MINIO_ENDPOINT || 'localhost'; const port = Number.parseInt(process.env.S3_PORT || process.env.MINIO_PORT || '9000', 10); const useSSL = (process.env.S3_USE_SSL || process.env.MINIO_USE_SSL) === 'true'; const protocol = useSSL ? 'https' : 'http'; // Para S3 externos (como RustFS), o endpointUrl pode não precisar da porta se for padrão (80/443) const endpointUrl = (port === 80 || port === 443) ? `${protocol}://${endpointHost}` : `${protocol}://${endpointHost}:${port}`; console.log(`[S3/MinIO] Configurando cliente: ${endpointUrl} (SSL: ${useSSL})`); export const bucketName = process.env.S3_BUCKET_NAME || process.env.MINIO_BUCKET_NAME || 'occto-images'; import { NodeHttpHandler } from '@smithy/node-http-handler'; export const s3Client = new S3Client({ region: 'us-east-1', endpoint: endpointUrl, forcePathStyle: true, credentials: { accessKeyId: process.env.S3_ACCESS_KEY || process.env.MINIO_ACCESS_KEY || 'admin', secretAccessKey: process.env.S3_SECRET_KEY || process.env.MINIO_SECRET_KEY || 'adminpassword', }, requestHandler: new NodeHttpHandler({ connectionTimeout: 10000, // 10 segundos socketTimeout: 10000, // 10 segundos }), }); type MetadataMap = Record | undefined; function extractContentType(metadata: MetadataMap) { if (!metadata) return undefined; const contentTypeKey = Object.keys(metadata).find(key => key.toLowerCase() === 'content-type'); return contentTypeKey ? metadata[contentTypeKey] : undefined; } function sanitizeMetadata(metadata: MetadataMap) { if (!metadata) return undefined; const entries = Object.entries(metadata).filter(([key]) => key.toLowerCase() !== 'content-type'); return entries.length ? Object.fromEntries(entries) : undefined; } export const minioClient = { async bucketExists(bucket: string) { try { await s3Client.send(new HeadBucketCommand({ Bucket: bucket })); return true; } catch (error: unknown) { const err = error as { $metadata?: { httpStatusCode?: number } }; if (err?.$metadata?.httpStatusCode === 404) return false; if (err?.$metadata?.httpStatusCode === 403) return true; // Sem permissão para HeadBucket, mas pode existir if (err?.$metadata?.httpStatusCode === 301) return true; // Bucket existe mas outra região throw error; } }, async makeBucket(bucket: string) { try { await s3Client.send(new CreateBucketCommand({ Bucket: bucket })); } catch (error: unknown) { const err = error as { $metadata?: { httpStatusCode?: number } }; if (err?.$metadata?.httpStatusCode === 409) { return; // bucket já existe } throw error; } }, async setBucketPolicy(bucket: string, policy: string) { try { await s3Client.send(new PutBucketPolicyCommand({ Bucket: bucket, Policy: policy })); } catch (error) { console.warn('Aviso: Não foi possível definir a política do bucket. Verifique as permissões do seu provedor S3.', error); } }, async putObject(bucket: string, key: string, body: Buffer | Uint8Array | string, size?: number, metadata?: MetadataMap) { const ContentType = extractContentType(metadata); const remainingMetadata = sanitizeMetadata(metadata); await s3Client.send(new PutObjectCommand({ Bucket: bucket, Key: key, Body: body, ContentLength: size, ContentType, Metadata: remainingMetadata, })); }, async getObject(bucket: string, key: string) { return s3Client.send(new GetObjectCommand({ Bucket: bucket, Key: key })); }, async removeObject(bucket: string, key: string) { await s3Client.send(new DeleteObjectCommand({ Bucket: bucket, Key: key })); }, async listObjects(bucket: string) { const command = new ListObjectsV2Command({ Bucket: bucket }); return s3Client.send(command); }, }; // Garante que o bucket exista antes de qualquer upload export async function ensureBucketExists() { try { const exists = await minioClient.bucketExists(bucketName); if (!exists) { console.log(`Tentando criar o bucket ${bucketName}...`); await minioClient.makeBucket(bucketName); const policy = { Version: '2012-10-17', Statement: [ { Effect: 'Allow', Principal: { AWS: ['*'] }, Action: ['s3:GetObject'], Resource: [`arn:aws:s3:::${bucketName}/*`], }, ], }; await minioClient.setBucketPolicy(bucketName, JSON.stringify(policy)); console.log(`Bucket ${bucketName} criado e política pública aplicada.`); } } catch (error) { console.error('Erro ao garantir existência do bucket S3:', error); } }