Files
gh-christianlouis-docuelevate/app/tasks/upload_to_dropbox.py
T
copilot-swe-agent[bot] a03b3af933 feat(pdfa): add FreeTSA timestamping, per-provider folder overrides, individual upload toggles
- Add RFC 3161 timestamping via FreeTSA (PDFA_TIMESTAMP_ENABLED, PDFA_TIMESTAMP_URL)
- Replace PDFA_UPLOAD_TO_PROVIDERS with individual PDFA_UPLOAD_ORIGINAL and PDFA_UPLOAD_PROCESSED
- Add PDFA_UPLOAD_FOLDER setting for per-provider subfolder configuration
- Add GOOGLE_DRIVE_PDFA_FOLDER_ID for Google Drive-specific folder override
- Add folder_override parameter to all 8 folder-using upload tasks
- Add folder_overrides dict parameter to send_to_all_destinations
- Add _compute_pdfa_folder_overrides() and _timestamp_file() helpers
- Expand tests to 26 (timestamping, folder overrides, individual toggles)
- Update docs/ConfigurationGuide.md and .env.demo

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
2026-03-02 13:55:34 +00:00

224 lines
8.8 KiB
Python

#!/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)