/** * Media Storage Types * * Defines interfaces and types for the unified media storage system. * Supports cloud storage adapters via plugins. * * Storage Backends: * - AWS S3 / Cloudflare R2 / MinIO (via @nextlyhq/storage-s3) * - Vercel Blob (via @nextlyhq/storage-vercel-blob) */ interface UploadOptions { /** Original filename from user */ filename: string; /** MIME type (e.g., 'image/png', 'video/mp4') */ mimeType: string; /** Optional content type override */ contentType?: string; /** Optional folder/prefix for organizing uploads */ folder?: string; /** Collection slug this upload belongs to (for collection-specific storage) */ collection?: string; /** Optional Content-Disposition header value (e.g., 'attachment' for SVG security) */ contentDisposition?: "inline" | "attachment"; } interface UploadResult { /** Public URL to access the file */ url: string; /** Storage path/key (for deletion and metadata retrieval) */ path: string; } /** * Extended file metadata returned by getMetadata() * * Contains comprehensive information about an uploaded file, * including dimensions for images and creation timestamps. */ interface FileMetadata { /** Unique identifier (typically the storage path/key) */ id: string; /** Storage filename (may differ from original) */ filename: string; /** Original filename as uploaded by user */ originalFilename: string; /** MIME type (e.g., 'image/jpeg', 'application/pdf') */ mimeType: string; /** File size in bytes */ size: number; /** Public URL to access the file */ url: string; /** Thumbnail URL for images (if generated) */ thumbnailUrl?: string; /** Image width in pixels (for images only) */ width?: number; /** Image height in pixels (for images only) */ height?: number; /** ISO timestamp when file was uploaded */ createdAt: string; /** ISO timestamp when file was last modified */ updatedAt?: string; } interface ImageMetadata { width: number; height: number; format: string; size: number; } interface ProcessedImage { buffer: Buffer; metadata: ImageMetadata; } /** * Storage type identifier. * - "s3": AWS S3 or S3-compatible services (R2, MinIO, DigitalOcean Spaces) * - "vercel-blob": Vercel Blob Storage * - "local": Local disk storage (default for development) * - "uploadthing": Uploadthing cloud storage */ type StorageType = "s3" | "vercel-blob" | "local" | "uploadthing"; /** * Information about a storage adapter's capabilities. * Returned by adapter.getInfo() method. */ interface StorageAdapterInfo { /** Storage type identifier */ type: StorageType; /** Human-readable adapter name */ name: string; /** Whether this adapter supports signed URLs for private access */ supportsSignedUrls: boolean; /** Whether this adapter supports client-side (direct) uploads */ supportsClientUploads: boolean; } /** * Per-collection storage configuration. * Allows customizing storage behavior for specific upload collections. */ interface CollectionStorageConfig { /** Prefix/folder for this collection's uploads */ prefix?: string; /** Enable client-side uploads (for serverless platforms with body size limits) */ clientUploads?: boolean; /** Generate signed URLs for downloads (for private buckets) */ signedDownloads?: boolean; /** Signed URL expiry time in seconds (default: 3600) */ signedUrlExpiresIn?: number; } /** * Collection storage map - maps collection slugs to their config. * Used in storage plugin configuration. * * @example * ```typescript * { * media: true, // Use default config * 'private-docs': { * prefix: 'private/', * signedDownloads: true, * signedUrlExpiresIn: 900 * } * } * ``` */ type CollectionStorageMap = Record; /** * Base configuration for storage plugins. * Extended by specific adapter configs (S3StorageConfig, etc.) */ interface StoragePluginConfig { /** Enable/disable the plugin (default: true) */ enabled?: boolean; /** Collections to apply this storage adapter to */ collections: CollectionStorageMap; } /** * Storage plugin returned by adapter plugin functions. * These are processed during Nextly initialization. * * @example * ```typescript * // From @nextlyhq/storage-s3 * const plugin = s3Storage({ * bucket: 'my-bucket', * region: 'us-east-1', * collections: { media: true } * }); * // plugin implements StoragePlugin * ``` */ interface StoragePlugin { /** Plugin name for identification */ name: string; /** Storage type */ type: StorageType; /** Collections this plugin handles */ collections: CollectionStorageMap; /** The storage adapter instance */ adapter: IStorageAdapter; /** * Handler for generating client-side upload URLs. * Called when clientUploads is enabled for a collection. */ getClientUploadUrl?: (filename: string, mimeType: string, collection: string) => Promise; /** * Handler for generating signed download URLs. * Called when signedDownloads is enabled for a collection. */ getSignedDownloadUrl?: (path: string, expiresIn?: number) => Promise; } /** * Data returned for client-side (direct) uploads. * Contains pre-signed URL and headers for direct-to-storage uploads. * * @example * ```typescript * // Usage in frontend * const uploadData = await fetch('/api/nextly/storage/upload-url', { * method: 'POST', * body: JSON.stringify({ filename: 'photo.jpg', mimeType: 'image/jpeg', collection: 'media' }) * }).then(r => r.json()); * * // Direct upload to storage * await fetch(uploadData.uploadUrl, { * method: uploadData.method, * headers: uploadData.headers, * body: file * }); * ``` */ interface ClientUploadData { /** Pre-signed URL for direct upload */ uploadUrl: string; /** Storage path/key that will be used */ path: string; /** HTTP method to use (usually PUT for S3, POST for some services) */ method: "PUT" | "POST"; /** Headers to include in upload request */ headers?: Record; /** Form fields for multipart uploads (some services require this) */ fields?: Record; /** URL expiry timestamp */ expiresAt: Date; } /** * Base storage adapter interface. * All storage adapters must implement this interface. * * Core methods (required): * - upload: Store file buffer * - delete: Remove file from storage * - exists: Check if file exists * - getPublicUrl: Get public URL for file access * - getType: Get storage type identifier * * Optional methods: * - getInfo: Get adapter capabilities (recommended) * - getMetadata: Retrieve file metadata * - getSignedUrl: Generate temporary signed URLs for private access * - getPresignedUploadUrl: Generate pre-signed URL for client uploads */ interface BulkDeleteResult { successful: string[]; failed: Array<{ filePath: string; error: string; }>; } interface IStorageAdapter { /** Upload file buffer to storage */ upload(buffer: Buffer, options: UploadOptions): Promise; /** Delete file from storage */ delete(filePath: string): Promise; /** Bulk delete files from storage. Optional — adapters that support batch operations should implement this. */ bulkDelete?(filePaths: string[]): Promise; /** Check if file exists in storage */ exists(filePath: string): Promise; /** Get public URL for file */ getPublicUrl(filePath: string): string; /** Get storage type identifier */ getType(): string; /** Read file contents from storage (optional - not all adapters support this) */ read?(filePath: string): Promise; /** Get adapter info including capabilities (optional but recommended) */ getInfo?(): StorageAdapterInfo; /** Get file metadata (optional - not all adapters support this) */ getMetadata?(filePath: string): Promise; /** Generate signed URL for temporary private access (optional) */ getSignedUrl?(filePath: string, expiresIn?: number): Promise; /** Generate pre-signed upload URL for client-side uploads (optional) */ getPresignedUploadUrl?(key: string, mimeType: string, expiresIn?: number): Promise; } /** * Image Processor * * Uses Sharp for high-performance image processing: * - Extract metadata (width, height, format) * - Generate thumbnails (300x300, cropped to center) * - Optimize images (compression, WebP conversion) * * Sharp is 4-5x faster than ImageMagick/GraphicsMagick * * NOTE: Sharp is lazy-loaded to work with Next.js serverExternalPackages */ declare class ImageProcessor { /** * Get image metadata without loading full image */ getMetadata(buffer: Buffer): Promise; /** * Generate thumbnail (300x300 by default, cropped to center) * * Uses "cover" fit to fill the entire 300x300 area while maintaining aspect ratio */ generateThumbnail(buffer: Buffer, size?: number): Promise; /** * Optimize image (compress, convert to WebP if beneficial) * * Strategy: * - Small images (<100KB) and already WebP: return as-is * - Otherwise: convert to WebP with quality 80 */ optimize(buffer: Buffer, quality?: number): Promise; /** * Resize image to specific dimensions * * @param maxWidth Maximum width (maintains aspect ratio) * @param maxHeight Maximum height (maintains aspect ratio) */ resize(buffer: Buffer, maxWidth?: number, maxHeight?: number): Promise; /** * Resize an image with focal point awareness and format conversion. * * When fit is 'cover' and a focal point is set, the crop anchors at that * point instead of center. Supports format conversion ('auto' outputs webp * for jpeg/png/tiff sources, keeps original for gif). */ resizeWithFocalPoint(buffer: Buffer, options: { width?: number; height?: number; fit: "cover" | "inside" | "contain" | "fill"; quality?: number; format?: "auto" | "webp" | "jpeg" | "png" | "avif"; focalX?: number; focalY?: number; }): Promise<{ buffer: Buffer; width: number; height: number; format: string; size: number; }>; /** * Check if buffer is a valid image */ isValidImage(buffer: Buffer): Promise; /** * Get image dimensions quickly (without full processing) */ getDimensions(buffer: Buffer): Promise<{ width: number; height: number; } | null>; } /** * Get singleton ImageProcessor instance */ declare function getImageProcessor(): ImageProcessor; /** * Reset processor singleton (for testing) */ declare function resetImageProcessor(): void; /** * Unified Media Storage Manager * * Manages storage adapters and routes uploads to appropriate backends * based on collection configuration. Supports: * - AWS S3 / Cloudflare R2 / MinIO (via @nextlyhq/storage-s3) * - Vercel Blob (via @nextlyhq/storage-vercel-blob) * - Collection-specific storage routing * * @example With Vercel Blob storage (configured in nextly.config.ts) * ```typescript * import { vercelBlobStorage } from '@nextlyhq/storage-vercel-blob'; * * export default defineConfig({ * storage: [ * vercelBlobStorage({ * collections: { media: true } * }) * ] * }); * ``` * * @example With S3 storage (configured in nextly.config.ts) * ```typescript * import { s3Storage } from '@nextlyhq/storage-s3'; * * export default defineConfig({ * storage: [ * s3Storage({ * bucket: process.env.S3_BUCKET!, * region: process.env.AWS_REGION!, * collections: { * media: true, * 'private-docs': { * prefix: 'private/', * signedDownloads: true, * clientUploads: true * } * } * }) * ] * }); * ``` */ /** * Configuration for MediaStorage initialization. */ interface MediaStorageConfig { /** * Storage plugins from config. * Each plugin provides an adapter for specific collections. * * @example * ```typescript * plugins: [ * s3Storage({ bucket: '...', collections: { media: true } }), * vercelBlobStorage({ collections: { videos: true } }) * ] * ``` */ plugins?: StoragePlugin[]; /** * Local storage configuration. * Used as the default fallback when no cloud plugins are configured. * * @example * ```typescript * local: { * uploadDir: './public/uploads', * publicPath: '/uploads', * } * ``` */ local?: { /** Directory to store uploaded files (default: ./public/uploads) */ uploadDir?: string; /** URL path prefix for serving files (default: /uploads) */ publicPath?: string; }; } /** * Unified Media Storage Manager. * * Routes uploads to appropriate storage backends based on collection * configuration. Supports plugin-based storage adapters for cloud * providers (S3, Vercel Blob) with collection-specific routing. * * Features: * - Plugin-based cloud storage (S3, Vercel Blob) * - Collection-specific routing * - Client-side upload URL generation * - Signed download URLs */ declare class MediaStorage { /** Registered storage plugins by name */ private plugins; /** Storage adapter per collection */ private collectionAdapters; /** Storage configuration per collection */ private collectionConfigs; /** Local storage adapter (always available as fallback) */ private localAdapter; /** * Create a new MediaStorage instance. * * @param config - Optional configuration for storage initialization */ constructor(config?: MediaStorageConfig); /** * Register a storage plugin. * * Plugins provide storage adapters for specific collections. * When a collection is registered with a plugin, uploads for that * collection will be routed to the plugin's adapter. * * @param plugin - The storage plugin to register * * @example * ```typescript * const storage = new MediaStorage(); * * storage.registerPlugin(s3Storage({ * bucket: 'my-bucket', * region: 'us-east-1', * collections: { * media: true, * 'private-docs': { prefix: 'private/' } * } * })); * ``` */ registerPlugin(plugin: StoragePlugin): void; /** * Check if any storage adapter is configured. * * @returns True if at least one storage plugin is registered */ hasAdapter(): boolean; /** * Get the storage adapter if available, or null if not configured. * * Unlike getAdapter(), this method does not throw an error if no storage * is configured. Useful for optional storage scenarios. * * @param collection - The collection slug (optional) * @returns The storage adapter instance, or null if not configured */ getAdapterOrNull(collection?: string): IStorageAdapter | null; /** * Get the storage adapter for a specific collection. * * If a plugin is configured for the collection, returns the plugin's adapter. * Otherwise, returns the default adapter (first registered plugin). * * @param collection - The collection slug (optional) * @returns The appropriate storage adapter * @throws Error if no storage plugin is configured */ getAdapterForCollection(collection?: string): IStorageAdapter; /** * Get configuration for a specific collection. * * @param collection - The collection slug * @returns The collection's storage configuration, or undefined */ getCollectionConfig(collection: string): CollectionStorageConfig | undefined; /** * Upload file to appropriate storage based on collection. * * Routes the upload to the correct adapter based on collection * configuration. Applies collection-specific prefix if configured. * * @param buffer - The file buffer to upload * @param options - Upload options including filename, mimeType, collection * @returns Upload result with URL and path * * @example * ```typescript * const result = await storage.upload(buffer, { * filename: 'photo.jpg', * mimeType: 'image/jpeg', * collection: 'media' * }); * console.log(result.url); // Public URL * console.log(result.path); // Storage path for deletion * ``` */ upload(buffer: Buffer, options: UploadOptions): Promise; /** * Delete file from storage. * * Determines correct adapter based on collection. * * @param filePath - The storage path/key of the file * @param collection - The collection slug (optional, for routing) */ delete(filePath: string, collection?: string): Promise; /** * Bulk delete files from storage. * Uses adapter's native bulkDelete if available, otherwise falls back to * sequential individual deletes in chunks of 10. */ bulkDelete(filePaths: string[], collection?: string): Promise; /** * Check if file exists in storage. * * @param filePath - The storage path/key to check * @param collection - The collection slug (optional, for routing) * @returns True if file exists */ exists(filePath: string, collection?: string): Promise; /** * Get public URL for file. * * @param filePath - The storage path/key * @param collection - The collection slug (optional, for routing) * @returns Public URL to access the file */ getPublicUrl(filePath: string, collection?: string): string; /** * Get storage type for a collection. * * @param collection - The collection slug (optional) * @returns Storage type identifier ('s3', 'vercel-blob') */ getStorageType(collection?: string): string; /** * Check if collection supports client-side uploads. * * Client-side uploads allow direct-to-storage uploads, bypassing * the server. This is essential for serverless platforms with * request body size limits (e.g., Vercel's 4.5MB limit). * * @param collection - The collection slug * @returns True if client uploads are enabled and supported */ supportsClientUploads(collection: string): boolean; /** * Get client upload URL for direct-to-storage uploads. * * Generates a pre-signed URL that allows the client to upload * directly to the storage backend, bypassing the server. * * Only available if: * 1. Collection is configured with `clientUploads: true` * 2. The storage adapter supports client uploads * * @param filename - Original filename * @param mimeType - File MIME type * @param collection - Collection slug * @returns Client upload data, or null if not supported * * @example * ```typescript * // Server-side: generate upload URL * const uploadData = await storage.getClientUploadUrl( * 'photo.jpg', * 'image/jpeg', * 'media' * ); * * // Client-side: upload directly to storage * await fetch(uploadData.uploadUrl, { * method: uploadData.method, * headers: uploadData.headers, * body: file * }); * ``` */ getClientUploadUrl(filename: string, mimeType: string, collection: string): Promise; /** * Check if collection supports signed download URLs. * * @param collection - The collection slug * @returns True if signed downloads are enabled and supported */ supportsSignedDownloads(collection: string): boolean; /** * Get signed download URL for secure file access. * * Generates a time-limited signed URL for accessing files in * private storage buckets. Only works if: * 1. Collection is configured with `signedDownloads: true` * 2. The storage adapter supports signed URLs * * @param filePath - Storage path/key of the file * @param collection - Collection slug * @param expiresIn - URL expiry time in seconds (optional) * @returns Signed URL, or null if not supported * * @example * ```typescript * const signedUrl = await storage.getSignedDownloadUrl( * 'private/doc.pdf', * 'private-docs', * 3600 // 1 hour * ); * ``` */ getSignedDownloadUrl(filePath: string, collection: string, expiresIn?: number): Promise; /** * Get the default storage adapter. * * @returns The default storage adapter (first registered plugin) * @throws Error if no storage plugin is configured */ getDefaultAdapter(): IStorageAdapter; /** * Get list of registered plugins. * * @returns Array of registered storage plugins */ getPlugins(): StoragePlugin[]; /** * Get the underlying storage adapter for a collection. * * Useful for passing to registerServices() which requires IStorageAdapter. * * @param collection - The collection slug (optional) * @returns The storage adapter instance */ getAdapter(collection?: string): IStorageAdapter; /** * Check if a collection has a configured storage adapter. * * @param collection - The collection slug * @returns True if a plugin is configured for this collection */ hasCollectionAdapter(collection: string): boolean; /** * Get list of collections with configured storage. * * @returns Array of collection slugs that have plugin storage */ getConfiguredCollections(): string[]; /** * Check if any storage plugin is configured. * * @returns True if at least one storage plugin is registered */ hasPlugins(): boolean; } /** * Initialize the global MediaStorage instance with plugins. * * Called during Nextly initialization to set up storage with * configured plugins from nextly.config.ts. * * @param config - Storage configuration with plugins * @returns The initialized MediaStorage instance * * @example * ```typescript * // In Nextly initialization * import { initializeMediaStorage } from 'nextly/storage'; * * const storage = initializeMediaStorage({ * plugins: config.storage, // From nextly.config.ts * }); * ``` */ declare function initializeMediaStorage(config?: MediaStorageConfig): MediaStorage; /** * Get the global MediaStorage instance. * * Returns the initialized MediaStorage singleton. If not yet initialized, * creates a new instance without plugins (which will throw errors on upload). * * @returns The MediaStorage instance * * @example * ```typescript * import { getMediaStorage } from 'nextly/storage'; * * const storage = getMediaStorage(); * const result = await storage.upload(buffer, { * filename: 'photo.jpg', * mimeType: 'image/jpeg', * collection: 'media' * }); * ``` */ declare function getMediaStorage(): MediaStorage; /** * Reset storage singleton. * * Clears the cached MediaStorage instance. Useful for testing * or when re-initializing with different configuration. * * @example * ```typescript * // In tests * beforeEach(() => { * resetMediaStorage(); * }); * ``` */ declare function resetMediaStorage(): void; export { MediaStorage as M, ImageProcessor as c, getImageProcessor as j, getMediaStorage as k, initializeMediaStorage as l, resetMediaStorage as m, resetImageProcessor as r }; export type { BulkDeleteResult as B, CollectionStorageConfig as C, FileMetadata as F, IStorageAdapter as I, ProcessedImage as P, StoragePlugin as S, UploadOptions as U, CollectionStorageMap as a, StoragePluginConfig as b, UploadResult as d, StorageAdapterInfo as e, ClientUploadData as f, ImageMetadata as g, MediaStorageConfig as h, StorageType as i };