#!/usr/bin/env python3 import logging import os import dropbox import requests from dropbox.exceptions import ApiError, AuthError from app.celery_app import celery from app.config import settings from app.tasks.retry_config import UploadTaskWithRetry from app.utils import log_task_progress from app.utils.filename_utils import extract_remote_path, get_unique_filename logger = logging.getLogger(__name__) def _validate_dropbox_settings(): """Validate that all required Dropbox settings are available.""" missing = [] if not hasattr(settings, "dropbox_refresh_token") or not settings.dropbox_refresh_token: missing.append("refresh token") if not hasattr(settings, "dropbox_app_key") or not settings.dropbox_app_key: missing.append("app key") if not hasattr(settings, "dropbox_app_secret") or not settings.dropbox_app_secret: missing.append("app secret") if missing: logger.error(f"Cannot refresh Dropbox token: Missing {', '.join(missing)}") return False return True def get_dropbox_access_token(): """Refresh the Dropbox access token using the stored refresh token from ENV.""" # Check if needed settings are available if not _validate_dropbox_settings(): return None token_url = "https://api.dropbox.com/oauth2/token" headers = {"Content-Type": "application/x-www-form-urlencoded"} data = { "grant_type": "refresh_token", "refresh_token": settings.dropbox_refresh_token, "client_id": settings.dropbox_app_key, "client_secret": settings.dropbox_app_secret, } response = requests.post(token_url, headers=headers, data=data, timeout=settings.http_request_timeout) if response.status_code == 200: return response.json()["access_token"] else: error_msg = f"Failed to refresh Dropbox token: {response.status_code} - {response.text}" logger.error(error_msg) raise Exception(error_msg) def get_dropbox_client(): """ Create and return an authenticated Dropbox client using the configured refresh token. Returns: dropbox.Dropbox: Authenticated Dropbox client instance Raises: ValueError: If required Dropbox configuration is missing AuthError: If authentication with Dropbox fails """ app_key = settings.dropbox_app_key app_secret = settings.dropbox_app_secret refresh_token = settings.dropbox_refresh_token # Validate configuration if not app_key or not app_secret: raise ValueError("Dropbox app key or app secret is not configured") if not refresh_token: raise ValueError("Dropbox refresh token is not configured") # Create a Dropbox client with refresh token try: dbx = dropbox.Dropbox(app_key=app_key, app_secret=app_secret, oauth2_refresh_token=refresh_token) # Test the connection dbx.users_get_current_account() logger.info("Successfully authenticated with Dropbox") return dbx except AuthError as auth_error: logger.error(f"Dropbox authentication failed: {str(auth_error)}") raise except Exception as e: logger.error(f"Error creating Dropbox client: {str(e)}") raise @celery.task(base=UploadTaskWithRetry, bind=True) def upload_to_dropbox(self, file_path: str, file_id: int = None, folder_override: str = None): """ Upload a file to Dropbox. Args: file_path: Path to the file to upload file_id: Optional file ID to associate with logs """ task_id = self.request.id logger.info(f"[{task_id}] Starting Dropbox upload: {file_path}") log_task_progress( task_id, "upload_to_dropbox", "in_progress", f"Uploading to Dropbox: {os.path.basename(file_path)}", file_id=file_id, ) if not os.path.exists(file_path): error_msg = f"File not found: {file_path}" logger.error(f"[{task_id}] {error_msg}") log_task_progress(task_id, "upload_to_dropbox", "failure", error_msg, file_id=file_id) raise FileNotFoundError(error_msg) # Check if Dropbox is properly configured if not ( hasattr(settings, "dropbox_app_key") and settings.dropbox_app_key and hasattr(settings, "dropbox_app_secret") and settings.dropbox_app_secret and hasattr(settings, "dropbox_refresh_token") and settings.dropbox_refresh_token ): logger.info(f"[{task_id}] Dropbox upload skipped: Missing configuration") log_task_progress(task_id, "upload_to_dropbox", "success", "Skipped: Not configured", file_id=file_id) return {"status": "Skipped", "reason": "Dropbox settings not configured"} filename = os.path.basename(file_path) try: # Get the Dropbox client dbx = get_dropbox_client() # Calculate remote path based on local file structure remote_base = folder_override if folder_override is not None else (settings.dropbox_folder or "") remote_path = extract_remote_path(file_path, settings.workdir, remote_base) # Function to check if file exists in Dropbox def check_exists_in_dropbox(path): try: dbx.files_get_metadata(path) return True except ApiError as e: if e.error.is_path() and e.error.get_path().is_not_found(): return False raise # Get a unique path in case of collision remote_full_path = f"/{remote_path}" # Dropbox paths should start with / remote_full_path = remote_full_path.replace("//", "/") # Clean double slashes # Check for potential file collision and get a unique name if needed dropbox_path = get_unique_filename(remote_full_path, check_exists_in_dropbox) # Upload the file logger.info(f"[{task_id}] Uploading {filename} to Dropbox at {dropbox_path}") log_task_progress(task_id, "upload_file", "in_progress", f"Uploading to {dropbox_path}", file_id=file_id) with open(file_path, "rb") as file_data: # Use files_upload_session for large files to avoid timeouts file_size = os.path.getsize(file_path) if file_size > 10 * 1024 * 1024: # 10 MB threshold for chunked upload cursor = None chunk_size = 4 * 1024 * 1024 # 4 MB chunks file_data.seek(0) # Start upload session session_start = dbx.files_upload_session_start(file_data.read(chunk_size)) cursor = dropbox.files.UploadSessionCursor(session_start.session_id, file_data.tell()) # Upload chunks until we reach the end while file_data.tell() < file_size: if (file_size - file_data.tell()) <= chunk_size: # Last chunk dbx.files_upload_session_finish( file_data.read(chunk_size), cursor, dropbox.files.CommitInfo(path=dropbox_path, mode=dropbox.files.WriteMode.overwrite), ) else: # More chunks to upload dbx.files_upload_session_append_v2(file_data.read(chunk_size), cursor) cursor.offset = file_data.tell() else: # Small file, direct upload file_data.seek(0) dbx.files_upload(file_data.read(), dropbox_path, mode=dropbox.files.WriteMode.overwrite) logger.info(f"[{task_id}] Successfully uploaded {filename} to Dropbox at {dropbox_path}") log_task_progress( task_id, "upload_to_dropbox", "success", f"Uploaded to Dropbox: {dropbox_path}", file_id=file_id ) return {"status": "Completed", "file_path": file_path, "dropbox_path": dropbox_path} except AuthError: error_msg = f"Authentication failed while uploading {filename} to Dropbox. Check token." logger.error(f"[{task_id}] {error_msg}") log_task_progress(task_id, "upload_to_dropbox", "failure", error_msg, file_id=file_id) raise Exception(error_msg) except ApiError as e: error_msg = f"Failed to upload {filename} to Dropbox: {e}" logger.error(f"[{task_id}] {error_msg}") log_task_progress(task_id, "upload_to_dropbox", "failure", error_msg, file_id=file_id) raise Exception(error_msg) except Exception as e: error_msg = f"Unexpected error uploading {filename} to Dropbox: {e}" logger.error(f"[{task_id}] {error_msg}") log_task_progress(task_id, "upload_to_dropbox", "failure", error_msg, file_id=file_id) raise Exception(error_msg)