diff --git a/.env.example b/.env.example index 95f1546..b13016d 100644 --- a/.env.example +++ b/.env.example @@ -27,14 +27,18 @@ S3_ACCESS_KEY=minioadmin S3_SECRET_KEY=minioadmin # SeaweedFS live example (preferred aliases also work without S3_*): -# Use the public HTTPS URL (SERVICE_URL_S3). Port 8333 is often internal-only. +# Use the public HTTPS URL (SERVICE_URL_S3). Never use plain http:// for the public host — +# Coolify/Traefik returns HTTP 307 and AWS SDK PutObject cannot follow it. +# Do NOT use :8333 from outside the SeaweedFS network (often refused). # S3_ENDPOINT=https://s3.game-no.ir # SERVICE_URL_S3=https://s3.game-no.ir -# SERVICE_URL_S3_8333=https://s3.game-no.ir:8333 # SERVICE_USER_S3=... # SERVICE_PASSWORD_S3=... # AWS_ACCESS_KEY_ID=${SERVICE_USER_S3} # AWS_SECRET_ACCESS_KEY=${SERVICE_PASSWORD_S3} +# +# File uploads: multer uses MEMORY storage and streams the buffer straight to the +# S3 temp bucket. No Coolify persistent volume / uploads folder is required for this. S3_TEMP_BUCKET=temp S3_CERTIFICATES_BUCKET=certificates diff --git a/config/config.js b/config/config.js index 4991db9..6f4823a 100644 --- a/config/config.js +++ b/config/config.js @@ -31,11 +31,25 @@ const config = { // S3-compatible storage (MinIO locally, SeaweedFS in production) // Accepts S3_* directly, or SeaweedFS/AWS service env aliases from the host platform. - S3_ENDPOINT: - process.env.S3_ENDPOINT - || process.env.SERVICE_URL_S3 - || process.env.SERVICE_URL_S3_8333 - || 'http://localhost:9000', + // IMPORTANT: use https:// for public SeaweedFS — http:// gets a 307 that AWS SDK cannot follow on PUT. + S3_ENDPOINT: (() => { + const raw = ( + process.env.S3_ENDPOINT + || process.env.SERVICE_URL_S3 + || process.env.SERVICE_URL_S3_8333 + || 'http://localhost:9000' + ).trim().replace(/\/$/, ''); + + // Upgrade plain http → https for non-local endpoints (Coolify/proxy 307 breaks PutObject) + const isLocal = /^(http:\/\/)?(localhost|127\.0\.0\.1|minio)(:|\/|$)/i.test(raw) + || raw.startsWith('http://localhost') + || raw.startsWith('http://127.0.0.1') + || raw.startsWith('http://minio'); + if (raw.startsWith('http://') && !isLocal) { + return `https://${raw.slice('http://'.length)}`; + } + return raw; + })(), S3_REGION: process.env.S3_REGION || process.env.AWS_REGION || 'us-east-1', S3_ACCESS_KEY: process.env.S3_ACCESS_KEY @@ -59,10 +73,17 @@ const config = { ), SIGNED_URL_EXPIRES_IN: parseInt(process.env.SIGNED_URL_EXPIRES_IN, 10) || 900, // 15 minutes default // Public base URL for public buckets only (private buckets leave fileUrl empty) - S3_PUBLIC_BASE_URL: - process.env.S3_PUBLIC_BASE_URL - || process.env.SERVICE_URL_S3 - || '', + S3_PUBLIC_BASE_URL: (() => { + const raw = ( + process.env.S3_PUBLIC_BASE_URL + || process.env.SERVICE_URL_S3 + || '' + ).trim().replace(/\/$/, ''); + if (raw.startsWith('http://') && !/localhost|127\.0\.0\.1/i.test(raw)) { + return `https://${raw.slice('http://'.length)}`; + } + return raw; + })(), S3_CERTIFICATES_PUBLIC: parseBool(process.env.S3_CERTIFICATES_PUBLIC, false), S3_DOCUMENTS_PUBLIC: parseBool(process.env.S3_DOCUMENTS_PUBLIC, false), // SeaweedFS admin console (optional; not used by the S3 SDK client) diff --git a/utils/s3Client.js b/utils/s3Client.js index 496d89d..1bae207 100644 --- a/utils/s3Client.js +++ b/utils/s3Client.js @@ -80,6 +80,12 @@ const logS3Error = (label, error) => { + (status ? ` (HTTP ${status})` : '') + (bodyPreview ? ` body=${bodyPreview.replace(/\s+/g, ' ')}` : '') ); + if (status === 307 || /Temporary Redirect/i.test(String(error.message) + bodyPreview)) { + logger.error( + '[S3 Storage ERROR] Hint: HTTP 307 means S3_ENDPOINT is redirecting (often http→https). ' + + `Set S3_ENDPOINT=https://… (current: ${config.S3_ENDPOINT}). AWS SDK does not follow PUT redirects.` + ); + } }; const uploadToTempBucket = async (fileBuffer, filename, contentType = 'application/octet-stream') => {