@kumix/storage 0.1.0 → 0.1.1

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.
@@ -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\nimport { lookup } from \"mime-types\";\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 extension = originalName.split(\".\").pop();\n const baseName = originalName.split(\".\").slice(0, -1).join(\".\");\n\n const fileName = `${baseName}-${timestamp}-${random}${extension ? `.${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 = lookup(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\n const k = 1024;\n const sizes = [\"Bytes\", \"KB\", \"MB\", \"GB\", \"TB\"];\n const i = Math.floor(Math.log(bytes) / Math.log(k));\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 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 .toLowerCase();\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 extension = fileName.split(\".\").pop();\n return extension ? `.${extension.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 // Remove both `.storage` subdomain and S3 path\n cleanBaseUrl = cleanBaseUrl\n .replace(/\\.storage/, \"\") // remove \".storage\"\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\" = \"md5\",\n): Promise<string> {\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: <>\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: <>\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\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 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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"}
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { A as DownloadOptions, B as PresignedUrlResult, C as RenameFolderResult, D as CopyResult, E as CopyOptions, F as ListOptions, G as FolderInfo, H as UploadResult, I as ListResult, J as StorageConfig, K as S3Config, L as MoveOptions, M as DuplicateOptions, N as DuplicateResult, O as DeleteOptions, P as ExistsResult, R as MoveResult, S as RenameFolderOptions, T as BatchDeleteResult, U as CloudinaryConfig, V as UploadOptions, W as FileInfo, Y as StorageProvider, _ as DeleteFolderOptions, a as hasStorageConfig, b as ListFoldersOptions, c as loadS3Config, d as validateS3EnvVars, f as StorageInterface, g as CreateFolderResult, h as CreateFolderOptions, i as getStorageProvider, j as DownloadResult, k as DeleteResult, l as loadStorageConfig, m as CopyFolderResult, n as EnvRecord, o as isStorageConfigured, p as CopyFolderOptions, q as S3ProviderType, r as getStorageEnvVars, s as loadCloudinaryConfig, t as ENV_VARS, u as validateCloudinaryEnvVars, v as DeleteFolderResult, w as BatchDeleteOptions, x as ListFoldersResult, y as FolderExistsResult, z as PresignedUrlOptions } from "./config-J6AdDh_c.js";
1
+ import { A as DownloadOptions, B as PresignedUrlResult, C as RenameFolderResult, D as CopyResult, E as CopyOptions, F as ListOptions, G as FolderInfo, H as UploadResult, I as ListResult, J as StorageConfig, K as S3Config, L as MoveOptions, M as DuplicateOptions, N as DuplicateResult, O as DeleteOptions, P as ExistsResult, R as MoveResult, S as RenameFolderOptions, T as BatchDeleteResult, U as CloudinaryConfig, V as UploadOptions, W as FileInfo, Y as StorageProvider, _ as DeleteFolderOptions, a as hasStorageConfig, b as ListFoldersOptions, c as loadS3Config, d as validateS3EnvVars, f as StorageInterface, g as CreateFolderResult, h as CreateFolderOptions, i as getStorageProvider, j as DownloadResult, k as DeleteResult, l as loadStorageConfig, m as CopyFolderResult, n as EnvRecord, o as isStorageConfigured, p as CopyFolderOptions, q as S3ProviderType, r as getStorageEnvVars, s as loadCloudinaryConfig, t as ENV_VARS, u as validateCloudinaryEnvVars, v as DeleteFolderResult, w as BatchDeleteOptions, x as ListFoldersResult, y as FolderExistsResult, z as PresignedUrlOptions } from "./config-DGzEH-Zd.js";
2
2
  export { type BatchDeleteOptions, type BatchDeleteResult, type CloudinaryConfig, type CopyFolderOptions, type CopyFolderResult, type CopyOptions, type CopyResult, type CreateFolderOptions, type CreateFolderResult, type DeleteFolderOptions, type DeleteFolderResult, type DeleteOptions, type DeleteResult, type DownloadOptions, type DownloadResult, type DuplicateOptions, type DuplicateResult, ENV_VARS, EnvRecord, type ExistsResult, type FileInfo, type FolderExistsResult, type FolderInfo, type ListFoldersOptions, type ListFoldersResult, type ListOptions, type ListResult, type MoveOptions, type MoveResult, type PresignedUrlOptions, type PresignedUrlResult, type RenameFolderOptions, type RenameFolderResult, type S3Config, type S3ProviderType, type StorageConfig, type StorageInterface, type StorageProvider, type UploadOptions, type UploadResult, getStorageEnvVars, getStorageProvider, hasStorageConfig, isStorageConfigured, loadCloudinaryConfig, loadS3Config, loadStorageConfig, validateCloudinaryEnvVars, validateS3EnvVars };
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as isStorageConfigured, c as loadStorageConfig, i as hasStorageConfig, l as validateCloudinaryEnvVars, n as getStorageEnvVars, o as loadCloudinaryConfig, r as getStorageProvider, s as loadS3Config, t as ENV_VARS, u as validateS3EnvVars } from "./config-t-NVJICl.js";
1
+ import { a as isStorageConfigured, c as loadStorageConfig, i as hasStorageConfig, l as validateCloudinaryEnvVars, n as getStorageEnvVars, o as loadCloudinaryConfig, r as getStorageProvider, s as loadS3Config, t as ENV_VARS, u as validateS3EnvVars } from "./config-By5ib98s.js";
2
2
  export { ENV_VARS, getStorageEnvVars, getStorageProvider, hasStorageConfig, isStorageConfigured, loadCloudinaryConfig, loadS3Config, loadStorageConfig, validateCloudinaryEnvVars, validateS3EnvVars };
