/** * StorageService - Main orchestrator for storage operations * * Responsibilities: * - Manage storage adapters via AdapterRegistry * - Handle event emission via EventManager * - Provide high-level file operations (upload, download, delete, getSignedUrl) * - Automatic failover on adapter failures * - Input validation * - Correlation ID tracking * * @example * ```ts * const service = new StorageService({ * adapters: [cloudflareAdapter, supabaseAdapter], * eventHandlers: { * [STORAGE_EVENT_TYPE.FileUploaded]: async (payload) => { * await saveToDatabase(payload.metadata); * }, * }, * }); * * const result = await service.uploadFile({ * file: fileBuffer, * filename: 'avatar.jpg', * mimeType: 'image/jpeg', * category: FILE_CATEGORY.AVATAR, * entityType: ENTITY_TYPE.USER, * entityId: 'user-123', * }); * ``` */ import type { UploadParams, UploadResult, DownloadParams, DownloadResult, DeleteParams, FileDeleteResult, PresignedUrlOptions, PresignedUrlResult, StorageEventPayload, StorageQueueStatistics, FileMetadata, GenerateFileParams, GenerateFileToPathParams, GenerateFileToPathResult, UpdateFileParams, StorageWebhookAdapter, StorageWebhookPayload, StorageWebhookProcessingResult, STORAGE_WEBHOOK_EVENT_TYPE, StorageServiceConfig, StoragePublicUrlParams, StorageSignedUploadUrlParams, StorageReplaceFileParams, StorageCopyFileParams, StorageMoveFileParams, StorageUpdateBucketParams, StorageBucketInfo, StorageListBucketsResult, StorageSignedUploadUrlResult, StorageListFilesParams, StorageListFilesResult } from '@plyaz/types/storage'; import { STORAGE_EVENT_TYPE } from '@plyaz/types/storage'; import { AdapterRegistry } from './AdapterRegistry'; import { EventManager } from './EventManager'; import type { BaseStorageAdapter } from '../adapters/base/BaseStorageAdapter'; import { TemplateEngine } from '../templates/TemplateEngine'; import { ComplianceManager } from '../compliance/ComplianceManager'; import { WebhookManager } from '../webhooks/WebhookManager'; /** * StorageService - Main orchestrator for storage operations */ export declare class StorageService { private readonly adapterRegistry; private readonly eventManager; private readonly bucketRouter; private readonly logger?; private readonly maxFailoverAttempts; private readonly templateEngine; private readonly queue?; private readonly queueProcessor?; private readonly queueConfig?; private readonly complianceManager; private readonly pluginRegistry; private readonly webhookManager?; private readonly presetManager; constructor(config: StorageServiceConfig); /** * Initialize renderer registry with auto-registration based on config * Supports config.template.renderer.use to specify the renderer to use * @private */ private initializeRendererRegistry; /** * Register a specific renderer (called on-demand or at initialization) */ private registerRenderer; /** * Upload a file to storage * Automatically selects best adapter and handles failover * * If templateId is provided, generates PDF from template before upload: * 1. Renders template with templateData * 2. Converts HTML to PDF * 3. Uploads generated PDF * * Locale override: params.locale > service config > 'en' // eslint-disable-next-line complexity */ uploadFile(params: UploadParams): Promise; /** * Upload multiple files in parallel or via queue * * Efficiently uploads multiple files using concurrent batching or background queue processing. * Each file is validated, uploaded, and tracked independently with proper error handling. * * **Features:** * - Concurrent upload with configurable batch size (default: 5) * - Queue mode for background processing * - Continue on error to upload remaining files * - Individual progress tracking per file (via onProgress callback in params) * - Automatic chunked uploads for large files (>100MB) * - Reuses all existing features: validation, events, plugins, retry logic * * **Important Notes:** * - Neither Supabase nor Cloudflare R2 SDKs support native batch uploads * - Each file makes a separate API call to the storage provider * - Concurrency control prevents overwhelming the API and network * - Queue mode is non-blocking and returns immediately with operation IDs * * @param files - Array of upload parameters (one per file) * @param options - Upload configuration options * @param options.concurrency - Max concurrent uploads (default: 5, max recommended: 10) * @param options.useQueue - Enable background queue processing (default: false) * @param options.continueOnError - Continue uploading remaining files on error (default: false) * * @returns Array of upload results (empty if useQueue=true) * * @throws {StoragePackageError} If no files provided, queue not enabled (when useQueue=true), or upload fails (when continueOnError=false) * * @example * ```typescript * // Parallel upload (immediate mode) * const results = await storageService.uploadMultipleFiles([ * { file: buffer1, filename: 'photo1.jpg', category: FILE_CATEGORY.IMAGE, entityType: ENTITY_TYPE.USER, entityId: 'user-1' }, * { file: buffer2, filename: 'photo2.jpg', category: FILE_CATEGORY.IMAGE, entityType: ENTITY_TYPE.USER, entityId: 'user-1' } * ]); * * // With concurrency control (3 files at a time) * const results = await storageService.uploadMultipleFiles(files, { concurrency: 3 }); * * // Continue on error (partial success allowed) * const results = await storageService.uploadMultipleFiles(files, { * continueOnError: true // Uploads all files even if some fail * }); * * // Queue mode for background processing * await storageService.uploadMultipleFiles(files, { useQueue: true }); * // Returns immediately, uploads happen in background * // Monitor progress with getQueueStatistics() * ``` */ uploadMultipleFiles(files: UploadParams[], options?: { concurrency?: number; useQueue?: boolean; continueOnError?: boolean; }): Promise; /** * Upload file from template (generates document in specified format) * @private */ private uploadFromTemplate; /** * Generate document from template using the template engine * * @param params - Upload parameters including templateId and templateData * @param outputFormat - Target output format (pdf, excel, word, etc.) * @returns Generated document buffer with renderer info and locale * @throws StoragePackageError if renderer registry not configured * @internal */ private generateDocumentFromTemplate; /** * Get file extension and MIME type for output format * * @param outputFormat - Output format to get details for * @returns Extension and MIME type for the format * @internal */ private getFormatDetails; /** * Get compliance/retention policy info for template upload * * @param params - Upload parameters with optional category * @returns Retention policy if compliance manager configured and category provided * @internal */ private getTemplateComplianceInfo; /** * Build metadata object for template-generated uploads * * @param options - Metadata building options * @returns Metadata object with generation details and compliance info * @internal */ private buildTemplateMetadata; /** * Handle template generation errors with consistent error wrapping * * @param error - The caught error * @param templateId - Template ID for context * @param outputFormat - Output format for context * @throws StoragePackageError - Always throws * @internal */ private handleTemplateError; /** * Upload file to adapter (actual upload logic) * @private */ private uploadFileToAdapter; /** // eslint-disable-next-line complexity * Download a file from storage */ downloadFile(params: DownloadParams): Promise; /** // eslint-disable-next-line complexity * Delete a file from storage */ deleteFile(params: DeleteParams): Promise; /** * Restore a soft-deleted file * * @param fileId - ID of the file to restore * @returns Promise - Restored file metadata * * @example * ```typescript * const restored = await service.restoreFile('file-123'); * console.log('File restored:', restored.fileId); * ``` */ restoreFile(fileId: string): Promise; /** * Generate file to buffer (without uploading) * * Useful when you want to: * - Process the file further before uploading * - Send the file directly in an HTTP response * - Store the file elsewhere * - Preview/test file generation * * @param params - Generation parameters * @returns Buffer containing the generated file * * @example * ```typescript * // Generate PDF invoice to buffer * const buffer = await storage.generateFile({ * templateId: 'invoices/standard-invoice', * templateData: { customer: 'John Doe', amount: 1500 }, * outputFormat: 'pdf', * }); * * // Do something with the buffer (e.g., send via email, process further, etc.) * await emailService.sendAttachment(buffer, 'invoice.pdf'); * ``` */ generateFile(params: GenerateFileParams): Promise; /** * Generate file and save to local path (without uploading to storage adapter) * * Useful when you want to: * - Save generated files to a local directory for batch processing * - Generate files for a local file system * - Create files that will be uploaded via a different mechanism * * @param params - Generation parameters including output path * @returns Metadata about the generated file * * @example * ```typescript * // Generate and save invoice to local path * const result = await storage.generateFileToPath({ * templateId: 'invoices/standard-invoice', * templateData: { customer: 'John Doe', amount: 1500 }, * outputFormat: 'pdf', * outputPath: '/tmp/invoices/invoice-12345.pdf', * }); * // eslint-disable-next-line complexity * console.log(`File saved to: ${result.path}`); * console.log(`File size: ${result.size} bytes`); * ``` */ generateFileToPath(params: GenerateFileToPathParams): Promise; /** * Update/replace an existing file * * @param params - Update parameters * @returns Updated file metadata * * @example * ```typescript * // Update existing invoice with new data * const result = await storage.updateFile({ * fileId: 'existing-file-id', * file: newBuffer, // or * templateId: 'invoices/standard-invoice', // regenerate from template // eslint-disable-next-line complexity * templateData: { ...updatedData }, * reason: 'Updated invoice with corrected amount', * }); * ``` */ updateFile(params: UpdateFileParams): Promise; /** * Generate a presigned/signed URL for file access */ getSignedUrl(options: PresignedUrlOptions): Promise; /** * Get adapter registry health summary */ getHealthSummary(): ReturnType; /** * Get event manager statistics */ getEventStatistics(): ReturnType; /** * Manually trigger health check for all adapters */ checkAllAdaptersHealth(): Promise; /** * Delete buckets from storage adapters * Useful for cleanup in tests or migrations * * @param bucketNames - Array of bucket names to delete * @param adapterName - Optional specific adapter to use (defaults to all adapters) * @returns Results showing which buckets were deleted and which failed * * @example * ```typescript * // Delete test buckets from all adapters * const result = await storageService.deleteBuckets([ * 'compliance', * 'media-images', * 'test-bucket' * ]); * console.log(`Deleted: ${result.deleted.length}, Failed: ${result.failed.length}`); * * // Delete from specific adapter * const result = await storageService.deleteBuckets( * ['old-bucket'], * 'supabase-storage' * ); * ``` */ deleteBuckets(bucketNames: string[], adapterName?: string): Promise<{ deleted: string[]; failed: Array<{ bucket: string; error: string; adapter?: string; }>; }>; /** * Register a new event handler */ onEvent(eventType: STORAGE_EVENT_TYPE, handler: (payload: StorageEventPayload) => void | Promise): () => void; /** * Register an event listener (alias for onEvent) * * @param eventType - The event type to listen for * @param handler - The event handler callback * @returns Unsubscribe function * * @example * ```typescript * // Listen to compliance events * service.on(STORAGE_EVENT_TYPE.RetentionEnforced, (event) => { * console.log('File deletion blocked:', event.data); * }); * * // Listen to soft delete events * service.on(STORAGE_EVENT_TYPE.SoftDeleteProcessed, (event) => { * console.log('File soft deleted:', event.metadata?.fileId); * }); * ``` */ on(eventType: STORAGE_EVENT_TYPE, handler: (payload: StorageEventPayload) => void | Promise): () => void; /** * Unregister an event listener * * @param eventType - The event type * @param handler - The event handler to remove * * @example * ```typescript * const handler = (event) => console.log(event); * service.on(STORAGE_EVENT_TYPE.FileUploaded, handler); * * // Later, unregister the handler * service.off(STORAGE_EVENT_TYPE.FileUploaded, handler); * ``` */ off(eventType: STORAGE_EVENT_TYPE, handler: (payload: StorageEventPayload) => void | Promise): void; /** * Get template engine instance (for PDF generation and custom template operations) * * @returns TemplateEngine instance * * @example * ```typescript * const engine = service.getTemplateEngine(); * engine.setDefaultLocale('es'); * const result = await engine.render('invoices/standard', data); * ``` */ getTemplateEngine(): TemplateEngine; /** * Process a queued operation * @private */ private processQueuedOperation; /** * Queue bulk uploads for background processing * * @param files - Array of upload parameters with optional priority * @returns Promise that resolves when all operations are queued * * @example * ```typescript * await service.queueBulkUpload([ * { file: buffer1, filename: 'image1.jpg', category: FILE_CATEGORY.PostImage, entityType: ENTITY_TYPE.POST, entityId: '1' }, * { file: buffer2, filename: 'image2.jpg', category: FILE_CATEGORY.PostImage, entityType: ENTITY_TYPE.POST, entityId: '2', priority: 'high' }, * ]); * ``` */ queueBulkUpload(files: Array): Promise; /** * Queue document generation for background processing * * @param options - Document generation options * @returns Promise that resolves when operation is queued * * @example * ```typescript * await service.queueDocumentGeneration({ * templateId: 'invoices/standard-invoice', * data: invoiceData, * outputOptions: { * filename: 'invoice.pdf', * category: FILE_CATEGORY.InvoiceDocument, * entityType: ENTITY_TYPE.USER, * entityId: 'user-123', * }, * priority: 'high', * }); * ``` */ queueDocumentGeneration(options: { templateId: string; data: Record; outputOptions: Omit; priority?: 'high' | 'normal' | 'low'; }): Promise; /** * Start the queue processor */ startQueue(): void; /** * Stop the queue processor * Waits for active operations to complete */ stopQueue(): Promise; /** * Get queue statistics * * @returns Queue statistics including counts and status */ getQueueStatistics(): StorageQueueStatistics; /** * Get ComplianceManager instance * * @returns ComplianceManager (always available, enabled by default) */ getComplianceManager(): ComplianceManager; /** * Check if compliance is enabled * * @returns True if compliance manager is enabled */ isComplianceEnabled(): boolean; /** * Register a webhook adapter * Allows dynamic registration of webhook adapters after initialization * * @param adapter - Webhook adapter to register * * @example * ```typescript * const mediaWebhook = new MediaProcessingWebhook({ * secret: process.env.MEDIA_PROCESSING_SECRET!, * signatureMethod: STORAGE_SIGNATURE_METHOD.HmacSha256, * logger, * }); * * storageService.registerWebhookAdapter(mediaWebhook); * ``` */ registerWebhookAdapter(adapter: StorageWebhookAdapter): void; /** * Handle incoming webhook from external service * * @param providerName - Provider name (e.g., 'media-processing', 'aws-mediaconvert') * @param eventType - Storage event type * @param payload - Raw webhook payload from HTTP request * @returns Processing result * * @example * ```typescript * // In your Express/Fastify webhook endpoint: * app.post('/webhooks/:provider', async (req, res) => { * const result = await storageService.handleWebhook( * req.params.provider, * req.headers['x-event-type'] ?? 'transcode.complete', * { * method: req.method, * url: req.url, * headers: req.headers, * body: req.body, * rawBody: req.rawBody, // Important for signature verification * } * ); * * if (result.success) { * res.status(200).json({ received: true }); * } else { * res.status(400).json({ error: result.error?.message }); * } * }); * ``` */ handleWebhook(providerName: string, eventType: STORAGE_WEBHOOK_EVENT_TYPE | string, payload: StorageWebhookPayload): Promise; /** * Get webhook manager instance * Provides access to webhook manager for advanced operations * * @returns WebhookManager instance or undefined if not enabled */ getWebhookManager(): WebhookManager | undefined; /** * Check if webhooks are enabled * * @returns True if webhook manager is initialized */ isWebhooksEnabled(): boolean; /** * Register event handlers using cleaner camelCase syntax * Maps camelCase method names to enum-based event types using centralized mapping * @private */ private registerCleanHandlers; /** * List all buckets * Requires adapter support for bucket listing * * @param adapterName - Optional specific adapter to use * @returns Promise - List of all buckets * * @example * ```typescript * const buckets = await service.listBuckets('supabase-storage'); * console.log('Buckets:', buckets.buckets.map(b => b.name)); * ``` */ listBuckets(adapterName?: string): Promise; /** * Get bucket details * Requires adapter support for bucket info retrieval * * @param bucketName - Bucket name * @param adapterName - Optional specific adapter to use * @returns Promise - Bucket information * * @example * ```typescript * const bucket = await service.getBucket('media-images', 'supabase-storage'); * console.log('Bucket:', bucket.name, 'Public:', bucket.public); * ``` */ getBucket(bucketName: string, adapterName?: string): Promise; /** * Update bucket settings * Requires adapter support for bucket updates * * @param params - Update parameters * @param adapterName - Optional specific adapter to use * * @example // eslint-disable-next-line complexity * ```typescript * await service.updateBucket({ * bucketName: 'media-images', * public: true, * fileSizeLimit: 10 * 1024 * 1024, // 10MB * }, 'supabase-storage'); * ``` */ updateBucket(params: StorageUpdateBucketParams, adapterName?: string): Promise; /** * List files in a bucket * Requires adapter support for file listing * * @param params - List parameters * @param adapterName - Optional specific adapter to use * @returns Promise - List of files * * @example * ```typescript * const files = await service.listFiles({ * bucket: 'media-images', * prefix: 'user-123/', * limit: 50, * sortBy: 'createdAt', * sortOrder: 'desc', * }); * console.log('Files:', files.files.length, 'Has more:', files.hasMore); * ``` */ listFiles(params: StorageListFilesParams, adapterName?: string): Promise; /** * Move a file from one location to another * Requires adapter support for file moving * * SECURITY: This method runs the complete compliance stack: * - beforeDelete hooks (check if source file can be deleted) * - beforeUpload hooks (validate destination) * - Compliance checks (immutability, retention on source file) * - Event emissions (FILE_MOVED or FILE_MOVE_FAILED) * - afterUpload hooks (notifications for new location) * - afterDelete hooks (cleanup for old location) * * NOTE: Move is equivalent to copy + delete, so source file must be deletable * * @param params - Move parameters * @returns Promise - Metadata of moved file * * @example * ```typescript * const metadata = await service.moveFile({ * sourceBucket: 'temp', * sourcePath: 'upload-123.pdf', * destinationBucket: 'documents', * destinationPath: 'final/invoice-123.pdf', * }); * ``` */ moveFile(params: StorageMoveFileParams): Promise; /** * Copy a file to another location * Requires adapter support for file copying * * SECURITY: This method runs the complete compliance stack: * - beforeUpload hooks (validate destination file will be created) * - Event emissions (FILE_COPIED or FILE_COPY_FAILED) * - afterUpload hooks (notifications, CDN invalidation for new file) * * NOTE: Copy creates a new file, so only beforeUpload/afterUpload hooks are needed * Source file is NOT modified, so no beforeDelete/afterDelete hooks * * @param params - Copy parameters * @returns Promise - Metadata of copied file * * @example * ```typescript * const metadata = await service.copyFile({ * sourceBucket: 'documents', * sourcePath: 'template.pdf', * destinationBucket: 'documents', * destinationPath: 'user-123/copy.pdf', * }); * ``` */ copyFile(params: StorageCopyFileParams): Promise; /** * Replace an existing file * Requires adapter support for file replacement * * SECURITY: This method runs the complete compliance stack: * - beforeDelete hooks (compliance checks on old file) * - beforeUpload hooks (virus scan, validation on new file) * - Compliance checks (immutability, retention policies) * - Event emissions (FILE_REPLACED or FILE_REPLACE_FAILED) * - afterUpload hooks (notifications, CDN invalidation) * - afterDelete hooks (cleanup, audit logs) * * @param params - Replace parameters * @returns Promise - Metadata of replaced file * * @example * ```typescript * const metadata = await service.replaceFile({ * fileId: 'invoice-123.pdf', * file: newBuffer, * bucket: 'documents', * mimeType: 'application/pdf', * }); * ``` */ replaceFile(params: StorageReplaceFileParams): Promise; /** * Create a signed URL for direct upload * Requires adapter support for signed upload URLs * * @param params - Signed upload URL parameters * @returns Promise - Signed URL and metadata * * @example * ```typescript * const result = await service.createSignedUploadUrl({ // eslint-disable-next-line complexity * bucket: 'user-uploads', * path: 'user-123/photo.jpg', * expiresIn: 3600, * contentType: 'image/jpeg', * fileSizeLimit: 5 * 1024 * 1024, // 5MB * }); * // Client can now upload directly to result.url * ``` */ createSignedUploadUrl(params: StorageSignedUploadUrlParams): Promise; /** * Get public URL for a file * Requires adapter support for public URLs * * @param params - Public URL parameters * @returns Promise - Public URL // eslint-disable-next-line complexity * * @example * ```typescript * const url = await service.getPublicUrl({ * bucket: 'media-images', * path: 'posts/image-123.jpg', * }); * // Use the URL directly in HTML: * ``` */ getPublicUrl(params: StoragePublicUrlParams): Promise; /** * Helper: Get compliance retention policy info */ private getComplianceInfo; /** * Helper: Validate file overwrite compliance (for upload operations) */ private validateFileOverwriteCompliance; /** * Helper: Handle compliance check for delete operation */ private handleDeleteComplianceCheck; /** * Helper: Validate move destination compliance */ private validateMoveDestinationCompliance; /** * Helper: Validate copy destination compliance */ private validateCopyDestinationCompliance; /** * Helper: Handle replace file compliance violation */ private handleReplaceComplianceViolation; /** * Helper: Perform delete compliance check */ private performDeleteComplianceCheck; /** * Helper: Check signed URL compliance for upsert operations */ private checkSignedUrlCompliance; /** * Helper: Handle move file compliance violation */ private handleMoveComplianceViolation; /** * Helper: Handle signed URL compliance violation */ private handleSignedUrlComplianceViolation; /** * Cleanup resources */ destroy(): Promise; }