// ============================================================ // OBJECT STORAGE SETTINGS // One place that answers "which bucket, on which S3, with which credentials", // so pointing the whole app at a different MinIO — or at a real S3 — is one set // of variables rather than three schemes that grew separately. // // Each purpose (documents, generated images, audio backups) reads, in order: // // 1. its own variables AUDIO_BACKUPS_S3_ENDPOINT, ... // 2. the shared ones S3_ENDPOINT, S3_REGION, S3_ACCESS_KEY, ... // 3. a per-purpose bucket S3_BUCKET_AUDIO_BACKUPS // // So one external endpoint plus three bucket names is enough, while a purpose // that needs its own account keeps overriding everything. Credentials can be // given inline or, preferably, as a path to a file — a file keeps them out of // the process environment, which is where `docker inspect` reads from. // ============================================================ var fs = require('fs'); // Historic names, kept working so no deployment has to be edited to upgrade. var LEGACY = { documents: { endpoint: ['S3_ENDPOINT'], region: ['S3_REGION', 'AWS_BEDROCK_REGION'], bucket: ['S3_BUCKET'], accessKey: ['S3_ACCESS_KEY_ID', 'AWS_ACCESS_KEY_ID'], secretKey: ['S3_SECRET_ACCESS_KEY', 'AWS_SECRET_ACCESS_KEY'], forcePathStyle: ['S3_FORCE_PATH_STYLE'] } }; function prefixOf(purpose) { return String(purpose).toUpperCase().replace(/[^A-Z0-9]+/g, '_') + '_S3_'; } function firstValue(env, names) { for (var i = 0; i < names.length; i++) { var value = env[names[i]]; if (value !== undefined && value !== null && String(value) !== '') return String(value); } return ''; } // A *_FILE variant always wins over an inline value: if someone has gone to the // trouble of mounting a secret, an inherited environment variable must not // quietly take precedence over it. function readCredential(env, names) { var fileNames = names.map(function (name) { return name + '_FILE'; }); var file = firstValue(env, fileNames); if (file) { try { return fs.readFileSync(file, 'utf8').trim(); } catch (e) { throw new Error('Cannot read credential file ' + file + ': ' + e.message); } } return firstValue(env, names); } /** * Resolve the settings for one purpose. Returns null when no endpoint and no * bucket can be found, which callers read as "not configured" — never as an * error, because every one of these is optional. */ function settingsFor(purpose, env) { env = env || process.env; var own = prefixOf(purpose); var legacy = LEGACY[purpose] || {}; var endpoint = firstValue(env, [own + 'ENDPOINT'].concat(legacy.endpoint || [], ['S3_ENDPOINT'])); var bucket = firstValue(env, [own + 'BUCKET', 'S3_BUCKET_' + own.replace(/_S3_$/, '')].concat(legacy.bucket || [])); if (!bucket) return null; var region = firstValue(env, [own + 'REGION'].concat(legacy.region || [], ['S3_REGION'])) || 'us-east-1'; var accessKey = readCredential(env, [own + 'ACCESS_KEY'].concat(legacy.accessKey || [], ['S3_ACCESS_KEY'])); var secretKey = readCredential(env, [own + 'SECRET_KEY'].concat(legacy.secretKey || [], ['S3_SECRET_KEY'])); // MinIO needs path-style addressing; hosted S3 does not care, and Backblaze // prefers virtual-hosted. An explicit setting always wins. Where a purpose // already had its own default, that default is kept: documents has always // been off unless asked for, so a Backblaze endpoint keeps working. var forced = firstValue(env, [own + 'FORCE_PATH_STYLE'].concat(legacy.forcePathStyle || [], ['S3_FORCE_PATH_STYLE'])); var forcePathStyle = forced ? forced === 'true' : (legacy.forcePathStyle ? false : !!endpoint); return { purpose: purpose, endpoint: endpoint || undefined, region: region, bucket: bucket, forcePathStyle: forcePathStyle, credentials: (accessKey && secretKey) ? { accessKeyId: accessKey, secretAccessKey: secretKey } : undefined }; } function isConfigured(purpose, env) { return !!settingsFor(purpose, env); } // Built per call site; callers cache their own client. function createClient(settings) { var { S3Client } = require('@aws-sdk/client-s3'); var config = { region: settings.region, maxAttempts: 2, requestHandler: { connectionTimeout: 3000, requestTimeout: 20000 } }; if (settings.endpoint) { config.endpoint = settings.endpoint; config.forcePathStyle = settings.forcePathStyle; } if (settings.credentials) config.credentials = settings.credentials; return new S3Client(config); } // Convenience: settings plus a client, or null when the purpose is unconfigured. function storeFor(purpose, env) { var settings = settingsFor(purpose, env); if (!settings) return null; return { settings: settings, Bucket: settings.bucket, client: createClient(settings) }; } module.exports = { settingsFor, isConfigured, createClient, storeFor, prefixOf };