package/dist/s3.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { A as DownloadOptions, B as PresignedUrlResult, C as RenameFolderResult, D as CopyResult, E as CopyOptions, F as ListOptions, H as UploadResult, I as ListResult, K as S3Config, L as MoveOptions, M as DuplicateOptions, N as DuplicateResult, O as DeleteOptions, P as ExistsResult, R as MoveResult, S as RenameFolderOptions, T as BatchDeleteResult, V as UploadOptions, _ as DeleteFolderOptions, b as ListFoldersOptions, f as StorageInterface, g as CreateFolderResult, h as CreateFolderOptions, j as DownloadResult, k as DeleteResult, m as CopyFolderResult, n as EnvRecord, p as CopyFolderOptions, v as DeleteFolderResult, w as BatchDeleteOptions, x as ListFoldersResult, y as FolderExistsResult, z as PresignedUrlOptions } from "./config-J6AdDh_c.js";
1
+ import { A as DownloadOptions, B as PresignedUrlResult, C as RenameFolderResult, D as CopyResult, E as CopyOptions, F as ListOptions, H as UploadResult, I as ListResult, K as S3Config, L as MoveOptions, M as DuplicateOptions, N as DuplicateResult, O as DeleteOptions, P as ExistsResult, R as MoveResult, S as RenameFolderOptions, T as BatchDeleteResult, V as UploadOptions, _ as DeleteFolderOptions, b as ListFoldersOptions, f as StorageInterface, g as CreateFolderResult, h as CreateFolderOptions, j as DownloadResult, k as DeleteResult, m as CopyFolderResult, n as EnvRecord, p as CopyFolderOptions, v as DeleteFolderResult, w as BatchDeleteOptions, x as ListFoldersResult, y as FolderExistsResult, z as PresignedUrlOptions } from "./config-DGzEH-Zd.js";
2
2
 
3
3
  //#region src/services/s3.d.ts
