import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Dropbox } from 'dropbox'; @Injectable() export class DropboxStorageService { private readonly logger = new Logger(DropboxStorageService.name); private dbx: Dropbox; private appFolder: string; constructor(private configService: ConfigService) { const accessToken = this.configService.get( 'dropboxStorage.accessToken', ); this.appFolder = this.configService.get('dropboxStorage.appFolder') || '/StreamflixDev'; if (!accessToken) { throw new Error('DROPBOX_ACCESS_TOKEN is not configured'); } this.dbx = new Dropbox({ accessToken }); this.logger.log('Dropbox Storage Service initialized'); } /** * Get folder path based on file type */ private getFolderPath(fileType: string): string { const folderMap = { IMAGE: 'images', VIDEO: 'videos', DOCUMENT: 'documents', }; return folderMap[fileType] || 'others'; } /** * Upload file to Dropbox */ async uploadFile(params: { fileBuffer: Buffer; filename: string; fileType?: string; customFilename?: string; }) { const { fileBuffer, filename, fileType, customFilename } = params; const subfolder = this.getFolderPath(fileType || 'IMAGE'); const timestamp = Date.now(); // Use custom filename if provided, otherwise use original filename const finalFilename = customFilename || filename; const sanitizedFilename = finalFilename.replace(/[^a-zA-Z0-9.-]/g, '_'); const dropboxPath = `${this.appFolder}/${subfolder}/${timestamp}-${sanitizedFilename}`; try { const res = await this.dbx.filesUpload({ path: dropboxPath, contents: fileBuffer, mode: { '.tag': 'add' }, autorename: true, mute: false, strict_conflict: false, }); this.logger.log(`File uploaded: ${res.result.path_lower}`); // Create shared link const sharedUrl = await this.createSharedLink( res.result.path_lower || dropboxPath, ); const directUrl = this.toDirectUrl(sharedUrl); return { success: true, name: res.result.name, dropboxPath: res.result.path_lower, size: (res.result as any).size, publicUrl: directUrl, sharedUrl: sharedUrl, }; } catch (error) { this.logger.error('Upload failed:', error); throw error; } } /** * Create shared link for a file */ async createSharedLink(path: string): Promise { try { const res = await this.dbx.sharingCreateSharedLinkWithSettings({ path }); return res.result.url; } catch (err: any) { // Handle case where link already exists if ( err?.error?.error?.['.tag'] === 'shared_link_already_exists' || err?.error?.error_summary?.includes('shared_link_already_exists') ) { const list = await this.dbx.sharingListSharedLinks({ path }); if (list.result.links && list.result.links.length > 0) { return list.result.links[0].url; } } throw err; } } /** * Convert Dropbox preview URL to direct download URL */ private toDirectUrl(sharedUrl: string): string { // Handle new Dropbox shared link format (scl/fi/) if (sharedUrl.includes('dropbox.com/scl/fi/')) { // Replace dl=0 with raw=1 for direct access return sharedUrl.replace('dl=0', 'raw=1'); } // Handle old format if (sharedUrl.includes('?dl=0')) { return sharedUrl.replace('?dl=0', '?raw=1'); } if (!sharedUrl.includes('?')) { return `${sharedUrl}?raw=1`; } return sharedUrl; } /** * Get file metadata and shared link */ async getFileInfo(path: string) { try { const metadata = await this.dbx.filesGetMetadata({ path }); const sharedUrl = await this.createSharedLink(path); const directUrl = this.toDirectUrl(sharedUrl); return { success: true, name: metadata.result.name, path: metadata.result.path_lower, size: (metadata.result as any).size, publicUrl: directUrl, sharedUrl: sharedUrl, }; } catch (error) { this.logger.error('Get file info failed:', error); throw error; } } /** * Delete file from Dropbox */ async deleteFile(path: string) { try { const res = await this.dbx.filesDeleteV2({ path }); this.logger.log(`File deleted: ${path}`); return { success: true, deletedPath: path, metadata: res.result.metadata, }; } catch (error) { this.logger.error('Delete failed:', error); throw error; } } /** * Delete file by public URL */ async deleteFileByUrl(url: string) { const path = this.extractPathFromUrl(url); if (!path) { throw new Error('Invalid Dropbox URL'); } return this.deleteFile(path); } /** * Extract Dropbox path from URL */ private extractPathFromUrl(url: string): string | null { try { // Handle direct URLs like: https://www.dropbox.com/s/xxxxx/filename.jpg?raw=1 // or shared URLs: https://www.dropbox.com/s/xxxxx/filename.jpg?dl=0 const urlObj = new URL(url); const pathname = urlObj.pathname; // Extract filename from URL const parts = pathname.split('/'); const filename = parts[parts.length - 1]; if (!filename) { return null; } // Try to match against app folder structure // This is a simplified approach - you might need to store the full path in DB return `${this.appFolder}/images/${filename}`; } catch (error) { this.logger.error('Failed to extract path from URL:', error); return null; } } /** * List files in a folder */ async listFiles(folderPath?: string) { const path = folderPath || this.appFolder; try { const res = await this.dbx.filesListFolder({ path }); return { success: true, files: res.result.entries, hasMore: res.result.has_more, }; } catch (error) { this.logger.error('List files failed:', error); throw error; } } }