@kumix/storage 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cloudinary.d.ts +5 -5
- package/dist/cloudinary.d.ts.map +1 -1
- package/dist/cloudinary.js +83 -46
- package/dist/cloudinary.js.map +1 -1
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +2 -2
- package/dist/helpers.js.map +1 -1
- package/dist/s3.js +101 -47
- package/dist/s3.js.map +1 -1
- package/package.json +3 -3
package/dist/helpers.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helpers.js","names":[],"sources":["../src/helpers.ts"],"sourcesContent":["/**\n * Storage helper utilities\n * Provides utility functions for file operations, MIME type detection, and path manipulation\n */\n\n// Compact, dependency-free MIME map covering the common types this package\n// deals with. Replaces `mime-types` (which drags in ~100KB of `mime-db` and a\n// CommonJS `require('path')`, breaking the cross-runtime `./helpers` entry).\nconst MIME_BY_EXTENSION: Record<string, string> = {\n // Images\n jpg: \"image/jpeg\",\n jpeg: \"image/jpeg\",\n png: \"image/png\",\n gif: \"image/gif\",\n webp: \"image/webp\",\n avif: \"image/avif\",\n bmp: \"image/bmp\",\n svg: \"image/svg+xml\",\n ico: \"image/x-icon\",\n tiff: \"image/tiff\",\n tif: \"image/tiff\",\n heic: \"image/heic\",\n heif: \"image/heif\",\n // Video\n mp4: \"video/mp4\",\n webm: \"video/webm\",\n mov: \"video/quicktime\",\n avi: \"video/x-msvideo\",\n mkv: \"video/x-matroska\",\n m4v: \"video/x-m4v\",\n wmv: \"video/x-ms-wmv\",\n flv: \"video/x-flv\",\n // Audio\n mp3: \"audio/mpeg\",\n wav: \"audio/wav\",\n ogg: \"audio/ogg\",\n flac: \"audio/flac\",\n aac: \"audio/aac\",\n m4a: \"audio/x-m4a\",\n opus: \"audio/opus\",\n // Documents\n pdf: \"application/pdf\",\n doc: \"application/msword\",\n docx: \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n xls: \"application/vnd.ms-excel\",\n xlsx: \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n ppt: \"application/vnd.ms-powerpoint\",\n pptx: \"application/vnd.openxmlformats-officedocument.presentationml.presentation\",\n // Text / code\n txt: \"text/plain\",\n md: \"text/markdown\",\n html: \"text/html\",\n htm: \"text/html\",\n css: \"text/css\",\n csv: \"text/csv\",\n ics: \"text/calendar\",\n json: \"application/json\",\n xml: \"application/xml\",\n yaml: \"application/x-yaml\",\n yml: \"application/x-yaml\",\n js: \"text/javascript\",\n mjs: \"text/javascript\",\n ts: \"text/typescript\",\n jsx: \"text/jsx\",\n tsx: \"text/tsx\",\n // Archives\n zip: \"application/zip\",\n gz: \"application/gzip\",\n tar: \"application/x-tar\",\n rar: \"application/vnd.rar\",\n \"7z\": \"application/x-7z-compressed\",\n bz2: \"application/x-bzip2\",\n // Fonts\n woff: \"font/woff\",\n woff2: \"font/woff2\",\n ttf: \"font/ttf\",\n otf: \"font/otf\",\n eot: \"application/vnd.ms-fontobject\",\n // Other\n bin: \"application/octet-stream\",\n exe: \"application/octet-stream\",\n};\n\nconst lookupMimeByExtension = (fileName: string): string | undefined => {\n const dot = fileName.lastIndexOf(\".\");\n if (dot < 0 || dot === fileName.length - 1) return undefined;\n const ext = fileName.slice(dot + 1).toLowerCase();\n return MIME_BY_EXTENSION[ext];\n};\n\n/**\n * Generate a unique file key with timestamp and random string\n * @param originalName Original file name\n * @param prefix Optional prefix for the key\n * @returns Unique file key\n * @public\n *\n * @example\n * ```typescript\n * import { generateFileKey } from '@kumix/storage/helpers';\n *\n * const key1 = generateFileKey('document.pdf');\n * // Returns: \"document-1703123456789-abc123.pdf\"\n *\n * const key2 = generateFileKey('photo.jpg', 'uploads/images');\n * // Returns: \"uploads/images/photo-1703123456789-def456.jpg\"\n * ```\n */\nexport function generateFileKey(originalName: string, prefix?: string): string {\n const timestamp = Date.now();\n const random = Math.random().toString(36).substring(2, 8);\n const lastDot = originalName.lastIndexOf(\".\");\n const baseName = lastDot > 0 ? originalName.slice(0, lastDot) : originalName;\n const extension = lastDot > 0 ? originalName.slice(lastDot) : \"\";\n\n const fileName = `${baseName}-${timestamp}-${random}${extension}`;\n\n return prefix ? `${prefix}/${fileName}` : fileName;\n}\n\n/**\n * Get MIME type from file name or extension\n * @param fileName File name or path\n * @returns MIME type string\n * @public\n *\n * @example\n * ```typescript\n * import { getMimeType } from '@kumix/storage/helpers';\n *\n * const type1 = getMimeType('document.pdf');\n * // Returns: \"application/pdf\"\n *\n * const type2 = getMimeType('photo.jpg');\n * // Returns: \"image/jpeg\"\n *\n * const type3 = getMimeType('unknown.xyz');\n * // Returns: \"application/octet-stream\"\n * ```\n */\nexport function getMimeType(fileName: string): string {\n const mimeType = lookupMimeByExtension(fileName);\n return mimeType || \"application/octet-stream\";\n}\n\n/**\n * Validate file size against maximum allowed size\n * @param fileSize File size in bytes\n * @param maxSize Maximum allowed size in bytes\n * @returns Validation result with error message if invalid\n * @public\n *\n * @example\n * ```typescript\n * import { validateFileSize } from '@kumix/storage/helpers';\n *\n * const result1 = validateFileSize(1024 * 1024, 5 * 1024 * 1024); // 1MB vs 5MB limit\n * // Returns: { valid: true }\n *\n * const result2 = validateFileSize(10 * 1024 * 1024, 5 * 1024 * 1024); // 10MB vs 5MB limit\n * // Returns: { valid: false, error: \"File size 10 MB exceeds maximum allowed size 5 MB\" }\n * ```\n */\nexport function validateFileSize(\n fileSize: number,\n maxSize: number,\n): { valid: boolean; error?: string } {\n if (fileSize > maxSize) {\n return {\n valid: false,\n error: `File size ${formatFileSize(fileSize)} exceeds maximum allowed size ${formatFileSize(maxSize)}`,\n };\n }\n return { valid: true };\n}\n\n/**\n * Validate file type against allowed types (MIME types or extensions)\n * @param fileName File name or path\n * @param allowedTypes Array of allowed MIME types or file extensions\n * @returns Validation result with error message if invalid\n * @public\n *\n * @example\n * ```typescript\n * import { validateFileType } from '@kumix/storage/helpers';\n *\n * // Using MIME types\n * const result1 = validateFileType('photo.jpg', ['image/*', 'application/pdf']);\n * // Returns: { valid: true }\n *\n * // Using file extensions\n * const result2 = validateFileType('document.txt', ['.pdf', '.doc', '.docx']);\n * // Returns: { valid: false, error: \"File type not allowed. Allowed types: .pdf, .doc, .docx\" }\n *\n * // Mixed MIME types and extensions\n * const result3 = validateFileType('video.mp4', ['image/*', '.pdf', 'video/mp4']);\n * // Returns: { valid: true }\n * ```\n */\nexport function validateFileType(\n fileName: string,\n allowedTypes: string[],\n): { valid: boolean; error?: string } {\n const mimeType = getMimeType(fileName);\n const extension = fileName.split(\".\").pop()?.toLowerCase();\n\n const isValidMime = allowedTypes.some((type) => {\n if (type.includes(\"*\")) {\n const baseType = type.split(\"/\")[0];\n return mimeType.startsWith(baseType);\n }\n return mimeType === type;\n });\n\n const isValidExtension = extension && allowedTypes.includes(`.${extension}`);\n\n if (!isValidMime && !isValidExtension) {\n return {\n valid: false,\n error: `File type not allowed. Allowed types: ${allowedTypes.join(\", \")}`,\n };\n }\n\n return { valid: true };\n}\n\n/**\n * Format file size in human readable format\n * @param bytes File size in bytes\n * @returns Formatted file size string\n * @public\n *\n * @example\n * ```typescript\n * import { formatFileSize } from '@kumix/storage/helpers';\n *\n * const size1 = formatFileSize(1024);\n * // Returns: \"1 KB\"\n *\n * const size2 = formatFileSize(1048576);\n * // Returns: \"1 MB\"\n *\n * const size3 = formatFileSize(1073741824);\n * // Returns: \"1 GB\"\n * ```\n */\nexport function formatFileSize(bytes: number): string {\n if (bytes === 0) return \"0 Bytes\";\n if (bytes < 0) return `-${formatFileSize(-bytes)}`;\n\n const k = 1024;\n const sizes = [\"Bytes\", \"KB\", \"MB\", \"GB\", \"TB\"];\n const i = Math.min(Math.floor(Math.log(bytes) / Math.log(k)), sizes.length - 1);\n\n return `${parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;\n}\n\n/**\n * Sanitize file name for S3 key by removing special characters\n * @param fileName Original file name\n * @returns Sanitized file name safe for S3 keys\n * @public\n *\n * @example\n * ```typescript\n * import { sanitizeFileName } from '@kumix/storage/helpers';\n *\n * const safe1 = sanitizeFileName('My Document (2023).pdf');\n * // Returns: \"my_document_2023.pdf\"\n *\n * const safe2 = sanitizeFileName('file@#$%name.txt');\n * // Returns: \"file_name.txt\"\n * ```\n */\nexport function sanitizeFileName(fileName: string): string {\n return (\n fileName\n .replace(/[^a-zA-Z0-9.-]/g, \"_\") // Replace special chars with underscore\n .replace(/_{2,}/g, \"_\") // Replace multiple underscores with single\n .replace(/^_|_$/g, \"\") // Remove leading/trailing underscores\n // Collapse any underscore immediately preceding the file extension so we\n // don't get \"name_.pdf\". Without this, `My Doc (2023).pdf` becomes\n // `my_doc_2023_.pdf` instead of the documented `my_doc_2023.pdf`.\n .replace(/_+(\\.[a-zA-Z0-9]+)$/, \"$1\")\n .toLowerCase()\n );\n}\n\n/**\n * Extract file extension from file name\n * @param fileName File name or path\n * @returns File extension with dot (e.g., '.pdf')\n * @public\n *\n * @example\n * ```typescript\n * import { getFileExtension } from '@kumix/storage/helpers';\n *\n * const ext1 = getFileExtension('document.pdf');\n * // Returns: \".pdf\"\n *\n * const ext2 = getFileExtension('photo.JPEG');\n * // Returns: \".jpeg\"\n *\n * const ext3 = getFileExtension('noextension');\n * // Returns: \"\"\n * ```\n */\nexport function getFileExtension(fileName: string): string {\n const lastDot = fileName.lastIndexOf(\".\");\n if (lastDot <= 0) return \"\";\n return `.${fileName.slice(lastDot + 1).toLowerCase()}`;\n}\n\n/**\n * Generate cache control header for HTTP responses\n * @param maxAge Cache max age in seconds (default: 1 year)\n * @param isPublic Whether cache should be public or private\n * @returns Cache control header string\n * @public\n *\n * @example\n * ```typescript\n * import { generateCacheControl } from '@kumix/storage/helpers';\n *\n * const cache1 = generateCacheControl();\n * // Returns: \"public, max-age=31536000\"\n *\n * const cache2 = generateCacheControl(3600, false);\n * // Returns: \"private, max-age=3600\"\n *\n * const cache3 = generateCacheControl(86400); // 1 day\n * // Returns: \"public, max-age=86400\"\n * ```\n */\nexport function generateCacheControl(\n maxAge: number = 31536000, // 1 year default\n isPublic: boolean = true,\n): string {\n const visibility = isPublic ? \"public\" : \"private\";\n return `${visibility}, max-age=${maxAge}`;\n}\n\n/**\n * Parse S3 URL to extract bucket and key\n * @param url S3 URL to parse\n * @returns Object containing bucket and key\n * @public\n *\n * @example\n * ```typescript\n * import { parseS3Url } from '@kumix/storage/helpers';\n *\n * // Virtual hosted-style URL\n * const result1 = parseS3Url('https://my-bucket.s3.us-east-1.amazonaws.com/folder/file.pdf');\n * // Returns: { bucket: \"my-bucket\", key: \"folder/file.pdf\" }\n *\n * // Path-style URL\n * const result2 = parseS3Url('https://s3.us-east-1.amazonaws.com/my-bucket/folder/file.pdf');\n * // Returns: { bucket: \"my-bucket\", key: \"folder/file.pdf\" }\n *\n * // Custom endpoint (R2, MinIO)\n * const result3 = parseS3Url('https://my-endpoint.com/my-bucket/folder/file.pdf');\n * // Returns: { bucket: \"my-bucket\", key: \"folder/file.pdf\" }\n * ```\n */\nexport function parseS3Url(url: string): { bucket?: string; key?: string } {\n try {\n const urlObj = new URL(url);\n\n // Handle different S3 URL formats\n if (urlObj.hostname.includes(\"amazonaws.com\")) {\n // Virtual hosted-style: https://bucket.s3.region.amazonaws.com/key\n if (urlObj.hostname.startsWith(\"s3\")) {\n // Path-style: https://s3.region.amazonaws.com/bucket/key\n const pathParts = urlObj.pathname.split(\"/\").filter(Boolean);\n return {\n bucket: pathParts[0],\n key: pathParts.slice(1).join(\"/\"),\n };\n } else {\n // Virtual hosted-style\n const bucket = urlObj.hostname.split(\".\")[0];\n const key = urlObj.pathname.substring(1); // Remove leading slash\n return { bucket, key };\n }\n }\n\n // Custom endpoint (like R2, MinIO)\n const pathParts = urlObj.pathname.split(\"/\").filter(Boolean);\n return {\n bucket: pathParts[0],\n key: pathParts.slice(1).join(\"/\"),\n };\n } catch {\n return {};\n }\n}\n\n/**\n * Build public URL for a file\n * @param baseUrl Base URL of the storage service\n * @param bucket Bucket name\n * @param key File key/path\n * @returns Complete public URL\n * @public\n *\n * @example\n * ```typescript\n * import { buildPublicUrl } from '@kumix/storage/helpers';\n *\n * const url1 = buildPublicUrl('https://cdn.example.com', 'my-bucket', 'folder/file.pdf');\n * // Returns: \"https://cdn.example.com/my-bucket/folder/file.pdf\"\n *\n * const url2 = buildPublicUrl('https://storage.example.com/', 'assets', 'images/logo.png');\n * // Returns: \"https://storage.example.com/assets/images/logo.png\"\n * ```\n */\nexport function buildPublicUrl(\n baseUrl: string,\n bucket: string,\n key: string,\n supabase?: boolean,\n): string {\n let cleanBaseUrl: string;\n\n cleanBaseUrl = baseUrl.replace(/\\/$/, \"\");\n const cleanKey = key.replace(/^\\//, \"\");\n\n if (supabase) {\n // Strip Supabase's storage subdomain / path prefix. The `.storage` host\n // label must be removed only when it appears as a subdomain of a Supabase\n // project host (e.g. `https://xyz.storage.supabase.co`), not when the\n // substring occurs inside a bucket name or path. Anchor with the Supabase\n // TLD to avoid corrupting URLs that merely contain \".storage\".\n cleanBaseUrl = cleanBaseUrl\n .replace(/\\.storage(?=\\.supabase\\.(?:co|in|net))/, \"\")\n .replace(/\\/storage\\/v1\\/s3$/, \"\"); // remove \"/storage/v1/s3\"\n cleanBaseUrl = `${cleanBaseUrl}/storage/v1/object/public`;\n }\n\n return `${cleanBaseUrl}/${bucket === \"\" ? \"\" : `${bucket}/`}${cleanKey}`;\n}\n\n/**\n * Validate S3 key format according to AWS S3 naming rules\n * @param key S3 object key to validate\n * @returns Validation result with error message if invalid\n * @public\n *\n * @example\n * ```typescript\n * import { validateS3Key } from '@kumix/storage/helpers';\n *\n * const result1 = validateS3Key('documents/report.pdf');\n * // Returns: { valid: true }\n *\n * const result2 = validateS3Key('/invalid/key/');\n * // Returns: { valid: false, error: \"Key cannot start or end with forward slash\" }\n * ```\n */\nexport function validateS3Key(key: string): { valid: boolean; error?: string } {\n if (!key || key.length === 0) {\n return { valid: false, error: \"Key cannot be empty\" };\n }\n\n if (key.length > 1024) {\n return { valid: false, error: \"Key cannot exceed 1024 characters\" };\n }\n\n if (key.startsWith(\"/\") || key.endsWith(\"/\")) {\n return {\n valid: false,\n error: \"Key cannot start or end with forward slash\",\n };\n }\n\n // Check for invalid characters\n const invalidChars = /[^\\w\\-_./]/;\n if (invalidChars.test(key)) {\n return { valid: false, error: \"Key contains invalid characters\" };\n }\n\n return { valid: true };\n}\n\n/**\n * Convert File object to Buffer for upload\n * @param file File object from browser input\n * @returns Promise that resolves to Buffer\n * @public\n *\n * @example\n * ```typescript\n * import { fileToBuffer } from '@kumix/storage/helpers';\n *\n * // In browser with file input\n * const fileInput = document.querySelector('input[type=\"file\"]') as HTMLInputElement;\n * const file = fileInput.files?.[0];\n *\n * if (file) {\n * const buffer = await fileToBuffer(file);\n * // Use buffer for upload\n * await storage.uploadFile('uploads/file.pdf', buffer);\n * }\n * ```\n */\nexport async function fileToBuffer(file: File): Promise<Uint8Array> {\n const arrayBuffer = await file.arrayBuffer();\n return new Uint8Array(arrayBuffer);\n}\n\n/**\n * Get file information from File object\n * @param file File object from browser input\n * @returns Object containing file metadata\n * @public\n *\n * @example\n * ```typescript\n * import { getFileInfo } from '@kumix/storage/helpers';\n *\n * // In browser with file input\n * const fileInput = document.querySelector('input[type=\"file\"]') as HTMLInputElement;\n * const file = fileInput.files?.[0];\n *\n * if (file) {\n * const info = getFileInfo(file);\n * console.log(info);\n * // Returns: {\n * // name: \"document.pdf\",\n * // size: 1048576,\n * // type: \"application/pdf\",\n * // lastModified: Date\n * // }\n * }\n * ```\n */\nexport function getFileInfo(file: File): {\n name: string;\n size: number;\n type: string;\n lastModified: Date;\n} {\n return {\n name: file.name,\n size: file.size,\n type: file.type || getMimeType(file.name),\n lastModified: new Date(file.lastModified),\n };\n}\n\n/**\n * Generate unique key with prefix and timestamp\n * @param fileName Original file name\n * @param prefix Optional prefix for the key\n * @param includeTimestamp Whether to include timestamp for uniqueness\n * @returns Unique file key\n * @public\n *\n * @example\n * ```typescript\n * import { generateUniqueKey } from '@kumix/storage/helpers';\n *\n * const key1 = generateUniqueKey('report.pdf', 'documents');\n * // Returns: \"documents/report-1703123456789-abc123.pdf\"\n *\n * const key2 = generateUniqueKey('photo.jpg', 'images', false);\n * // Returns: \"images/photo.jpg\"\n * ```\n */\nexport function generateUniqueKey(\n fileName: string,\n prefix?: string,\n includeTimestamp: boolean = true,\n): string {\n const sanitized = sanitizeFileName(fileName);\n const extension = getFileExtension(sanitized);\n const baseName = sanitized.replace(extension, \"\");\n\n let uniquePart = \"\";\n if (includeTimestamp) {\n const timestamp = Date.now();\n const random = Math.random().toString(36).substring(2, 8);\n uniquePart = `-${timestamp}-${random}`;\n }\n\n const finalName = `${baseName}${uniquePart}${extension}`;\n\n return prefix ? `${prefix}/${finalName}` : finalName;\n}\n\n/**\n * Extract bucket, key, and region from S3 URL\n * @param url S3 URL to parse\n * @returns Object containing bucket, key, and region information\n * @public\n *\n * @example\n * ```typescript\n * import { extractS3Info } from '@kumix/storage/helpers';\n *\n * // AWS S3 virtual hosted-style\n * const result1 = extractS3Info('https://my-bucket.s3.us-west-2.amazonaws.com/folder/file.pdf');\n * // Returns: { bucket: \"my-bucket\", key: \"folder/file.pdf\", region: \"us-west-2\" }\n *\n * // AWS S3 path-style\n * const result2 = extractS3Info('https://s3.eu-central-1.amazonaws.com/my-bucket/file.pdf');\n * // Returns: { bucket: \"my-bucket\", key: \"file.pdf\", region: \"eu-central-1\" }\n *\n * // Custom endpoint\n * const result3 = extractS3Info('https://minio.example.com/my-bucket/folder/file.pdf');\n * // Returns: { bucket: \"my-bucket\", key: \"folder/file.pdf\" }\n * ```\n */\nexport function extractS3Info(url: string): {\n bucket?: string;\n key?: string;\n region?: string;\n} {\n try {\n const urlObj = new URL(url);\n\n // AWS S3 URL patterns\n if (urlObj.hostname.includes(\"amazonaws.com\")) {\n if (urlObj.hostname.startsWith(\"s3\")) {\n // Path-style: https://s3.region.amazonaws.com/bucket/key\n const pathParts = urlObj.pathname.split(\"/\").filter(Boolean);\n const region = urlObj.hostname.split(\".\")[1];\n return {\n bucket: pathParts[0],\n key: pathParts.slice(1).join(\"/\"),\n region,\n };\n } else {\n // Virtual hosted-style: https://bucket.s3.region.amazonaws.com/key\n const hostParts = urlObj.hostname.split(\".\");\n const bucket = hostParts[0];\n const region = hostParts[2];\n const key = urlObj.pathname.substring(1);\n return { bucket, key, region };\n }\n }\n\n // Custom endpoint (R2, MinIO, etc.)\n const pathParts = urlObj.pathname.split(\"/\").filter(Boolean);\n return {\n bucket: pathParts[0],\n key: pathParts.slice(1).join(\"/\"),\n };\n } catch {\n return {};\n }\n}\n\n/**\n * Validate S3 configuration object\n * @param config S3 configuration object to validate\n * @returns Validation result with list of errors\n * @public\n *\n * @example\n * ```typescript\n * import { validateS3Config } from '@kumix/storage/helpers';\n *\n * const config = {\n * provider: 'aws',\n * region: 'us-east-1',\n * bucket: 'my-bucket',\n * accessKeyId: 'AKIA...',\n * secretAccessKey: 'secret...'\n * };\n *\n * const result = validateS3Config(config);\n * // Returns: { valid: true, errors: [] }\n *\n * const invalidConfig = { provider: 'aws' }; // Missing required fields\n * const result2 = validateS3Config(invalidConfig);\n * // Returns: { valid: false, errors: [\"Region is required\", \"Bucket is required\", ...] }\n * ```\n */\nexport function validateS3Config(config: Record<string, unknown>): {\n valid: boolean;\n errors: string[];\n} {\n const errors: string[] = [];\n\n if (!config.provider) {\n errors.push(\"Provider is required\");\n }\n\n if (!config.region) {\n errors.push(\"Region is required\");\n }\n\n if (!config.bucket) {\n errors.push(\"Bucket is required\");\n }\n\n if (!config.accessKeyId) {\n errors.push(\"Access Key ID is required\");\n }\n\n if (!config.secretAccessKey) {\n errors.push(\"Secret Access Key is required\");\n }\n\n // Validate bucket name\n if (config.bucket && typeof config.bucket === \"string\") {\n const bucketValidation = validateBucketName(config.bucket);\n if (!bucketValidation.valid) {\n errors.push(`Invalid bucket name: ${bucketValidation.error}`);\n }\n }\n\n return {\n valid: errors.length === 0,\n errors,\n };\n}\n\n/**\n * Validate S3 bucket name according to AWS naming rules\n * @param bucketName Bucket name to validate\n * @returns Validation result with error message if invalid\n * @public\n *\n * @example\n * ```typescript\n * import { validateBucketName } from '@kumix/storage/helpers';\n *\n * const result1 = validateBucketName('my-valid-bucket');\n * // Returns: { valid: true }\n *\n * const result2 = validateBucketName('My-Invalid-Bucket');\n * // Returns: { valid: false, error: \"Bucket name can only contain lowercase letters, numbers, dots, and hyphens\" }\n *\n * const result3 = validateBucketName('ab');\n * // Returns: { valid: false, error: \"Bucket name must be between 3 and 63 characters\" }\n * ```\n */\nexport function validateBucketName(bucketName: string): {\n valid: boolean;\n error?: string;\n} {\n if (!bucketName || bucketName.length === 0) {\n return { valid: false, error: \"Bucket name cannot be empty\" };\n }\n\n if (bucketName.length < 3 || bucketName.length > 63) {\n return {\n valid: false,\n error: \"Bucket name must be between 3 and 63 characters\",\n };\n }\n\n if (!/^[a-z0-9.-]+$/.test(bucketName)) {\n return {\n valid: false,\n error: \"Bucket name can only contain lowercase letters, numbers, dots, and hyphens\",\n };\n }\n\n if (bucketName.startsWith(\".\") || bucketName.endsWith(\".\")) {\n return {\n valid: false,\n error: \"Bucket name cannot start or end with a dot\",\n };\n }\n\n if (bucketName.startsWith(\"-\") || bucketName.endsWith(\"-\")) {\n return {\n valid: false,\n error: \"Bucket name cannot start or end with a hyphen\",\n };\n }\n\n if (bucketName.includes(\"..\")) {\n return {\n valid: false,\n error: \"Bucket name cannot contain consecutive dots\",\n };\n }\n\n return { valid: true };\n}\n\n/**\n * Generate content disposition header for file downloads\n * @param fileName File name for the download\n * @param disposition Whether to display inline or as attachment\n * @returns Content disposition header string\n * @public\n *\n * @example\n * ```typescript\n * import { getContentDisposition } from '@kumix/storage/helpers';\n *\n * const header1 = getContentDisposition('document.pdf');\n * // Returns: 'attachment; filename=\"document.pdf\"'\n *\n * const header2 = getContentDisposition('image.jpg', 'inline');\n * // Returns: 'inline; filename=\"image.jpg\"'\n *\n * const header3 = getContentDisposition('file@#$name.txt');\n * // Returns: 'attachment; filename=\"filename.txt\"'\n * ```\n */\nexport function getContentDisposition(\n fileName: string,\n disposition: \"inline\" | \"attachment\" = \"attachment\",\n): string {\n const sanitizedName = fileName.replace(/[^\\w\\s.-]/gi, \"\");\n return `${disposition}; filename=\"${sanitizedName}\"`;\n}\n\n/**\n * Check if file is an image based on MIME type\n * @param fileName File name or path\n * @returns True if file is an image\n * @public\n *\n * @example\n * ```typescript\n * import { isImageFile } from '@kumix/storage/helpers';\n *\n * const result1 = isImageFile('photo.jpg');\n * // Returns: true\n *\n * const result2 = isImageFile('document.pdf');\n * // Returns: false\n * ```\n */\nexport function isImageFile(fileName: string): boolean {\n const mimeType = getMimeType(fileName);\n return mimeType.startsWith(\"image/\");\n}\n\n/**\n * Check if file is a video based on MIME type\n * @param fileName File name or path\n * @returns True if file is a video\n * @public\n *\n * @example\n * ```typescript\n * import { isVideoFile } from '@kumix/storage/helpers';\n *\n * const result1 = isVideoFile('movie.mp4');\n * // Returns: true\n *\n * const result2 = isVideoFile('document.pdf');\n * // Returns: false\n * ```\n */\nexport function isVideoFile(fileName: string): boolean {\n const mimeType = getMimeType(fileName);\n return mimeType.startsWith(\"video/\");\n}\n\n/**\n * Check if file is an audio file based on MIME type\n * @param fileName File name or path\n * @returns True if file is an audio file\n * @public\n *\n * @example\n * ```typescript\n * import { isAudioFile } from '@kumix/storage/helpers';\n *\n * const result1 = isAudioFile('song.mp3');\n * // Returns: true\n *\n * const result2 = isAudioFile('document.pdf');\n * // Returns: false\n * ```\n */\nexport function isAudioFile(fileName: string): boolean {\n const mimeType = getMimeType(fileName);\n return mimeType.startsWith(\"audio/\");\n}\n\n/**\n * Check if file is a document based on MIME type\n * @param fileName File name or path\n * @returns True if file is a document\n * @public\n *\n * @example\n * ```typescript\n * import { isDocumentFile } from '@kumix/storage/helpers';\n *\n * const result1 = isDocumentFile('report.pdf');\n * // Returns: true\n *\n * const result2 = isDocumentFile('spreadsheet.xlsx');\n * // Returns: true\n *\n * const result3 = isDocumentFile('photo.jpg');\n * // Returns: false\n * ```\n */\nexport function isDocumentFile(fileName: string): boolean {\n const mimeType = getMimeType(fileName);\n const documentTypes = [\n \"application/pdf\",\n \"application/msword\",\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n \"application/vnd.ms-excel\",\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n \"application/vnd.ms-powerpoint\",\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\",\n \"text/plain\",\n \"text/csv\",\n ];\n return documentTypes.includes(mimeType);\n}\n\n/**\n * Normalize path separators and remove redundant slashes\n * @param path Path to normalize\n * @returns Normalized path\n * @public\n *\n * @example\n * ```typescript\n * import { normalizePath } from '@kumix/storage/helpers';\n *\n * const path1 = normalizePath('folder//subfolder///file.txt');\n * // Returns: \"folder/subfolder/file.txt\"\n *\n * const path2 = normalizePath('\\\\folder\\\\file.txt');\n * // Returns: \"folder/file.txt\"\n *\n * const path3 = normalizePath('/folder/file.txt/');\n * // Returns: \"folder/file.txt\"\n * ```\n */\nexport function normalizePath(path: string): string {\n return path\n .replace(/\\\\/g, \"/\") // Convert backslashes to forward slashes\n .replace(/\\/+/g, \"/\") // Replace multiple slashes with single slash\n .replace(/^\\/|\\/$/g, \"\"); // Remove leading and trailing slashes\n}\n\n/**\n * Join path segments safely with proper separators\n * @param segments Path segments to join\n * @returns Joined path\n * @public\n *\n * @example\n * ```typescript\n * import { joinPath } from '@kumix/storage/helpers';\n *\n * const path1 = joinPath('folder', 'subfolder', 'file.txt');\n * // Returns: \"folder/subfolder/file.txt\"\n *\n * const path2 = joinPath('/folder/', '/subfolder/', '/file.txt/');\n * // Returns: \"folder/subfolder/file.txt\"\n *\n * const path3 = joinPath('', 'folder', '', 'file.txt');\n * // Returns: \"folder/file.txt\"\n * ```\n */\nexport function joinPath(...segments: string[]): string {\n return segments\n .filter((segment) => segment && segment.trim() !== \"\")\n .map((segment) => segment.replace(/^\\/+|\\/+$/g, \"\"))\n .filter((segment) => segment !== \"\")\n .join(\"/\");\n}\n\n/**\n * Get parent directory path from a file key\n * @param key File key or path\n * @returns Parent directory path\n * @public\n *\n * @example\n * ```typescript\n * import { getParentPath } from '@kumix/storage/helpers';\n *\n * const parent1 = getParentPath('folder/subfolder/file.txt');\n * // Returns: \"folder/subfolder\"\n *\n * const parent2 = getParentPath('file.txt');\n * // Returns: \"\"\n *\n * const parent3 = getParentPath('folder/file.txt');\n * // Returns: \"folder\"\n * ```\n */\nexport function getParentPath(key: string): string {\n const normalizedKey = normalizePath(key);\n const lastSlashIndex = normalizedKey.lastIndexOf(\"/\");\n return lastSlashIndex === -1 ? \"\" : normalizedKey.substring(0, lastSlashIndex);\n}\n\n/**\n * Get filename from a file key or path\n * @param key File key or path\n * @returns Filename without path\n * @public\n *\n * @example\n * ```typescript\n * import { getFileName } from '@kumix/storage/helpers';\n *\n * const name1 = getFileName('folder/subfolder/document.pdf');\n * // Returns: \"document.pdf\"\n *\n * const name2 = getFileName('file.txt');\n * // Returns: \"file.txt\"\n *\n * const name3 = getFileName('folder/subfolder/');\n * // Returns: \"\"\n * ```\n */\nexport function getFileName(key: string): string {\n const normalizedKey = normalizePath(key);\n const lastSlashIndex = normalizedKey.lastIndexOf(\"/\");\n return lastSlashIndex === -1 ? normalizedKey : normalizedKey.substring(lastSlashIndex + 1);\n}\n\n/**\n * Convert base64 string to Buffer\n * @param base64 Base64 encoded string\n * @returns Buffer containing the decoded data\n * @public\n *\n * @example\n * ```typescript\n * import { base64ToBuffer } from '@kumix/storage/helpers';\n *\n * const base64 = 'SGVsbG8gV29ybGQ='; // \"Hello World\" in base64\n * const buffer = base64ToBuffer(base64);\n * console.log(buffer.toString()); // \"Hello World\"\n *\n * // For file uploads from base64 data\n * const imageBase64 = 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD...';\n * const cleanBase64 = imageBase64.split(',')[1]; // Remove data URL prefix\n * const imageBuffer = base64ToBuffer(cleanBase64);\n * await storage.uploadFile('images/photo.jpg', imageBuffer);\n * ```\n */\nexport function base64ToBuffer(base64: string): Uint8Array {\n const cleanBase64 = base64.includes(\",\") ? base64.split(\",\")[1] : base64;\n\n if (typeof Buffer !== \"undefined\") {\n return Buffer.from(cleanBase64, \"base64\");\n }\n\n const binary = atob(cleanBase64);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n return bytes;\n}\n\n/**\n * Convert Buffer to base64 string\n * @param buffer Buffer to encode\n * @param includeDataUrl Whether to include data URL prefix\n * @param mimeType MIME type for data URL (required if includeDataUrl is true)\n * @returns Base64 encoded string\n * @public\n *\n * @example\n * ```typescript\n * import { bufferToBase64 } from '@kumix/storage/helpers';\n *\n * const buffer = Buffer.from('Hello World');\n * const base64 = bufferToBase64(buffer);\n * // Returns: \"SGVsbG8gV29ybGQ=\"\n *\n * // With data URL for images\n * const imageBuffer = await storage.downloadFile('images/photo.jpg');\n * const dataUrl = bufferToBase64(imageBuffer.content!, true, 'image/jpeg');\n * // Returns: \"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD...\"\n *\n * // Use in HTML img tag\n * document.querySelector('img').src = dataUrl;\n * ```\n */\nexport function bufferToBase64(\n buffer: Uint8Array,\n includeDataUrl: boolean = false,\n mimeType?: string,\n): string {\n let base64: string;\n\n if (typeof Buffer !== \"undefined\" && buffer instanceof Buffer) {\n base64 = buffer.toString(\"base64\");\n } else {\n let binary = \"\";\n for (let i = 0; i < buffer.length; i++) {\n binary += String.fromCharCode(buffer[i]);\n }\n base64 = btoa(binary);\n }\n\n if (includeDataUrl) {\n if (!mimeType) {\n throw new Error(\"MIME type is required when includeDataUrl is true\");\n }\n return `data:${mimeType};base64,${base64}`;\n }\n\n return base64;\n}\n\n/**\n * Generate file hash/checksum for integrity verification\n * @param content File content as Buffer or string\n * @param algorithm Hash algorithm to use\n * @returns Hash string\n * @public\n *\n * @example\n * ```typescript\n * import { generateFileHash } from '@kumix/storage/helpers';\n *\n * const content = Buffer.from('Hello World');\n *\n * const md5Hash = generateFileHash(content, 'md5');\n * // Returns: \"b10a8db164e0754105b7a99be72e3fe5\"\n *\n * const sha256Hash = generateFileHash(content, 'sha256');\n * // Returns: \"a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e\"\n *\n * // For file integrity checking\n * const fileBuffer = await storage.downloadFile('document.pdf');\n * const currentHash = generateFileHash(fileBuffer.content!, 'sha256');\n * const expectedHash = 'a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e';\n *\n * if (currentHash === expectedHash) {\n * console.log('File integrity verified');\n * } else {\n * console.log('File may be corrupted');\n * }\n * ```\n */\nexport async function generateFileHash(\n content: Uint8Array | string,\n algorithm: \"md5\" | \"sha1\" | \"sha256\" = \"sha256\",\n): Promise<string> {\n // md5/sha1 are intentionally not implemented via Web Crypto (no MD5 in\n // subtle.digest, and SHA-1 is widely available but discouraged). When the\n // caller asks for one of those, fall back to node:crypto. For the default\n // (sha256) and sha1 we use Web Crypto when available so this helper works\n // cross-runtime.\n if (algorithm !== \"md5\" && typeof crypto !== \"undefined\" && crypto.subtle) {\n const rawData = typeof content === \"string\" ? new TextEncoder().encode(content) : content;\n const hashBuffer = await crypto.subtle.digest(\n algorithm === \"sha1\" ? \"SHA-1\" : \"SHA-256\",\n // biome-ignore lint/suspicious/noExplicitAny: BufferLike vs ArrayBufferView variance across runtimes\n rawData as any,\n );\n return Array.from(new Uint8Array(hashBuffer))\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\");\n }\n\n const nodeCrypto = await import(\"node:crypto\");\n const hash = nodeCrypto.createHash(algorithm);\n // biome-ignore lint/suspicious/noExplicitAny: BufferLike vs string union variance across runtimes\n hash.update(content as any);\n return hash.digest(\"hex\");\n}\n\n/**\n * Generate multiple unique keys at once for batch operations\n * @param fileNames Array of file names\n * @param prefix Optional prefix for all keys\n * @returns Array of unique keys\n * @public\n *\n * @example\n * ```typescript\n * import { generateBatchKeys } from '@kumix/storage/helpers';\n *\n * const fileNames = ['document1.pdf', 'document2.pdf', 'image.jpg'];\n * const keys = generateBatchKeys(fileNames, 'uploads');\n * // Returns: [\n * // \"uploads/document1-1703123456789-abc123.pdf\",\n * // \"uploads/document2-1703123456790-def456.pdf\",\n * // \"uploads/image-1703123456791-ghi789.jpg\"\n * // ]\n *\n * // Use for batch uploads\n * const files = [file1, file2, file3];\n * const keys = generateBatchKeys(files.map(f => f.name), 'batch-upload');\n *\n * for (let i = 0; i < files.length; i++) {\n * await storage.uploadFile(keys[i], files[i]);\n * }\n * ```\n */\nexport function generateBatchKeys(fileNames: string[], prefix?: string): string[] {\n return fileNames.map((fileName) => generateFileKey(fileName, prefix));\n}\n\n/**\n * Validate multiple files at once with consistent options\n * @param files Array of File objects to validate\n * @param options Validation options\n * @returns Array of validation results\n * @public\n *\n * @example\n * ```typescript\n * import { validateBatchFiles } from '@kumix/storage/helpers';\n *\n * const files = [file1, file2, file3]; // File objects from input\n * const options = {\n * maxSize: 5 * 1024 * 1024, // 5MB\n * allowedTypes: ['image/*', 'application/pdf'],\n * maxFiles: 10\n * };\n *\n * const results = validateBatchFiles(files, options);\n * // Returns: [\n * // { valid: true, fileName: \"image.jpg\" },\n * // { valid: false, fileName: \"large.pdf\", errors: [\"File size exceeds limit\"] },\n * // { valid: true, fileName: \"document.pdf\" }\n * // ]\n *\n * const validFiles = files.filter((_, index) => results[index].valid);\n * const invalidFiles = files.filter((_, index) => !results[index].valid);\n * ```\n */\nexport function validateBatchFiles(\n files: File[],\n options: {\n maxSize?: number;\n allowedTypes?: string[];\n maxFiles?: number;\n },\n): Array<{ valid: boolean; fileName: string; errors?: string[] }> {\n const results: Array<{\n valid: boolean;\n fileName: string;\n errors?: string[];\n }> = [];\n\n // Check max files limit\n if (options.maxFiles && files.length > options.maxFiles) {\n return files.map((file) => ({\n valid: false,\n fileName: file.name,\n errors: [`Maximum ${options.maxFiles} files allowed, but ${files.length} files provided`],\n }));\n }\n\n for (const file of files) {\n const errors: string[] = [];\n\n // Validate file size\n if (options.maxSize) {\n const sizeResult = validateFileSize(file.size, options.maxSize);\n if (!sizeResult.valid && sizeResult.error) {\n errors.push(sizeResult.error);\n }\n }\n\n // Validate file type\n if (options.allowedTypes) {\n const typeResult = validateFileType(file.name, options.allowedTypes);\n if (!typeResult.valid && typeResult.error) {\n errors.push(typeResult.error);\n }\n }\n\n results.push({\n valid: errors.length === 0,\n fileName: file.name,\n errors: errors.length > 0 ? errors : undefined,\n });\n }\n\n return results;\n}\n\n/**\n * Detect file type from content using magic numbers/file signatures\n * @param buffer File content buffer\n * @returns Detected MIME type or 'application/octet-stream' if unknown\n * @public\n *\n * @example\n * ```typescript\n * import { detectFileTypeFromContent } from '@kumix/storage/helpers';\n *\n * // Read file content\n * const fileBuffer = await storage.downloadFile('unknown-file');\n * const detectedType = detectFileTypeFromContent(fileBuffer.content!);\n *\n * console.log(detectedType);\n * // Returns: \"image/jpeg\" for JPEG files\n * // Returns: \"application/pdf\" for PDF files\n * // Returns: \"image/png\" for PNG files\n * // Returns: \"application/octet-stream\" for unknown types\n *\n * // Verify file type matches extension\n * const fileName = 'document.pdf';\n * const expectedType = getMimeType(fileName);\n * const actualType = detectFileTypeFromContent(fileBuffer.content!);\n *\n * if (expectedType !== actualType) {\n * console.warn('File extension does not match content type');\n * }\n * ```\n */\nexport function detectFileTypeFromContent(buffer: Uint8Array): string {\n if (buffer.length === 0) {\n return \"application/octet-stream\";\n }\n\n // Check magic numbers/file signatures\n const header = buffer.subarray(0, 16);\n\n // PDF\n if (header.subarray(0, 4).reduce((s, b) => s + String.fromCharCode(b), \"\") === \"%PDF\") {\n return \"application/pdf\";\n }\n\n // JPEG\n if (header[0] === 0xff && header[1] === 0xd8 && header[2] === 0xff) {\n return \"image/jpeg\";\n }\n\n // PNG\n const pngSig = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);\n if (header.length >= 8 && header.slice(0, 8).every((b, i) => b === pngSig[i])) {\n return \"image/png\";\n }\n\n // GIF\n const gifHead = header.subarray(0, 6).reduce((s, b) => s + String.fromCharCode(b), \"\");\n if (gifHead === \"GIF87a\" || gifHead === \"GIF89a\") {\n return \"image/gif\";\n }\n\n // WebP\n if (\n header.subarray(0, 4).reduce((s, b) => s + String.fromCharCode(b), \"\") === \"RIFF\" &&\n header.subarray(8, 12).reduce((s, b) => s + String.fromCharCode(b), \"\") === \"WEBP\"\n ) {\n return \"image/webp\";\n }\n\n // ZIP (includes DOCX, XLSX, etc.)\n if (header[0] === 0x50 && header[1] === 0x4b && (header[2] === 0x03 || header[2] === 0x05)) {\n const zipContent = new TextDecoder().decode(buffer.slice(0, Math.min(buffer.length, 1000)));\n if (zipContent.includes(\"word/\"))\n return \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\";\n if (zipContent.includes(\"xl/\"))\n return \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\";\n if (zipContent.includes(\"ppt/\"))\n return \"application/vnd.openxmlformats-officedocument.presentationml.presentation\";\n return \"application/zip\";\n }\n\n // MP4\n if (new TextDecoder().decode(header.subarray(4, 8)) === \"ftyp\") {\n return \"video/mp4\";\n }\n\n // MP3: MPEG audio frame sync is the high 11 bits = 0xFFE. This is `0xFF`\n // for byte 0 and the top 3 bits of byte 1 (mask 0xE0), so the check below\n // is the standard MP3 sync test.\n if (\n (header[0] === 0xff && (header[1] & 0xe0) === 0xe0) ||\n new TextDecoder().decode(header.subarray(0, 3)) === \"ID3\"\n ) {\n return \"audio/mpeg\";\n }\n\n // Text files (check if all bytes are printable ASCII or UTF-8)\n let isText = true;\n const sampleSize = Math.min(buffer.length, 512);\n for (let i = 0; i < sampleSize; i++) {\n const byte = buffer[i];\n if (byte === 0 || (byte < 32 && byte !== 9 && byte !== 10 && byte !== 13)) {\n isText = false;\n break;\n }\n }\n\n if (isText) {\n return \"text/plain\";\n }\n\n return \"application/octet-stream\";\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"helpers.js","names":[],"sources":["../src/helpers.ts"],"sourcesContent":["/**\n * Storage helper utilities\n * Provides utility functions for file operations, MIME type detection, and path manipulation\n */\n\n// Compact, dependency-free MIME map covering the common types this package\n// deals with. Replaces `mime-types` (which drags in ~100KB of `mime-db` and a\n// CommonJS `require('path')`, breaking the cross-runtime `./helpers` entry).\nconst MIME_BY_EXTENSION: Record<string, string> = {\n // Images\n jpg: \"image/jpeg\",\n jpeg: \"image/jpeg\",\n png: \"image/png\",\n gif: \"image/gif\",\n webp: \"image/webp\",\n avif: \"image/avif\",\n bmp: \"image/bmp\",\n svg: \"image/svg+xml\",\n ico: \"image/x-icon\",\n tiff: \"image/tiff\",\n tif: \"image/tiff\",\n heic: \"image/heic\",\n heif: \"image/heif\",\n // Video\n mp4: \"video/mp4\",\n webm: \"video/webm\",\n mov: \"video/quicktime\",\n avi: \"video/x-msvideo\",\n mkv: \"video/x-matroska\",\n m4v: \"video/x-m4v\",\n wmv: \"video/x-ms-wmv\",\n flv: \"video/x-flv\",\n // Audio\n mp3: \"audio/mpeg\",\n wav: \"audio/wav\",\n ogg: \"audio/ogg\",\n flac: \"audio/flac\",\n aac: \"audio/aac\",\n m4a: \"audio/x-m4a\",\n opus: \"audio/opus\",\n // Documents\n pdf: \"application/pdf\",\n doc: \"application/msword\",\n docx: \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n xls: \"application/vnd.ms-excel\",\n xlsx: \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n ppt: \"application/vnd.ms-powerpoint\",\n pptx: \"application/vnd.openxmlformats-officedocument.presentationml.presentation\",\n // Text / code\n txt: \"text/plain\",\n md: \"text/markdown\",\n html: \"text/html\",\n htm: \"text/html\",\n css: \"text/css\",\n csv: \"text/csv\",\n ics: \"text/calendar\",\n json: \"application/json\",\n xml: \"application/xml\",\n yaml: \"application/x-yaml\",\n yml: \"application/x-yaml\",\n js: \"text/javascript\",\n mjs: \"text/javascript\",\n ts: \"text/typescript\",\n jsx: \"text/jsx\",\n tsx: \"text/tsx\",\n // Archives\n zip: \"application/zip\",\n gz: \"application/gzip\",\n tar: \"application/x-tar\",\n rar: \"application/vnd.rar\",\n \"7z\": \"application/x-7z-compressed\",\n bz2: \"application/x-bzip2\",\n // Fonts\n woff: \"font/woff\",\n woff2: \"font/woff2\",\n ttf: \"font/ttf\",\n otf: \"font/otf\",\n eot: \"application/vnd.ms-fontobject\",\n // Other\n bin: \"application/octet-stream\",\n exe: \"application/octet-stream\",\n};\n\nconst lookupMimeByExtension = (fileName: string): string | undefined => {\n const dot = fileName.lastIndexOf(\".\");\n if (dot < 0 || dot === fileName.length - 1) return undefined;\n const ext = fileName.slice(dot + 1).toLowerCase();\n return MIME_BY_EXTENSION[ext];\n};\n\n/**\n * Generate a unique file key with timestamp and random string\n * @param originalName Original file name\n * @param prefix Optional prefix for the key\n * @returns Unique file key\n * @public\n *\n * @example\n * ```typescript\n * import { generateFileKey } from '@kumix/storage/helpers';\n *\n * const key1 = generateFileKey('document.pdf');\n * // Returns: \"document-1703123456789-abc123.pdf\"\n *\n * const key2 = generateFileKey('photo.jpg', 'uploads/images');\n * // Returns: \"uploads/images/photo-1703123456789-def456.jpg\"\n * ```\n */\nexport function generateFileKey(originalName: string, prefix?: string): string {\n const timestamp = Date.now();\n const random = Math.random().toString(36).substring(2, 8);\n const lastDot = originalName.lastIndexOf(\".\");\n const baseName = lastDot > 0 ? originalName.slice(0, lastDot) : originalName;\n const extension = lastDot > 0 ? originalName.slice(lastDot) : \"\";\n\n const fileName = `${baseName}-${timestamp}-${random}${extension}`;\n\n return prefix ? `${prefix}/${fileName}` : fileName;\n}\n\n/**\n * Get MIME type from file name or extension\n * @param fileName File name or path\n * @returns MIME type string\n * @public\n *\n * @example\n * ```typescript\n * import { getMimeType } from '@kumix/storage/helpers';\n *\n * const type1 = getMimeType('document.pdf');\n * // Returns: \"application/pdf\"\n *\n * const type2 = getMimeType('photo.jpg');\n * // Returns: \"image/jpeg\"\n *\n * const type3 = getMimeType('unknown.xyz');\n * // Returns: \"application/octet-stream\"\n * ```\n */\nexport function getMimeType(fileName: string): string {\n const mimeType = lookupMimeByExtension(fileName);\n return mimeType || \"application/octet-stream\";\n}\n\n/**\n * Validate file size against maximum allowed size\n * @param fileSize File size in bytes\n * @param maxSize Maximum allowed size in bytes\n * @returns Validation result with error message if invalid\n * @public\n *\n * @example\n * ```typescript\n * import { validateFileSize } from '@kumix/storage/helpers';\n *\n * const result1 = validateFileSize(1024 * 1024, 5 * 1024 * 1024); // 1MB vs 5MB limit\n * // Returns: { valid: true }\n *\n * const result2 = validateFileSize(10 * 1024 * 1024, 5 * 1024 * 1024); // 10MB vs 5MB limit\n * // Returns: { valid: false, error: \"File size 10 MB exceeds maximum allowed size 5 MB\" }\n * ```\n */\nexport function validateFileSize(\n fileSize: number,\n maxSize: number,\n): { valid: boolean; error?: string } {\n if (fileSize > maxSize) {\n return {\n valid: false,\n error: `File size ${formatFileSize(fileSize)} exceeds maximum allowed size ${formatFileSize(maxSize)}`,\n };\n }\n return { valid: true };\n}\n\n/**\n * Validate file type against allowed types (MIME types or extensions)\n * @param fileName File name or path\n * @param allowedTypes Array of allowed MIME types or file extensions\n * @returns Validation result with error message if invalid\n * @public\n *\n * @example\n * ```typescript\n * import { validateFileType } from '@kumix/storage/helpers';\n *\n * // Using MIME types\n * const result1 = validateFileType('photo.jpg', ['image/*', 'application/pdf']);\n * // Returns: { valid: true }\n *\n * // Using file extensions\n * const result2 = validateFileType('document.txt', ['.pdf', '.doc', '.docx']);\n * // Returns: { valid: false, error: \"File type not allowed. Allowed types: .pdf, .doc, .docx\" }\n *\n * // Mixed MIME types and extensions\n * const result3 = validateFileType('video.mp4', ['image/*', '.pdf', 'video/mp4']);\n * // Returns: { valid: true }\n * ```\n */\nexport function validateFileType(\n fileName: string,\n allowedTypes: string[],\n): { valid: boolean; error?: string } {\n const mimeType = getMimeType(fileName);\n const extension = fileName.split(\".\").pop()?.toLowerCase();\n\n const isValidMime = allowedTypes.some((type) => {\n if (type.includes(\"*\")) {\n const baseType = type.split(\"/\")[0];\n return mimeType.startsWith(baseType);\n }\n return mimeType === type;\n });\n\n const isValidExtension = extension && allowedTypes.includes(`.${extension}`);\n\n if (!isValidMime && !isValidExtension) {\n return {\n valid: false,\n error: `File type not allowed. Allowed types: ${allowedTypes.join(\", \")}`,\n };\n }\n\n return { valid: true };\n}\n\n/**\n * Format file size in human readable format\n * @param bytes File size in bytes\n * @returns Formatted file size string\n * @public\n *\n * @example\n * ```typescript\n * import { formatFileSize } from '@kumix/storage/helpers';\n *\n * const size1 = formatFileSize(1024);\n * // Returns: \"1 KB\"\n *\n * const size2 = formatFileSize(1048576);\n * // Returns: \"1 MB\"\n *\n * const size3 = formatFileSize(1073741824);\n * // Returns: \"1 GB\"\n * ```\n */\nexport function formatFileSize(bytes: number): string {\n if (bytes === 0) return \"0 Bytes\";\n if (bytes < 0) return `-${formatFileSize(-bytes)}`;\n\n const k = 1024;\n const sizes = [\"Bytes\", \"KB\", \"MB\", \"GB\", \"TB\"];\n const i = Math.min(Math.floor(Math.log(bytes) / Math.log(k)), sizes.length - 1);\n\n return `${parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;\n}\n\n/**\n * Sanitize file name for S3 key by removing special characters\n * @param fileName Original file name\n * @returns Sanitized file name safe for S3 keys\n * @public\n *\n * @example\n * ```typescript\n * import { sanitizeFileName } from '@kumix/storage/helpers';\n *\n * const safe1 = sanitizeFileName('My Document (2023).pdf');\n * // Returns: \"my_document_2023.pdf\"\n *\n * const safe2 = sanitizeFileName('file@#$%name.txt');\n * // Returns: \"file_name.txt\"\n * ```\n */\nexport function sanitizeFileName(fileName: string): string {\n return (\n fileName\n .replace(/[^a-zA-Z0-9.-]/g, \"_\") // Replace special chars with underscore\n .replace(/_{2,}/g, \"_\") // Replace multiple underscores with single\n .replace(/^_|_$/g, \"\") // Remove leading/trailing underscores\n // Collapse any underscore immediately preceding the file extension so we\n // don't get \"name_.pdf\". Without this, `My Doc (2023).pdf` becomes\n // `my_doc_2023_.pdf` instead of the documented `my_doc_2023.pdf`.\n .replace(/_+(\\.[a-zA-Z0-9]+)$/, \"$1\")\n .toLowerCase()\n );\n}\n\n/**\n * Extract file extension from file name\n * @param fileName File name or path\n * @returns File extension with dot (e.g., '.pdf')\n * @public\n *\n * @example\n * ```typescript\n * import { getFileExtension } from '@kumix/storage/helpers';\n *\n * const ext1 = getFileExtension('document.pdf');\n * // Returns: \".pdf\"\n *\n * const ext2 = getFileExtension('photo.JPEG');\n * // Returns: \".jpeg\"\n *\n * const ext3 = getFileExtension('noextension');\n * // Returns: \"\"\n * ```\n */\nexport function getFileExtension(fileName: string): string {\n const lastDot = fileName.lastIndexOf(\".\");\n // `lastDot <= 0` skips dotfiles/no-extension; also treat a trailing dot\n // (\"file.\") as having no extension instead of returning a bare \".\".\n if (lastDot <= 0 || lastDot === fileName.length - 1) return \"\";\n return `.${fileName.slice(lastDot + 1).toLowerCase()}`;\n}\n\n/**\n * Generate cache control header for HTTP responses\n * @param maxAge Cache max age in seconds (default: 1 year)\n * @param isPublic Whether cache should be public or private\n * @returns Cache control header string\n * @public\n *\n * @example\n * ```typescript\n * import { generateCacheControl } from '@kumix/storage/helpers';\n *\n * const cache1 = generateCacheControl();\n * // Returns: \"public, max-age=31536000\"\n *\n * const cache2 = generateCacheControl(3600, false);\n * // Returns: \"private, max-age=3600\"\n *\n * const cache3 = generateCacheControl(86400); // 1 day\n * // Returns: \"public, max-age=86400\"\n * ```\n */\nexport function generateCacheControl(\n maxAge: number = 31536000, // 1 year default\n isPublic: boolean = true,\n): string {\n const visibility = isPublic ? \"public\" : \"private\";\n return `${visibility}, max-age=${maxAge}`;\n}\n\n/**\n * Parse S3 URL to extract bucket and key\n * @param url S3 URL to parse\n * @returns Object containing bucket and key\n * @public\n *\n * @example\n * ```typescript\n * import { parseS3Url } from '@kumix/storage/helpers';\n *\n * // Virtual hosted-style URL\n * const result1 = parseS3Url('https://my-bucket.s3.us-east-1.amazonaws.com/folder/file.pdf');\n * // Returns: { bucket: \"my-bucket\", key: \"folder/file.pdf\" }\n *\n * // Path-style URL\n * const result2 = parseS3Url('https://s3.us-east-1.amazonaws.com/my-bucket/folder/file.pdf');\n * // Returns: { bucket: \"my-bucket\", key: \"folder/file.pdf\" }\n *\n * // Custom endpoint (R2, MinIO)\n * const result3 = parseS3Url('https://my-endpoint.com/my-bucket/folder/file.pdf');\n * // Returns: { bucket: \"my-bucket\", key: \"folder/file.pdf\" }\n * ```\n */\nexport function parseS3Url(url: string): { bucket?: string; key?: string } {\n try {\n const urlObj = new URL(url);\n\n // Handle different S3 URL formats\n if (urlObj.hostname.includes(\"amazonaws.com\")) {\n // Virtual hosted-style: https://bucket.s3.region.amazonaws.com/key\n if (urlObj.hostname.startsWith(\"s3\")) {\n // Path-style: https://s3.region.amazonaws.com/bucket/key\n const pathParts = urlObj.pathname.split(\"/\").filter(Boolean);\n return {\n bucket: pathParts[0],\n key: pathParts.slice(1).join(\"/\"),\n };\n } else {\n // Virtual hosted-style\n const bucket = urlObj.hostname.split(\".\")[0];\n const key = urlObj.pathname.substring(1); // Remove leading slash\n return { bucket, key };\n }\n }\n\n // Custom endpoint (like R2, MinIO)\n const pathParts = urlObj.pathname.split(\"/\").filter(Boolean);\n return {\n bucket: pathParts[0],\n key: pathParts.slice(1).join(\"/\"),\n };\n } catch {\n return {};\n }\n}\n\n/**\n * Build public URL for a file\n * @param baseUrl Base URL of the storage service\n * @param bucket Bucket name\n * @param key File key/path\n * @returns Complete public URL\n * @public\n *\n * @example\n * ```typescript\n * import { buildPublicUrl } from '@kumix/storage/helpers';\n *\n * const url1 = buildPublicUrl('https://cdn.example.com', 'my-bucket', 'folder/file.pdf');\n * // Returns: \"https://cdn.example.com/my-bucket/folder/file.pdf\"\n *\n * const url2 = buildPublicUrl('https://storage.example.com/', 'assets', 'images/logo.png');\n * // Returns: \"https://storage.example.com/assets/images/logo.png\"\n * ```\n */\nexport function buildPublicUrl(\n baseUrl: string,\n bucket: string,\n key: string,\n supabase?: boolean,\n): string {\n let cleanBaseUrl: string;\n\n cleanBaseUrl = baseUrl.replace(/\\/$/, \"\");\n const cleanKey = key.replace(/^\\//, \"\");\n\n if (supabase) {\n // Strip Supabase's storage subdomain / path prefix. The `.storage` host\n // label must be removed only when it appears as a subdomain of a Supabase\n // project host (e.g. `https://xyz.storage.supabase.co`), not when the\n // substring occurs inside a bucket name or path. Anchor with the Supabase\n // TLD to avoid corrupting URLs that merely contain \".storage\".\n cleanBaseUrl = cleanBaseUrl\n .replace(/\\.storage(?=\\.supabase\\.(?:co|in|net))/, \"\")\n .replace(/\\/storage\\/v1\\/s3$/, \"\"); // remove \"/storage/v1/s3\"\n cleanBaseUrl = `${cleanBaseUrl}/storage/v1/object/public`;\n }\n\n return `${cleanBaseUrl}/${bucket === \"\" ? \"\" : `${bucket}/`}${cleanKey}`;\n}\n\n/**\n * Validate S3 key format according to AWS S3 naming rules\n * @param key S3 object key to validate\n * @returns Validation result with error message if invalid\n * @public\n *\n * @example\n * ```typescript\n * import { validateS3Key } from '@kumix/storage/helpers';\n *\n * const result1 = validateS3Key('documents/report.pdf');\n * // Returns: { valid: true }\n *\n * const result2 = validateS3Key('/invalid/key/');\n * // Returns: { valid: false, error: \"Key cannot start or end with forward slash\" }\n * ```\n */\nexport function validateS3Key(key: string): { valid: boolean; error?: string } {\n if (!key || key.length === 0) {\n return { valid: false, error: \"Key cannot be empty\" };\n }\n\n if (key.length > 1024) {\n return { valid: false, error: \"Key cannot exceed 1024 characters\" };\n }\n\n if (key.startsWith(\"/\") || key.endsWith(\"/\")) {\n return {\n valid: false,\n error: \"Key cannot start or end with forward slash\",\n };\n }\n\n // Check for invalid characters\n const invalidChars = /[^\\w\\-_./]/;\n if (invalidChars.test(key)) {\n return { valid: false, error: \"Key contains invalid characters\" };\n }\n\n return { valid: true };\n}\n\n/**\n * Convert File object to Buffer for upload\n * @param file File object from browser input\n * @returns Promise that resolves to Buffer\n * @public\n *\n * @example\n * ```typescript\n * import { fileToBuffer } from '@kumix/storage/helpers';\n *\n * // In browser with file input\n * const fileInput = document.querySelector('input[type=\"file\"]') as HTMLInputElement;\n * const file = fileInput.files?.[0];\n *\n * if (file) {\n * const buffer = await fileToBuffer(file);\n * // Use buffer for upload\n * await storage.uploadFile('uploads/file.pdf', buffer);\n * }\n * ```\n */\nexport async function fileToBuffer(file: File): Promise<Uint8Array> {\n const arrayBuffer = await file.arrayBuffer();\n return new Uint8Array(arrayBuffer);\n}\n\n/**\n * Get file information from File object\n * @param file File object from browser input\n * @returns Object containing file metadata\n * @public\n *\n * @example\n * ```typescript\n * import { getFileInfo } from '@kumix/storage/helpers';\n *\n * // In browser with file input\n * const fileInput = document.querySelector('input[type=\"file\"]') as HTMLInputElement;\n * const file = fileInput.files?.[0];\n *\n * if (file) {\n * const info = getFileInfo(file);\n * console.log(info);\n * // Returns: {\n * // name: \"document.pdf\",\n * // size: 1048576,\n * // type: \"application/pdf\",\n * // lastModified: Date\n * // }\n * }\n * ```\n */\nexport function getFileInfo(file: File): {\n name: string;\n size: number;\n type: string;\n lastModified: Date;\n} {\n return {\n name: file.name,\n size: file.size,\n type: file.type || getMimeType(file.name),\n lastModified: new Date(file.lastModified),\n };\n}\n\n/**\n * Generate unique key with prefix and timestamp\n * @param fileName Original file name\n * @param prefix Optional prefix for the key\n * @param includeTimestamp Whether to include timestamp for uniqueness\n * @returns Unique file key\n * @public\n *\n * @example\n * ```typescript\n * import { generateUniqueKey } from '@kumix/storage/helpers';\n *\n * const key1 = generateUniqueKey('report.pdf', 'documents');\n * // Returns: \"documents/report-1703123456789-abc123.pdf\"\n *\n * const key2 = generateUniqueKey('photo.jpg', 'images', false);\n * // Returns: \"images/photo.jpg\"\n * ```\n */\nexport function generateUniqueKey(\n fileName: string,\n prefix?: string,\n includeTimestamp: boolean = true,\n): string {\n const sanitized = sanitizeFileName(fileName);\n const extension = getFileExtension(sanitized);\n // Strip only the TRAILING extension. `String.replace(extension, \"\")` is\n // unanchored and would remove the first occurrence of the extension substring\n // anywhere in the name (e.g. \".pdf\" inside \"my.pdf.notes.pdf\").\n const baseName = extension ? sanitized.slice(0, sanitized.length - extension.length) : sanitized;\n\n let uniquePart = \"\";\n if (includeTimestamp) {\n const timestamp = Date.now();\n const random = Math.random().toString(36).substring(2, 8);\n uniquePart = `-${timestamp}-${random}`;\n }\n\n const finalName = `${baseName}${uniquePart}${extension}`;\n\n return prefix ? `${prefix}/${finalName}` : finalName;\n}\n\n/**\n * Extract bucket, key, and region from S3 URL\n * @param url S3 URL to parse\n * @returns Object containing bucket, key, and region information\n * @public\n *\n * @example\n * ```typescript\n * import { extractS3Info } from '@kumix/storage/helpers';\n *\n * // AWS S3 virtual hosted-style\n * const result1 = extractS3Info('https://my-bucket.s3.us-west-2.amazonaws.com/folder/file.pdf');\n * // Returns: { bucket: \"my-bucket\", key: \"folder/file.pdf\", region: \"us-west-2\" }\n *\n * // AWS S3 path-style\n * const result2 = extractS3Info('https://s3.eu-central-1.amazonaws.com/my-bucket/file.pdf');\n * // Returns: { bucket: \"my-bucket\", key: \"file.pdf\", region: \"eu-central-1\" }\n *\n * // Custom endpoint\n * const result3 = extractS3Info('https://minio.example.com/my-bucket/folder/file.pdf');\n * // Returns: { bucket: \"my-bucket\", key: \"folder/file.pdf\" }\n * ```\n */\nexport function extractS3Info(url: string): {\n bucket?: string;\n key?: string;\n region?: string;\n} {\n try {\n const urlObj = new URL(url);\n\n // AWS S3 URL patterns\n if (urlObj.hostname.includes(\"amazonaws.com\")) {\n if (urlObj.hostname.startsWith(\"s3\")) {\n // Path-style: https://s3.region.amazonaws.com/bucket/key\n const pathParts = urlObj.pathname.split(\"/\").filter(Boolean);\n const region = urlObj.hostname.split(\".\")[1];\n return {\n bucket: pathParts[0],\n key: pathParts.slice(1).join(\"/\"),\n region,\n };\n } else {\n // Virtual hosted-style: https://bucket.s3.region.amazonaws.com/key\n const hostParts = urlObj.hostname.split(\".\");\n const bucket = hostParts[0];\n const region = hostParts[2];\n const key = urlObj.pathname.substring(1);\n return { bucket, key, region };\n }\n }\n\n // Custom endpoint (R2, MinIO, etc.)\n const pathParts = urlObj.pathname.split(\"/\").filter(Boolean);\n return {\n bucket: pathParts[0],\n key: pathParts.slice(1).join(\"/\"),\n };\n } catch {\n return {};\n }\n}\n\n/**\n * Validate S3 configuration object\n * @param config S3 configuration object to validate\n * @returns Validation result with list of errors\n * @public\n *\n * @example\n * ```typescript\n * import { validateS3Config } from '@kumix/storage/helpers';\n *\n * const config = {\n * provider: 'aws',\n * region: 'us-east-1',\n * bucket: 'my-bucket',\n * accessKeyId: 'AKIA...',\n * secretAccessKey: 'secret...'\n * };\n *\n * const result = validateS3Config(config);\n * // Returns: { valid: true, errors: [] }\n *\n * const invalidConfig = { provider: 'aws' }; // Missing required fields\n * const result2 = validateS3Config(invalidConfig);\n * // Returns: { valid: false, errors: [\"Region is required\", \"Bucket is required\", ...] }\n * ```\n */\nexport function validateS3Config(config: Record<string, unknown>): {\n valid: boolean;\n errors: string[];\n} {\n const errors: string[] = [];\n\n if (!config.provider) {\n errors.push(\"Provider is required\");\n }\n\n if (!config.region) {\n errors.push(\"Region is required\");\n }\n\n if (!config.bucket) {\n errors.push(\"Bucket is required\");\n }\n\n if (!config.accessKeyId) {\n errors.push(\"Access Key ID is required\");\n }\n\n if (!config.secretAccessKey) {\n errors.push(\"Secret Access Key is required\");\n }\n\n // Validate bucket name\n if (config.bucket && typeof config.bucket === \"string\") {\n const bucketValidation = validateBucketName(config.bucket);\n if (!bucketValidation.valid) {\n errors.push(`Invalid bucket name: ${bucketValidation.error}`);\n }\n }\n\n return {\n valid: errors.length === 0,\n errors,\n };\n}\n\n/**\n * Validate S3 bucket name according to AWS naming rules\n * @param bucketName Bucket name to validate\n * @returns Validation result with error message if invalid\n * @public\n *\n * @example\n * ```typescript\n * import { validateBucketName } from '@kumix/storage/helpers';\n *\n * const result1 = validateBucketName('my-valid-bucket');\n * // Returns: { valid: true }\n *\n * const result2 = validateBucketName('My-Invalid-Bucket');\n * // Returns: { valid: false, error: \"Bucket name can only contain lowercase letters, numbers, dots, and hyphens\" }\n *\n * const result3 = validateBucketName('ab');\n * // Returns: { valid: false, error: \"Bucket name must be between 3 and 63 characters\" }\n * ```\n */\nexport function validateBucketName(bucketName: string): {\n valid: boolean;\n error?: string;\n} {\n if (!bucketName || bucketName.length === 0) {\n return { valid: false, error: \"Bucket name cannot be empty\" };\n }\n\n if (bucketName.length < 3 || bucketName.length > 63) {\n return {\n valid: false,\n error: \"Bucket name must be between 3 and 63 characters\",\n };\n }\n\n if (!/^[a-z0-9.-]+$/.test(bucketName)) {\n return {\n valid: false,\n error: \"Bucket name can only contain lowercase letters, numbers, dots, and hyphens\",\n };\n }\n\n if (bucketName.startsWith(\".\") || bucketName.endsWith(\".\")) {\n return {\n valid: false,\n error: \"Bucket name cannot start or end with a dot\",\n };\n }\n\n if (bucketName.startsWith(\"-\") || bucketName.endsWith(\"-\")) {\n return {\n valid: false,\n error: \"Bucket name cannot start or end with a hyphen\",\n };\n }\n\n if (bucketName.includes(\"..\")) {\n return {\n valid: false,\n error: \"Bucket name cannot contain consecutive dots\",\n };\n }\n\n return { valid: true };\n}\n\n/**\n * Generate content disposition header for file downloads\n * @param fileName File name for the download\n * @param disposition Whether to display inline or as attachment\n * @returns Content disposition header string\n * @public\n *\n * @example\n * ```typescript\n * import { getContentDisposition } from '@kumix/storage/helpers';\n *\n * const header1 = getContentDisposition('document.pdf');\n * // Returns: 'attachment; filename=\"document.pdf\"'\n *\n * const header2 = getContentDisposition('image.jpg', 'inline');\n * // Returns: 'inline; filename=\"image.jpg\"'\n *\n * const header3 = getContentDisposition('file@#$name.txt');\n * // Returns: 'attachment; filename=\"filename.txt\"'\n * ```\n */\nexport function getContentDisposition(\n fileName: string,\n disposition: \"inline\" | \"attachment\" = \"attachment\",\n): string {\n const sanitizedName = fileName.replace(/[^\\w\\s.-]/gi, \"\");\n return `${disposition}; filename=\"${sanitizedName}\"`;\n}\n\n/**\n * Check if file is an image based on MIME type\n * @param fileName File name or path\n * @returns True if file is an image\n * @public\n *\n * @example\n * ```typescript\n * import { isImageFile } from '@kumix/storage/helpers';\n *\n * const result1 = isImageFile('photo.jpg');\n * // Returns: true\n *\n * const result2 = isImageFile('document.pdf');\n * // Returns: false\n * ```\n */\nexport function isImageFile(fileName: string): boolean {\n const mimeType = getMimeType(fileName);\n return mimeType.startsWith(\"image/\");\n}\n\n/**\n * Check if file is a video based on MIME type\n * @param fileName File name or path\n * @returns True if file is a video\n * @public\n *\n * @example\n * ```typescript\n * import { isVideoFile } from '@kumix/storage/helpers';\n *\n * const result1 = isVideoFile('movie.mp4');\n * // Returns: true\n *\n * const result2 = isVideoFile('document.pdf');\n * // Returns: false\n * ```\n */\nexport function isVideoFile(fileName: string): boolean {\n const mimeType = getMimeType(fileName);\n return mimeType.startsWith(\"video/\");\n}\n\n/**\n * Check if file is an audio file based on MIME type\n * @param fileName File name or path\n * @returns True if file is an audio file\n * @public\n *\n * @example\n * ```typescript\n * import { isAudioFile } from '@kumix/storage/helpers';\n *\n * const result1 = isAudioFile('song.mp3');\n * // Returns: true\n *\n * const result2 = isAudioFile('document.pdf');\n * // Returns: false\n * ```\n */\nexport function isAudioFile(fileName: string): boolean {\n const mimeType = getMimeType(fileName);\n return mimeType.startsWith(\"audio/\");\n}\n\n/**\n * Check if file is a document based on MIME type\n * @param fileName File name or path\n * @returns True if file is a document\n * @public\n *\n * @example\n * ```typescript\n * import { isDocumentFile } from '@kumix/storage/helpers';\n *\n * const result1 = isDocumentFile('report.pdf');\n * // Returns: true\n *\n * const result2 = isDocumentFile('spreadsheet.xlsx');\n * // Returns: true\n *\n * const result3 = isDocumentFile('photo.jpg');\n * // Returns: false\n * ```\n */\nexport function isDocumentFile(fileName: string): boolean {\n const mimeType = getMimeType(fileName);\n const documentTypes = [\n \"application/pdf\",\n \"application/msword\",\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n \"application/vnd.ms-excel\",\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n \"application/vnd.ms-powerpoint\",\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\",\n \"text/plain\",\n \"text/csv\",\n ];\n return documentTypes.includes(mimeType);\n}\n\n/**\n * Normalize path separators and remove redundant slashes\n * @param path Path to normalize\n * @returns Normalized path\n * @public\n *\n * @example\n * ```typescript\n * import { normalizePath } from '@kumix/storage/helpers';\n *\n * const path1 = normalizePath('folder//subfolder///file.txt');\n * // Returns: \"folder/subfolder/file.txt\"\n *\n * const path2 = normalizePath('\\\\folder\\\\file.txt');\n * // Returns: \"folder/file.txt\"\n *\n * const path3 = normalizePath('/folder/file.txt/');\n * // Returns: \"folder/file.txt\"\n * ```\n */\nexport function normalizePath(path: string): string {\n return path\n .replace(/\\\\/g, \"/\") // Convert backslashes to forward slashes\n .replace(/\\/+/g, \"/\") // Replace multiple slashes with single slash\n .replace(/^\\/|\\/$/g, \"\"); // Remove leading and trailing slashes\n}\n\n/**\n * Join path segments safely with proper separators\n * @param segments Path segments to join\n * @returns Joined path\n * @public\n *\n * @example\n * ```typescript\n * import { joinPath } from '@kumix/storage/helpers';\n *\n * const path1 = joinPath('folder', 'subfolder', 'file.txt');\n * // Returns: \"folder/subfolder/file.txt\"\n *\n * const path2 = joinPath('/folder/', '/subfolder/', '/file.txt/');\n * // Returns: \"folder/subfolder/file.txt\"\n *\n * const path3 = joinPath('', 'folder', '', 'file.txt');\n * // Returns: \"folder/file.txt\"\n * ```\n */\nexport function joinPath(...segments: string[]): string {\n return segments\n .filter((segment) => segment && segment.trim() !== \"\")\n .map((segment) => segment.replace(/^\\/+|\\/+$/g, \"\"))\n .filter((segment) => segment !== \"\")\n .join(\"/\");\n}\n\n/**\n * Get parent directory path from a file key\n * @param key File key or path\n * @returns Parent directory path\n * @public\n *\n * @example\n * ```typescript\n * import { getParentPath } from '@kumix/storage/helpers';\n *\n * const parent1 = getParentPath('folder/subfolder/file.txt');\n * // Returns: \"folder/subfolder\"\n *\n * const parent2 = getParentPath('file.txt');\n * // Returns: \"\"\n *\n * const parent3 = getParentPath('folder/file.txt');\n * // Returns: \"folder\"\n * ```\n */\nexport function getParentPath(key: string): string {\n const normalizedKey = normalizePath(key);\n const lastSlashIndex = normalizedKey.lastIndexOf(\"/\");\n return lastSlashIndex === -1 ? \"\" : normalizedKey.substring(0, lastSlashIndex);\n}\n\n/**\n * Get filename from a file key or path\n * @param key File key or path\n * @returns Filename without path\n * @public\n *\n * @example\n * ```typescript\n * import { getFileName } from '@kumix/storage/helpers';\n *\n * const name1 = getFileName('folder/subfolder/document.pdf');\n * // Returns: \"document.pdf\"\n *\n * const name2 = getFileName('file.txt');\n * // Returns: \"file.txt\"\n *\n * const name3 = getFileName('folder/subfolder/');\n * // Returns: \"\"\n * ```\n */\nexport function getFileName(key: string): string {\n const normalizedKey = normalizePath(key);\n const lastSlashIndex = normalizedKey.lastIndexOf(\"/\");\n return lastSlashIndex === -1 ? normalizedKey : normalizedKey.substring(lastSlashIndex + 1);\n}\n\n/**\n * Convert base64 string to Buffer\n * @param base64 Base64 encoded string\n * @returns Buffer containing the decoded data\n * @public\n *\n * @example\n * ```typescript\n * import { base64ToBuffer } from '@kumix/storage/helpers';\n *\n * const base64 = 'SGVsbG8gV29ybGQ='; // \"Hello World\" in base64\n * const buffer = base64ToBuffer(base64);\n * console.log(buffer.toString()); // \"Hello World\"\n *\n * // For file uploads from base64 data\n * const imageBase64 = 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD...';\n * const cleanBase64 = imageBase64.split(',')[1]; // Remove data URL prefix\n * const imageBuffer = base64ToBuffer(cleanBase64);\n * await storage.uploadFile('images/photo.jpg', imageBuffer);\n * ```\n */\nexport function base64ToBuffer(base64: string): Uint8Array {\n const cleanBase64 = base64.includes(\",\") ? base64.split(\",\")[1] : base64;\n\n if (typeof Buffer !== \"undefined\") {\n return Buffer.from(cleanBase64, \"base64\");\n }\n\n const binary = atob(cleanBase64);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n return bytes;\n}\n\n/**\n * Convert Buffer to base64 string\n * @param buffer Buffer to encode\n * @param includeDataUrl Whether to include data URL prefix\n * @param mimeType MIME type for data URL (required if includeDataUrl is true)\n * @returns Base64 encoded string\n * @public\n *\n * @example\n * ```typescript\n * import { bufferToBase64 } from '@kumix/storage/helpers';\n *\n * const buffer = Buffer.from('Hello World');\n * const base64 = bufferToBase64(buffer);\n * // Returns: \"SGVsbG8gV29ybGQ=\"\n *\n * // With data URL for images\n * const imageBuffer = await storage.downloadFile('images/photo.jpg');\n * const dataUrl = bufferToBase64(imageBuffer.content!, true, 'image/jpeg');\n * // Returns: \"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD...\"\n *\n * // Use in HTML img tag\n * document.querySelector('img').src = dataUrl;\n * ```\n */\nexport function bufferToBase64(\n buffer: Uint8Array,\n includeDataUrl: boolean = false,\n mimeType?: string,\n): string {\n let base64: string;\n\n if (typeof Buffer !== \"undefined\" && buffer instanceof Buffer) {\n base64 = buffer.toString(\"base64\");\n } else {\n let binary = \"\";\n for (let i = 0; i < buffer.length; i++) {\n binary += String.fromCharCode(buffer[i]);\n }\n base64 = btoa(binary);\n }\n\n if (includeDataUrl) {\n if (!mimeType) {\n throw new Error(\"MIME type is required when includeDataUrl is true\");\n }\n return `data:${mimeType};base64,${base64}`;\n }\n\n return base64;\n}\n\n/**\n * Generate file hash/checksum for integrity verification\n * @param content File content as Buffer or string\n * @param algorithm Hash algorithm to use\n * @returns Hash string\n * @public\n *\n * @example\n * ```typescript\n * import { generateFileHash } from '@kumix/storage/helpers';\n *\n * const content = Buffer.from('Hello World');\n *\n * const md5Hash = generateFileHash(content, 'md5');\n * // Returns: \"b10a8db164e0754105b7a99be72e3fe5\"\n *\n * const sha256Hash = generateFileHash(content, 'sha256');\n * // Returns: \"a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e\"\n *\n * // For file integrity checking\n * const fileBuffer = await storage.downloadFile('document.pdf');\n * const currentHash = generateFileHash(fileBuffer.content!, 'sha256');\n * const expectedHash = 'a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e';\n *\n * if (currentHash === expectedHash) {\n * console.log('File integrity verified');\n * } else {\n * console.log('File may be corrupted');\n * }\n * ```\n */\nexport async function generateFileHash(\n content: Uint8Array | string,\n algorithm: \"md5\" | \"sha1\" | \"sha256\" = \"sha256\",\n): Promise<string> {\n // md5/sha1 are intentionally not implemented via Web Crypto (no MD5 in\n // subtle.digest, and SHA-1 is widely available but discouraged). When the\n // caller asks for one of those, fall back to node:crypto. For the default\n // (sha256) and sha1 we use Web Crypto when available so this helper works\n // cross-runtime.\n if (algorithm !== \"md5\" && typeof crypto !== \"undefined\" && crypto.subtle) {\n const rawData = typeof content === \"string\" ? new TextEncoder().encode(content) : content;\n const hashBuffer = await crypto.subtle.digest(\n algorithm === \"sha1\" ? \"SHA-1\" : \"SHA-256\",\n // biome-ignore lint/suspicious/noExplicitAny: BufferLike vs ArrayBufferView variance across runtimes\n rawData as any,\n );\n return Array.from(new Uint8Array(hashBuffer))\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\");\n }\n\n const nodeCrypto = await import(\"node:crypto\");\n const hash = nodeCrypto.createHash(algorithm);\n // biome-ignore lint/suspicious/noExplicitAny: BufferLike vs string union variance across runtimes\n hash.update(content as any);\n return hash.digest(\"hex\");\n}\n\n/**\n * Generate multiple unique keys at once for batch operations\n * @param fileNames Array of file names\n * @param prefix Optional prefix for all keys\n * @returns Array of unique keys\n * @public\n *\n * @example\n * ```typescript\n * import { generateBatchKeys } from '@kumix/storage/helpers';\n *\n * const fileNames = ['document1.pdf', 'document2.pdf', 'image.jpg'];\n * const keys = generateBatchKeys(fileNames, 'uploads');\n * // Returns: [\n * // \"uploads/document1-1703123456789-abc123.pdf\",\n * // \"uploads/document2-1703123456790-def456.pdf\",\n * // \"uploads/image-1703123456791-ghi789.jpg\"\n * // ]\n *\n * // Use for batch uploads\n * const files = [file1, file2, file3];\n * const keys = generateBatchKeys(files.map(f => f.name), 'batch-upload');\n *\n * for (let i = 0; i < files.length; i++) {\n * await storage.uploadFile(keys[i], files[i]);\n * }\n * ```\n */\nexport function generateBatchKeys(fileNames: string[], prefix?: string): string[] {\n return fileNames.map((fileName) => generateFileKey(fileName, prefix));\n}\n\n/**\n * Validate multiple files at once with consistent options\n * @param files Array of File objects to validate\n * @param options Validation options\n * @returns Array of validation results\n * @public\n *\n * @example\n * ```typescript\n * import { validateBatchFiles } from '@kumix/storage/helpers';\n *\n * const files = [file1, file2, file3]; // File objects from input\n * const options = {\n * maxSize: 5 * 1024 * 1024, // 5MB\n * allowedTypes: ['image/*', 'application/pdf'],\n * maxFiles: 10\n * };\n *\n * const results = validateBatchFiles(files, options);\n * // Returns: [\n * // { valid: true, fileName: \"image.jpg\" },\n * // { valid: false, fileName: \"large.pdf\", errors: [\"File size exceeds limit\"] },\n * // { valid: true, fileName: \"document.pdf\" }\n * // ]\n *\n * const validFiles = files.filter((_, index) => results[index].valid);\n * const invalidFiles = files.filter((_, index) => !results[index].valid);\n * ```\n */\nexport function validateBatchFiles(\n files: File[],\n options: {\n maxSize?: number;\n allowedTypes?: string[];\n maxFiles?: number;\n },\n): Array<{ valid: boolean; fileName: string; errors?: string[] }> {\n const results: Array<{\n valid: boolean;\n fileName: string;\n errors?: string[];\n }> = [];\n\n // Check max files limit\n if (options.maxFiles && files.length > options.maxFiles) {\n return files.map((file) => ({\n valid: false,\n fileName: file.name,\n errors: [`Maximum ${options.maxFiles} files allowed, but ${files.length} files provided`],\n }));\n }\n\n for (const file of files) {\n const errors: string[] = [];\n\n // Validate file size\n if (options.maxSize) {\n const sizeResult = validateFileSize(file.size, options.maxSize);\n if (!sizeResult.valid && sizeResult.error) {\n errors.push(sizeResult.error);\n }\n }\n\n // Validate file type\n if (options.allowedTypes) {\n const typeResult = validateFileType(file.name, options.allowedTypes);\n if (!typeResult.valid && typeResult.error) {\n errors.push(typeResult.error);\n }\n }\n\n results.push({\n valid: errors.length === 0,\n fileName: file.name,\n errors: errors.length > 0 ? errors : undefined,\n });\n }\n\n return results;\n}\n\n/**\n * Detect file type from content using magic numbers/file signatures\n * @param buffer File content buffer\n * @returns Detected MIME type or 'application/octet-stream' if unknown\n * @public\n *\n * @example\n * ```typescript\n * import { detectFileTypeFromContent } from '@kumix/storage/helpers';\n *\n * // Read file content\n * const fileBuffer = await storage.downloadFile('unknown-file');\n * const detectedType = detectFileTypeFromContent(fileBuffer.content!);\n *\n * console.log(detectedType);\n * // Returns: \"image/jpeg\" for JPEG files\n * // Returns: \"application/pdf\" for PDF files\n * // Returns: \"image/png\" for PNG files\n * // Returns: \"application/octet-stream\" for unknown types\n *\n * // Verify file type matches extension\n * const fileName = 'document.pdf';\n * const expectedType = getMimeType(fileName);\n * const actualType = detectFileTypeFromContent(fileBuffer.content!);\n *\n * if (expectedType !== actualType) {\n * console.warn('File extension does not match content type');\n * }\n * ```\n */\nexport function detectFileTypeFromContent(buffer: Uint8Array): string {\n if (buffer.length === 0) {\n return \"application/octet-stream\";\n }\n\n // Check magic numbers/file signatures\n const header = buffer.subarray(0, 16);\n\n // PDF\n if (header.subarray(0, 4).reduce((s, b) => s + String.fromCharCode(b), \"\") === \"%PDF\") {\n return \"application/pdf\";\n }\n\n // JPEG\n if (header[0] === 0xff && header[1] === 0xd8 && header[2] === 0xff) {\n return \"image/jpeg\";\n }\n\n // PNG\n const pngSig = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);\n if (header.length >= 8 && header.slice(0, 8).every((b, i) => b === pngSig[i])) {\n return \"image/png\";\n }\n\n // GIF\n const gifHead = header.subarray(0, 6).reduce((s, b) => s + String.fromCharCode(b), \"\");\n if (gifHead === \"GIF87a\" || gifHead === \"GIF89a\") {\n return \"image/gif\";\n }\n\n // WebP\n if (\n header.subarray(0, 4).reduce((s, b) => s + String.fromCharCode(b), \"\") === \"RIFF\" &&\n header.subarray(8, 12).reduce((s, b) => s + String.fromCharCode(b), \"\") === \"WEBP\"\n ) {\n return \"image/webp\";\n }\n\n // ZIP (includes DOCX, XLSX, etc.)\n if (header[0] === 0x50 && header[1] === 0x4b && (header[2] === 0x03 || header[2] === 0x05)) {\n const zipContent = new TextDecoder().decode(buffer.slice(0, Math.min(buffer.length, 1000)));\n if (zipContent.includes(\"word/\"))\n return \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\";\n if (zipContent.includes(\"xl/\"))\n return \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\";\n if (zipContent.includes(\"ppt/\"))\n return \"application/vnd.openxmlformats-officedocument.presentationml.presentation\";\n return \"application/zip\";\n }\n\n // MP4\n if (new TextDecoder().decode(header.subarray(4, 8)) === \"ftyp\") {\n return \"video/mp4\";\n }\n\n // MP3: MPEG audio frame sync is the high 11 bits = 0xFFE. This is `0xFF`\n // for byte 0 and the top 3 bits of byte 1 (mask 0xE0), so the check below\n // is the standard MP3 sync test.\n if (\n (header[0] === 0xff && (header[1] & 0xe0) === 0xe0) ||\n new TextDecoder().decode(header.subarray(0, 3)) === \"ID3\"\n ) {\n return \"audio/mpeg\";\n }\n\n // Text files (check if all bytes are printable ASCII or UTF-8)\n let isText = true;\n const sampleSize = Math.min(buffer.length, 512);\n for (let i = 0; i < sampleSize; i++) {\n const byte = buffer[i];\n if (byte === 0 || (byte < 32 && byte !== 9 && byte !== 10 && byte !== 13)) {\n isText = false;\n break;\n }\n }\n\n if (isText) {\n return \"text/plain\";\n }\n\n return \"application/octet-stream\";\n}\n"],"mappings":"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"}
|
package/dist/s3.js
CHANGED
|
@@ -1,12 +1,30 @@
|
|
|
1
1
|
import { s as loadS3Config } from "./config-By5ib98s.js";
|
|
2
2
|
import { buildPublicUrl, getMimeType } from "./helpers.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
//#region src/operations/s3-sdk.ts
|
|
4
|
+
let s3SdkPromise;
|
|
5
|
+
let presignerPromise;
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Dynamically import `@aws-sdk/client-s3`, throwing a clear error when the
|
|
8
|
+
* optional peer dependency is not installed.
|
|
9
9
|
*/
|
|
10
|
+
async function loadS3Sdk() {
|
|
11
|
+
if (!s3SdkPromise) s3SdkPromise = import("@aws-sdk/client-s3").catch(() => {
|
|
12
|
+
throw new Error("@aws-sdk/client-s3 is not available. Install it to use the S3 storage provider.");
|
|
13
|
+
});
|
|
14
|
+
return s3SdkPromise;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Dynamically import `@aws-sdk/s3-request-presigner`, throwing a clear error
|
|
18
|
+
* when the optional peer dependency is not installed.
|
|
19
|
+
*/
|
|
20
|
+
async function loadPresigner() {
|
|
21
|
+
if (!presignerPromise) presignerPromise = import("@aws-sdk/s3-request-presigner").catch(() => {
|
|
22
|
+
throw new Error("@aws-sdk/s3-request-presigner is not available. Install it to generate presigned URLs.");
|
|
23
|
+
});
|
|
24
|
+
return presignerPromise;
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region src/operations/file-operations.ts
|
|
10
28
|
/**
|
|
11
29
|
* S3 file operations implementation
|
|
12
30
|
* Handles all file-related operations for S3-compatible storage providers
|
|
@@ -21,6 +39,7 @@ var FileOperations = class {
|
|
|
21
39
|
}
|
|
22
40
|
async upload(options) {
|
|
23
41
|
try {
|
|
42
|
+
const { PutObjectCommand } = await loadS3Sdk();
|
|
24
43
|
const contentType = options.contentType || getMimeType(options.key);
|
|
25
44
|
const command = new PutObjectCommand({
|
|
26
45
|
Bucket: this.config.bucket,
|
|
@@ -52,6 +71,7 @@ var FileOperations = class {
|
|
|
52
71
|
}
|
|
53
72
|
async download(options) {
|
|
54
73
|
try {
|
|
74
|
+
const { GetObjectCommand } = await loadS3Sdk();
|
|
55
75
|
const command = new GetObjectCommand({
|
|
56
76
|
Bucket: this.config.bucket,
|
|
57
77
|
Key: options.key,
|
|
@@ -89,6 +109,7 @@ var FileOperations = class {
|
|
|
89
109
|
}
|
|
90
110
|
async delete(options) {
|
|
91
111
|
try {
|
|
112
|
+
const { DeleteObjectCommand } = await loadS3Sdk();
|
|
92
113
|
const command = new DeleteObjectCommand({
|
|
93
114
|
Bucket: this.config.bucket,
|
|
94
115
|
Key: options.key
|
|
@@ -104,6 +125,7 @@ var FileOperations = class {
|
|
|
104
125
|
}
|
|
105
126
|
async batchDelete(options) {
|
|
106
127
|
try {
|
|
128
|
+
const { DeleteObjectsCommand } = await loadS3Sdk();
|
|
107
129
|
const command = new DeleteObjectsCommand({
|
|
108
130
|
Bucket: this.config.bucket,
|
|
109
131
|
Delete: {
|
|
@@ -134,6 +156,7 @@ var FileOperations = class {
|
|
|
134
156
|
}
|
|
135
157
|
async list(options = {}) {
|
|
136
158
|
try {
|
|
159
|
+
const { ListObjectsV2Command } = await loadS3Sdk();
|
|
137
160
|
const command = new ListObjectsV2Command({
|
|
138
161
|
Bucket: this.config.bucket,
|
|
139
162
|
Prefix: options.prefix,
|
|
@@ -164,6 +187,7 @@ var FileOperations = class {
|
|
|
164
187
|
}
|
|
165
188
|
async exists(key) {
|
|
166
189
|
try {
|
|
190
|
+
const { HeadObjectCommand } = await loadS3Sdk();
|
|
167
191
|
const command = new HeadObjectCommand({
|
|
168
192
|
Bucket: this.config.bucket,
|
|
169
193
|
Key: key
|
|
@@ -190,6 +214,7 @@ var FileOperations = class {
|
|
|
190
214
|
}
|
|
191
215
|
async copy(options) {
|
|
192
216
|
try {
|
|
217
|
+
const { CopyObjectCommand } = await loadS3Sdk();
|
|
193
218
|
const command = new CopyObjectCommand({
|
|
194
219
|
Bucket: this.config.bucket,
|
|
195
220
|
CopySource: `${this.config.bucket}/${options.sourceKey.split("/").map(encodeURIComponent).join("/")}`,
|
|
@@ -241,6 +266,8 @@ var FileOperations = class {
|
|
|
241
266
|
}
|
|
242
267
|
async getPresignedUrl(options) {
|
|
243
268
|
try {
|
|
269
|
+
const { GetObjectCommand, PutObjectCommand } = await loadS3Sdk();
|
|
270
|
+
const { getSignedUrl } = await loadPresigner();
|
|
244
271
|
const expiresIn = options.expiresIn || 3600;
|
|
245
272
|
let command;
|
|
246
273
|
if (options.operation === "get") command = new GetObjectCommand({
|
|
@@ -270,21 +297,19 @@ var FileOperations = class {
|
|
|
270
297
|
if (this.config.publicUrl) return buildPublicUrl(this.config.publicUrl, isCloudflare ? "" : this.config.bucket, key, isSupabase);
|
|
271
298
|
if (this.config.endpoint) return buildPublicUrl(this.config.endpoint, this.config.bucket, key, isSupabase);
|
|
272
299
|
if (this.config.provider === "minio") throw new Error("MinIO public URL could not be built: set `publicUrl` (or `endpoint`) on the S3 config to a publicly reachable base, since MinIO has no default public host.");
|
|
273
|
-
|
|
300
|
+
const encodedKey = key.split("/").map(encodeURIComponent).join("/");
|
|
301
|
+
return `https://${this.config.bucket}.s3.${this.config.region}.amazonaws.com/${encodedKey}`;
|
|
274
302
|
}
|
|
275
303
|
};
|
|
276
304
|
//#endregion
|
|
277
305
|
//#region src/operations/folder-operations.ts
|
|
278
|
-
/**
|
|
279
|
-
* S3 folder operations implementation
|
|
280
|
-
* Provides comprehensive folder management operations for S3-compatible storage providers
|
|
281
|
-
*/
|
|
282
306
|
/** S3 DeleteObjectsCommand allows at most 1000 keys per request */
|
|
283
307
|
const S3_DELETE_MAX_KEYS = 1e3;
|
|
284
308
|
/**
|
|
285
309
|
* List ALL objects under a prefix, following pagination (handles >1000 keys)
|
|
286
310
|
*/
|
|
287
311
|
async function listAllObjects(client, bucket, prefix) {
|
|
312
|
+
const { ListObjectsV2Command } = await loadS3Sdk();
|
|
288
313
|
const objects = [];
|
|
289
314
|
let continuationToken;
|
|
290
315
|
do {
|
|
@@ -301,6 +326,7 @@ async function listAllObjects(client, bucket, prefix) {
|
|
|
301
326
|
}
|
|
302
327
|
/** Delete objects in batches of 1000 (S3 hard limit per DeleteObjectsCommand) */
|
|
303
328
|
async function deleteObjectsBatched(client, bucket, keys) {
|
|
329
|
+
const { DeleteObjectsCommand } = await loadS3Sdk();
|
|
304
330
|
const deleted = [];
|
|
305
331
|
for (let i = 0; i < keys.length; i += S3_DELETE_MAX_KEYS) {
|
|
306
332
|
const command = new DeleteObjectsCommand({
|
|
@@ -331,6 +357,7 @@ var FolderOperations = class {
|
|
|
331
357
|
}
|
|
332
358
|
async createFolder(options) {
|
|
333
359
|
try {
|
|
360
|
+
const { PutObjectCommand } = await loadS3Sdk();
|
|
334
361
|
const folderPath = options.path.endsWith("/") ? options.path : `${options.path}/`;
|
|
335
362
|
const command = new PutObjectCommand({
|
|
336
363
|
Bucket: this.config.bucket,
|
|
@@ -352,6 +379,7 @@ var FolderOperations = class {
|
|
|
352
379
|
}
|
|
353
380
|
async deleteFolder(options) {
|
|
354
381
|
try {
|
|
382
|
+
const { DeleteObjectsCommand } = await loadS3Sdk();
|
|
355
383
|
const folderPath = options.path.endsWith("/") ? options.path : `${options.path}/`;
|
|
356
384
|
if (options.recursive) {
|
|
357
385
|
const allObjects = await listAllObjects(this.client, this.config.bucket, folderPath);
|
|
@@ -387,6 +415,7 @@ var FolderOperations = class {
|
|
|
387
415
|
}
|
|
388
416
|
async listFolders(options = {}) {
|
|
389
417
|
try {
|
|
418
|
+
const { ListObjectsV2Command } = await loadS3Sdk();
|
|
390
419
|
const command = new ListObjectsV2Command({
|
|
391
420
|
Bucket: this.config.bucket,
|
|
392
421
|
Prefix: options.prefix,
|
|
@@ -423,6 +452,7 @@ var FolderOperations = class {
|
|
|
423
452
|
}
|
|
424
453
|
async folderExists(path) {
|
|
425
454
|
try {
|
|
455
|
+
const { ListObjectsV2Command } = await loadS3Sdk();
|
|
426
456
|
const folderPath = path.endsWith("/") ? path : `${path}/`;
|
|
427
457
|
const command = new ListObjectsV2Command({
|
|
428
458
|
Bucket: this.config.bucket,
|
|
@@ -447,6 +477,7 @@ var FolderOperations = class {
|
|
|
447
477
|
}
|
|
448
478
|
async renameFolder(options) {
|
|
449
479
|
try {
|
|
480
|
+
const { CopyObjectCommand } = await loadS3Sdk();
|
|
450
481
|
const oldPath = options.oldPath.endsWith("/") ? options.oldPath : `${options.oldPath}/`;
|
|
451
482
|
const newPath = options.newPath.endsWith("/") ? options.newPath : `${options.newPath}/`;
|
|
452
483
|
const allObjects = await listAllObjects(this.client, this.config.bucket, oldPath);
|
|
@@ -481,6 +512,8 @@ var FolderOperations = class {
|
|
|
481
512
|
}
|
|
482
513
|
async copyFolder(options) {
|
|
483
514
|
try {
|
|
515
|
+
const { CopyObjectCommand } = await loadS3Sdk();
|
|
516
|
+
const recursive = options.recursive ?? true;
|
|
484
517
|
const sourcePath = options.sourcePath.endsWith("/") ? options.sourcePath : `${options.sourcePath}/`;
|
|
485
518
|
const destPath = options.destinationPath.endsWith("/") ? options.destinationPath : `${options.destinationPath}/`;
|
|
486
519
|
const allObjects = await listAllObjects(this.client, this.config.bucket, sourcePath);
|
|
@@ -491,7 +524,7 @@ var FolderOperations = class {
|
|
|
491
524
|
const copiedFiles = [];
|
|
492
525
|
for (const obj of allObjects) {
|
|
493
526
|
const sourceKey = obj.Key;
|
|
494
|
-
if (!
|
|
527
|
+
if (!recursive) {
|
|
495
528
|
if (sourceKey.slice(sourcePath.length).includes("/")) continue;
|
|
496
529
|
}
|
|
497
530
|
const destKey = destPath + sourceKey.slice(sourcePath.length);
|
|
@@ -518,84 +551,105 @@ var FolderOperations = class {
|
|
|
518
551
|
//#endregion
|
|
519
552
|
//#region src/providers/s3.ts
|
|
520
553
|
/**
|
|
521
|
-
* S3-compatible storage provider implementation
|
|
522
|
-
* Provides cloud storage operations using AWS S3 SDK for S3-compatible services
|
|
523
|
-
*/
|
|
524
|
-
/**
|
|
525
554
|
* S3-compatible storage provider
|
|
526
555
|
* Implements StorageInterface for AWS S3 and S3-compatible services
|
|
527
556
|
* @internal
|
|
528
557
|
*/
|
|
529
558
|
var S3Provider = class {
|
|
530
|
-
client;
|
|
531
559
|
config;
|
|
532
|
-
|
|
533
|
-
|
|
560
|
+
_client = null;
|
|
561
|
+
_fileOps = null;
|
|
562
|
+
_folderOps = null;
|
|
534
563
|
constructor(config) {
|
|
535
564
|
this.config = config;
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Lazily construct the S3 client and operation helpers. The `@aws-sdk/client-s3`
|
|
568
|
+
* dependency is imported dynamically so the optional peer isn't required at
|
|
569
|
+
* module load — consumers using only Cloudinary can import the package without
|
|
570
|
+
* installing it.
|
|
571
|
+
*/
|
|
572
|
+
async getFileOps() {
|
|
573
|
+
if (!this._fileOps) {
|
|
574
|
+
const client = await this.getClient();
|
|
575
|
+
this._fileOps = new FileOperations(client, this.config);
|
|
576
|
+
}
|
|
577
|
+
return this._fileOps;
|
|
578
|
+
}
|
|
579
|
+
async getFolderOps() {
|
|
580
|
+
if (!this._folderOps) {
|
|
581
|
+
const client = await this.getClient();
|
|
582
|
+
this._folderOps = new FolderOperations(client, this.config);
|
|
583
|
+
}
|
|
584
|
+
return this._folderOps;
|
|
585
|
+
}
|
|
586
|
+
async getClient() {
|
|
587
|
+
if (!this._client) {
|
|
588
|
+
const { S3Client } = await loadS3Sdk();
|
|
589
|
+
this._client = new S3Client({
|
|
590
|
+
region: this.config.region,
|
|
591
|
+
credentials: {
|
|
592
|
+
accessKeyId: this.config.accessKeyId,
|
|
593
|
+
secretAccessKey: this.config.secretAccessKey,
|
|
594
|
+
...this.config.sessionToken ? { sessionToken: this.config.sessionToken } : {}
|
|
595
|
+
},
|
|
596
|
+
endpoint: this.config.endpoint,
|
|
597
|
+
forcePathStyle: this.config.forcePathStyle ?? false
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
return this._client;
|
|
548
601
|
}
|
|
549
602
|
async upload(options) {
|
|
550
|
-
return this.
|
|
603
|
+
return (await this.getFileOps()).upload(options);
|
|
551
604
|
}
|
|
552
605
|
async download(options) {
|
|
553
|
-
return this.
|
|
606
|
+
return (await this.getFileOps()).download(options);
|
|
554
607
|
}
|
|
555
608
|
async delete(options) {
|
|
556
|
-
return this.
|
|
609
|
+
return (await this.getFileOps()).delete(options);
|
|
557
610
|
}
|
|
558
611
|
async batchDelete(options) {
|
|
559
|
-
return this.
|
|
612
|
+
return (await this.getFileOps()).batchDelete(options);
|
|
560
613
|
}
|
|
561
614
|
async list(options) {
|
|
562
|
-
return this.
|
|
615
|
+
return (await this.getFileOps()).list(options);
|
|
563
616
|
}
|
|
564
617
|
async exists(key) {
|
|
565
|
-
return this.
|
|
618
|
+
return (await this.getFileOps()).exists(key);
|
|
566
619
|
}
|
|
567
620
|
async copy(options) {
|
|
568
|
-
return this.
|
|
621
|
+
return (await this.getFileOps()).copy(options);
|
|
569
622
|
}
|
|
570
623
|
async move(options) {
|
|
571
|
-
return this.
|
|
624
|
+
return (await this.getFileOps()).move(options);
|
|
572
625
|
}
|
|
573
626
|
async duplicate(options) {
|
|
574
|
-
return this.
|
|
627
|
+
return (await this.getFileOps()).duplicate(options);
|
|
575
628
|
}
|
|
576
629
|
async getPresignedUrl(options) {
|
|
577
|
-
return this.
|
|
630
|
+
return (await this.getFileOps()).getPresignedUrl(options);
|
|
578
631
|
}
|
|
579
632
|
getPublicUrl(key) {
|
|
580
|
-
return this.
|
|
633
|
+
if (this._fileOps) return this._fileOps.getPublicUrl(key);
|
|
634
|
+
return new FileOperations(null, this.config).getPublicUrl(key);
|
|
581
635
|
}
|
|
582
636
|
async createFolder(options) {
|
|
583
|
-
return this.
|
|
637
|
+
return (await this.getFolderOps()).createFolder(options);
|
|
584
638
|
}
|
|
585
639
|
async deleteFolder(options) {
|
|
586
|
-
return this.
|
|
640
|
+
return (await this.getFolderOps()).deleteFolder(options);
|
|
587
641
|
}
|
|
588
642
|
async listFolders(options) {
|
|
589
|
-
return this.
|
|
643
|
+
return (await this.getFolderOps()).listFolders(options);
|
|
590
644
|
}
|
|
591
645
|
async folderExists(path) {
|
|
592
|
-
return this.
|
|
646
|
+
return (await this.getFolderOps()).folderExists(path);
|
|
593
647
|
}
|
|
594
648
|
async renameFolder(options) {
|
|
595
|
-
return this.
|
|
649
|
+
return (await this.getFolderOps()).renameFolder(options);
|
|
596
650
|
}
|
|
597
651
|
async copyFolder(options) {
|
|
598
|
-
return this.
|
|
652
|
+
return (await this.getFolderOps()).copyFolder(options);
|
|
599
653
|
}
|
|
600
654
|
};
|
|
601
655
|
//#endregion
|