4
4
  /**
@@ -104,6 +104,7 @@ declare class S3Service implements StorageInterface {
104
104
  declare function createS3(): S3Service;
105
105
  declare function createS3(config: S3Config): S3Service;
106
106
  declare function createS3(config: S3Config, env: EnvRecord): S3Service;
107
+ declare function createS3(config: undefined, env: EnvRecord): S3Service;
107
108
  //#endregion
108
109
  export { type EnvRecord, S3Service, createS3 };
109
110
  //# sourceMappingURL=s3.d.ts.map
package/dist/s3.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"s3.d.ts","names":[],"sources":["../src/services/s3.ts","../src/s3.ts"],"mappings":"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"}
1
+ {"version":3,"file":"s3.d.ts","names":[],"sources":["../src/services/s3.ts","../src/s3.ts"],"mappings":"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"}
package/dist/s3.js CHANGED
@@ -1,4 +1,4 @@
1
- import { s as loadS3Config } from "./config-t-NVJICl.js";
1
+ import { s as loadS3Config } from "./config-By5ib98s.js";
2
2
  import { buildPublicUrl, getMimeType } from "./helpers.js";
3
3
  import { CopyObjectCommand, DeleteObjectCommand, DeleteObjectsCommand, GetObjectCommand, HeadObjectCommand, ListObjectsV2Command, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
4
4
  import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
@@ -30,7 +30,7 @@ var FileOperations = class {
30
30
  Metadata: options.metadata,
31
31
  CacheControl: options.cacheControl,
32
32
  ContentDisposition: options.contentDisposition,
33
- ACL: options.acl,
33
+ ...options.acl && (this.config.provider === "aws" || this.config.provider === "digitalocean") ? { ACL: options.acl } : {},
34
34
  Expires: options.expires
35
35
  });
36
36
  const result = await this.client.send(command);
@@ -118,7 +118,7 @@ var FileOperations = class {
118
118
  error: err.Message || "Unknown error"
119
119
  })) || [];
120
120
  return {
121
- success: true,
121
+ success: errors.length === 0,
122
122
  deleted,
123
123
  errors: errors.length > 0 ? errors : void 0
124
124
  };
@@ -192,7 +192,7 @@ var FileOperations = class {
192
192
  try {
193
193
  const command = new CopyObjectCommand({
194
194
  Bucket: this.config.bucket,
195
- CopySource: `${this.config.bucket}/${options.sourceKey}`,
195
+ CopySource: `${this.config.bucket}/${options.sourceKey.split("/").map(encodeURIComponent).join("/")}`,
196
196
  Key: options.destinationKey,
197
197
  Metadata: options.metadata,
198
198
  MetadataDirective: options.metadataDirective || "COPY"
@@ -269,6 +269,7 @@ var FileOperations = class {
269
269
  const isSupabase = this.config.provider === "supabase";
270
270
  if (this.config.publicUrl) return buildPublicUrl(this.config.publicUrl, isCloudflare ? "" : this.config.bucket, key, isSupabase);
271
271
  if (this.config.endpoint) return buildPublicUrl(this.config.endpoint, this.config.bucket, key, isSupabase);
272
+ 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.");
272
273
  return `https://${this.config.bucket}.s3.${this.config.region}.amazonaws.com/${key}`;
273
274
  }
274
275
  };
@@ -278,6 +279,44 @@ var FileOperations = class {
278
279
  * S3 folder operations implementation
279
280
  * Provides comprehensive folder management operations for S3-compatible storage providers
280
281
  */
