UNPKG

@push.rocks/smartbucket

Version:

A TypeScript library providing a cloud-agnostic interface for managing object storage with functionalities like bucket management, file and directory operations, and advanced features such as metadata handling and file locking.

254 lines (229 loc) 8.13 kB
import * as plugins from './plugins.js'; import * as interfaces from './interfaces.js'; export const reducePathDescriptorToPath = async (pathDescriptorArg: interfaces.IPathDecriptor): Promise<string> => { let returnPath = `` if (pathDescriptorArg.directory) { if (pathDescriptorArg.path && plugins.path.isAbsolute(pathDescriptorArg.path)) { console.warn('Directory is being ignored when path is absolute.'); returnPath = pathDescriptorArg.path; } else if (pathDescriptorArg.path) { returnPath = plugins.path.join(pathDescriptorArg.directory.getBasePath(), pathDescriptorArg.path); } } else if (pathDescriptorArg.path) { returnPath = pathDescriptorArg.path; } else { throw new Error('You must specify either a path or a directory.'); } if (returnPath.startsWith('/')) { returnPath = returnPath.substring(1); } return returnPath; } // Storage Descriptor Normalization export interface IStorageWarning { code: string; message: string; } export interface INormalizedStorageConfig { endpointUrl: string; host: string; protocol: 'http' | 'https'; port?: number; region: string; credentials: { accessKeyId: string; secretAccessKey: string; }; forcePathStyle: boolean; } function coerceBooleanMaybe(value: unknown): { value: boolean | undefined; warning?: IStorageWarning } { if (typeof value === 'boolean') return { value }; if (typeof value === 'string') { const v = value.trim().toLowerCase(); if (v === 'true' || v === '1') { return { value: true, warning: { code: 'SBK_S3_COERCED_USESSL', message: `Coerced useSsl='${value}' (string) to boolean true.` } }; } if (v === 'false' || v === '0') { return { value: false, warning: { code: 'SBK_S3_COERCED_USESSL', message: `Coerced useSsl='${value}' (string) to boolean false.` } }; } } return { value: undefined }; } function coercePortMaybe(port: unknown): { value: number | undefined; warning?: IStorageWarning } { if (port === undefined || port === null || port === '') return { value: undefined }; const n = typeof port === 'number' ? port : Number(String(port).trim()); if (!Number.isFinite(n) || !Number.isInteger(n) || n <= 0 || n > 65535) { return { value: undefined, warning: { code: 'SBK_S3_INVALID_PORT', message: `Invalid port '${String(port)}' - expected integer in [1..65535].` } }; } return { value: n }; } function sanitizeEndpointString(raw: unknown): { value: string; warnings: IStorageWarning[] } { const warnings: IStorageWarning[] = []; let s = String(raw ?? '').trim(); if (s !== String(raw ?? '')) { warnings.push({ code: 'SBK_S3_TRIMMED_ENDPOINT', message: 'Trimmed surrounding whitespace from endpoint.' }); } return { value: s, warnings }; } function parseEndpointHostPort( endpoint: string, provisionalProtocol: 'http' | 'https' ): { hadScheme: boolean; host: string; port?: number; extras: { droppedPath?: boolean; droppedQuery?: boolean; droppedCreds?: boolean } } { let url: URL | undefined; const extras: { droppedPath?: boolean; droppedQuery?: boolean; droppedCreds?: boolean } = {}; // Check if endpoint already has a scheme const hasScheme = /^https?:\/\//i.test(endpoint); // Try parsing as full URL first try { if (hasScheme) { url = new URL(endpoint); } else { // Not a full URL; try host[:port] by attaching provisional scheme // Remove anything after first '/' for safety const cleanEndpoint = endpoint.replace(/\/.*/, ''); url = new URL(`${provisionalProtocol}://${cleanEndpoint}`); } } catch (e) { throw new Error(`Unable to parse endpoint '${endpoint}'.`); } // Check for dropped components if (url.username || url.password) extras.droppedCreds = true; if (url.pathname && url.pathname !== '/') extras.droppedPath = true; if (url.search) extras.droppedQuery = true; const hadScheme = hasScheme; const host = url.hostname; // hostnames lowercased by URL; IPs preserved const port = url.port ? Number(url.port) : undefined; return { hadScheme, host, port, extras }; } export function normalizeStorageDescriptor( input: plugins.tsclass.storage.IStorageDescriptor, logger?: { warn: (msg: string) => void } ): { normalized: INormalizedStorageConfig; warnings: IStorageWarning[] } { const warnings: IStorageWarning[] = []; const logWarn = (w: IStorageWarning) => { warnings.push(w); if (logger) { logger.warn(`[SmartBucket] ${w.code}: ${w.message}`); } else { console.warn(`[SmartBucket] ${w.code}: ${w.message}`); } }; // Coerce and sanitize inputs const { value: coercedUseSsl, warning: useSslWarn } = coerceBooleanMaybe((input as any).useSsl); if (useSslWarn) logWarn(useSslWarn); const { value: coercedPort, warning: portWarn } = coercePortMaybe((input as any).port); if (portWarn) logWarn(portWarn); const { value: endpointStr, warnings: endpointSanWarnings } = sanitizeEndpointString((input as any).endpoint); endpointSanWarnings.forEach(logWarn); if (!endpointStr) { throw new Error('Storage endpoint is required (got empty string). Provide hostname or URL.'); } // Provisional protocol selection for parsing host:port forms const provisionalProtocol: 'http' | 'https' = coercedUseSsl === false ? 'http' : 'https'; const { hadScheme, host, port: epPort, extras } = parseEndpointHostPort(endpointStr, provisionalProtocol); if (extras.droppedCreds) { logWarn({ code: 'SBK_S3_DROPPED_CREDENTIALS', message: 'Ignored credentials in endpoint URL.' }); } if (extras.droppedPath) { logWarn({ code: 'SBK_S3_DROPPED_PATH', message: 'Removed path segment from endpoint URL; S3 endpoint should be host[:port] only.' }); } if (extras.droppedQuery) { logWarn({ code: 'SBK_S3_DROPPED_QUERY', message: 'Removed query string from endpoint URL; S3 endpoint should be host[:port] only.' }); } // Final protocol decision let finalProtocol: 'http' | 'https'; if (hadScheme) { // Scheme from endpoint wins const schemeFromEndpoint = endpointStr.trim().toLowerCase().startsWith('http://') ? 'http' : 'https'; finalProtocol = schemeFromEndpoint; if (typeof coercedUseSsl === 'boolean') { const expected = coercedUseSsl ? 'https' : 'http'; if (expected !== finalProtocol) { logWarn({ code: 'SBK_S3_SCHEME_CONFLICT', message: `useSsl=${String(coercedUseSsl)} conflicts with endpoint scheme '${finalProtocol}'; using endpoint scheme.` }); } } } else { if (typeof coercedUseSsl === 'boolean') { finalProtocol = coercedUseSsl ? 'https' : 'http'; } else { finalProtocol = 'https'; logWarn({ code: 'SBK_S3_GUESSED_PROTOCOL', message: "No scheme in endpoint and useSsl not provided; defaulting to 'https'." }); } } // Final port decision let finalPort: number | undefined = undefined; if (coercedPort !== undefined && epPort !== undefined && coercedPort !== epPort) { logWarn({ code: 'SBK_S3_PORT_CONFLICT', message: `Port in config (${coercedPort}) conflicts with endpoint port (${epPort}); using config port.` }); finalPort = coercedPort; } else { finalPort = (coercedPort !== undefined) ? coercedPort : epPort; } // Build canonical endpoint URL (origin only, no trailing slash) const url = new URL(`${finalProtocol}://${host}`); if (finalPort !== undefined) url.port = String(finalPort); const endpointUrl = url.origin; const region = input.region || 'us-east-1'; return { normalized: { endpointUrl, host, protocol: finalProtocol, port: finalPort, region, credentials: { accessKeyId: input.accessKey, secretAccessKey: input.accessSecret, }, forcePathStyle: true, }, warnings, }; }