282
+ /** S3 DeleteObjectsCommand allows at most 1000 keys per request */
283
+ const S3_DELETE_MAX_KEYS = 1e3;
284
+ /**
285
+ * List ALL objects under a prefix, following pagination (handles >1000 keys)
286
+ */
287
+ async function listAllObjects(client, bucket, prefix) {
288
+ const objects = [];
289
+ let continuationToken;
290
+ do {
291
+ const command = new ListObjectsV2Command({
292
+ Bucket: bucket,
293
+ Prefix: prefix,
294
+ ContinuationToken: continuationToken
295
+ });
296
+ const result = await client.send(command);
297
+ if (result.Contents) objects.push(...result.Contents);
298
+ continuationToken = result.IsTruncated ? result.NextContinuationToken : void 0;
299
+ } while (continuationToken);
300
+ return objects;
301
+ }
302
+ /** Delete objects in batches of 1000 (S3 hard limit per DeleteObjectsCommand) */
303
+ async function deleteObjectsBatched(client, bucket, keys) {
304
+ const deleted = [];
305
+ for (let i = 0; i < keys.length; i += S3_DELETE_MAX_KEYS) {
306
+ const command = new DeleteObjectsCommand({
307
+ Bucket: bucket,
308
+ Delete: {
309
+ Objects: keys.slice(i, i + S3_DELETE_MAX_KEYS).map((key) => ({ Key: key })),
310
+ Quiet: true
311
+ }
312
+ });
313
+ const result = await client.send(command);
314
+ if (result.Deleted) {
315
+ for (const obj of result.Deleted) if (obj.Key) deleted.push(obj.Key);
316
+ }
317
+ }
318
+ return deleted;
319
+ }
281
320
  /**
282
321
  * S3 folder operations implementation
283
322
  * Handles all folder-related operations for S3-compatible storage providers
@@ -315,25 +354,15 @@ var FolderOperations = class {
315
354
  try {
316
355
  const folderPath = options.path.endsWith("/") ? options.path : `${options.path}/`;
317
356
  if (options.recursive) {
318
- const listCommand = new ListObjectsV2Command({
319
- Bucket: this.config.bucket,
320
- Prefix: folderPath
321
- });
322
- const listResult = await this.client.send(listCommand);
323
- if (!listResult.Contents || listResult.Contents.length === 0) return {
357
+ const allObjects = await listAllObjects(this.client, this.config.bucket, folderPath);
358
+ if (allObjects.length === 0) return {
324
359
  success: true,
325
360
  deletedFiles: []
326
361
  };
327
- const deleteCommand = new DeleteObjectsCommand({
328
- Bucket: this.config.bucket,
329
- Delete: {
330
- Objects: listResult.Contents.map((obj) => ({ Key: obj.Key })),
331
- Quiet: false
332
- }
333
- });
362
+ const keys = allObjects.map((obj) => obj.Key).filter(Boolean);
334
363
  return {
335
364
  success: true,
336
- deletedFiles: (await this.client.send(deleteCommand)).Deleted?.map((obj) => obj.Key).filter(Boolean) || []
365
+ deletedFiles: await deleteObjectsBatched(this.client, this.config.bucket, keys)
337
366
  };
338
367
  } else {
339
368
  const deleteCommand = new DeleteObjectsCommand({
@@ -372,10 +401,7 @@ var FolderOperations = class {
372
401
  const path = cp.Prefix;
373
402
  return {
374
403
  name: path.split("/").filter(Boolean).pop() || "",
375
- path,
376
- size: 0,
377
- fileCount: 0,
378
- lastModified: /* @__PURE__ */ new Date()
404
+ path
379
405
  };
380
406
  }) || [],
381
407
  files: result.Contents?.map((obj) => ({
@@ -408,10 +434,7 @@ var FolderOperations = class {
408
434
  exists: true,
409
435
  folderInfo: {
410
436
  name: folderPath.split("/").filter(Boolean).pop() || "",
411
- path: folderPath,
412
- size: 0,
413
- fileCount: 0,
414
- lastModified: /* @__PURE__ */ new Date()
437
+ path: folderPath
415
438
  }
416
439
  };
417
440
  return { exists: false };
@@ -426,35 +449,25 @@ var FolderOperations = class {
426
449
  try {
427
450
  const oldPath = options.oldPath.endsWith("/") ? options.oldPath : `${options.oldPath}/`;
428
451
  const newPath = options.newPath.endsWith("/") ? options.newPath : `${options.newPath}/`;
429
- const listCommand = new ListObjectsV2Command({
430
- Bucket: this.config.bucket,
431
- Prefix: oldPath
432
- });
433
- const listResult = await this.client.send(listCommand);
434
- if (!listResult.Contents || listResult.Contents.length === 0) return {
452
+ const allObjects = await listAllObjects(this.client, this.config.bucket, oldPath);
453
+ if (allObjects.length === 0) return {
435
454
  success: true,
436
455
  movedFiles: []
437
456
  };
438
457
  const movedFiles = [];
439
- for (const obj of listResult.Contents) {
458
+ for (const obj of allObjects) {
440
459
  const oldKey = obj.Key;
441
- const newKey = oldKey.replace(oldPath, newPath);
460
+ const newKey = newPath + oldKey.slice(oldPath.length);
442
461
  const copyCommand = new CopyObjectCommand({
443
462
  Bucket: this.config.bucket,
444
463
  Key: newKey,
445
- CopySource: `${this.config.bucket}/${oldKey}`
464
+ CopySource: `${this.config.bucket}/${oldKey.split("/").map(encodeURIComponent).join("/")}`
446
465
  });
447
466
  await this.client.send(copyCommand);
448
467
  movedFiles.push(newKey);
449
468
  }
450
- const deleteCommand = new DeleteObjectsCommand({
451
- Bucket: this.config.bucket,
452
- Delete: {
453
- Objects: listResult.Contents.map((obj) => ({ Key: obj.Key })),
454
- Quiet: true
455
- }
456
- });
457
- await this.client.send(deleteCommand);
469
+ const oldKeys = allObjects.map((obj) => obj.Key).filter(Boolean);
470
+ await deleteObjectsBatched(this.client, this.config.bucket, oldKeys);
458
471
  return {
459
472
  success: true,
460
473
  movedFiles
@@ -470,23 +483,22 @@ var FolderOperations = class {
470
483
  try {
471
484
  const sourcePath = options.sourcePath.endsWith("/") ? options.sourcePath : `${options.sourcePath}/`;
472
485
  const destPath = options.destinationPath.endsWith("/") ? options.destinationPath : `${options.destinationPath}/`;
473
- const listCommand = new ListObjectsV2Command({
474
- Bucket: this.config.bucket,
475
- Prefix: sourcePath
476
- });
477
- const listResult = await this.client.send(listCommand);
478
- if (!listResult.Contents || listResult.Contents.length === 0) return {
486
+ const allObjects = await listAllObjects(this.client, this.config.bucket, sourcePath);
487
+ if (allObjects.length === 0) return {
479
488
  success: true,
480
489
  copiedFiles: []
481
490
  };
482
491
  const copiedFiles = [];
483
- for (const obj of listResult.Contents) {
492
+ for (const obj of allObjects) {
484
493
  const sourceKey = obj.Key;
485
- const destKey = sourceKey.replace(sourcePath, destPath);
494
+ if (!options.recursive) {
495
+ if (sourceKey.slice(sourcePath.length).includes("/")) continue;
496
+ }
497
+ const destKey = destPath + sourceKey.slice(sourcePath.length);
486
498
  const copyCommand = new CopyObjectCommand({
487
499
  Bucket: this.config.bucket,
488
500
  Key: destKey,
489
- CopySource: `${this.config.bucket}/${sourceKey}`
501
+ CopySource: `${this.config.bucket}/${sourceKey.split("/").map(encodeURIComponent).join("/")}`
490
502
  });
491
503
  await this.client.send(copyCommand);
492
504
  copiedFiles.push(destKey);
@@ -525,10 +537,11 @@ var S3Provider = class {
525
537
  region: config.region,
526
538
  credentials: {
527
539
  accessKeyId: config.accessKeyId,
528
- secretAccessKey: config.secretAccessKey
540
+ secretAccessKey: config.secretAccessKey,
541
+ ...config.sessionToken ? { sessionToken: config.sessionToken } : {}
529
542
  },
530
543
  endpoint: config.endpoint,
531
- forcePathStyle: config.forcePathStyle || false
544
+ forcePathStyle: config.forcePathStyle ?? false
532
545
  });
533
546
  this.fileOps = new FileOperations(this.client, this.config);
534
547
  this.folderOps = new FolderOperations(this.client, this.config);