495 lines
18 KiB
Python
495 lines
18 KiB
Python
"""
|
|
Helpers for Dropbox API integration
|
|
"""
|
|
from flask import current_app, url_for, redirect, session
|
|
import requests
|
|
import json
|
|
import os
|
|
from datetime import datetime, timedelta
|
|
from flask_login import current_user
|
|
|
|
def get_dropbox_auth_url():
|
|
"""Get the authorization URL for Dropbox OAuth flow"""
|
|
app_key = current_app.config.get('DROPBOX_APP_KEY')
|
|
redirect_uri = url_for('users.dropbox_callback', _external=True)
|
|
|
|
# Add the required scopes for our application
|
|
scopes = ["files.content.read", "files.content.write", "sharing.write","account_info.read"]
|
|
|
|
auth_url = f'https://www.dropbox.com/oauth2/authorize?client_id={app_key}&response_type=code&redirect_uri={redirect_uri}&scope={" ".join(scopes)}&token_access_type=offline'
|
|
return auth_url
|
|
|
|
def exchange_code_for_token(code):
|
|
"""Exchange the authorization code for an access token"""
|
|
app_key = current_app.config.get('DROPBOX_APP_KEY')
|
|
app_secret = current_app.config.get('DROPBOX_APP_SECRET')
|
|
redirect_uri = url_for('users.dropbox_callback', _external=True)
|
|
|
|
data = {
|
|
'code': code,
|
|
'grant_type': 'authorization_code',
|
|
'client_id': app_key,
|
|
'client_secret': app_secret,
|
|
'redirect_uri': redirect_uri
|
|
}
|
|
|
|
response = requests.post('https://api.dropboxapi.com/oauth2/token', data=data)
|
|
|
|
if response.status_code == 200:
|
|
return response.json()
|
|
else:
|
|
current_app.logger.error(f"Error exchanging code for token: {response.text}")
|
|
return None
|
|
|
|
def refresh_dropbox_token(refresh_token):
|
|
"""Refresh an expired Dropbox access token"""
|
|
app_key = current_app.config.get('DROPBOX_APP_KEY')
|
|
app_secret = current_app.config.get('DROPBOX_APP_SECRET')
|
|
|
|
data = {
|
|
'refresh_token': refresh_token,
|
|
'grant_type': 'refresh_token',
|
|
'client_id': app_key,
|
|
'client_secret': app_secret
|
|
}
|
|
|
|
response = requests.post('https://api.dropboxapi.com/oauth2/token', data=data)
|
|
|
|
if response.status_code == 200:
|
|
return response.json()
|
|
else:
|
|
current_app.logger.error(f"Error refreshing token: {response.text}")
|
|
return None
|
|
|
|
def get_dropbox_user_info(access_token):
|
|
"""Get user info from Dropbox API"""
|
|
headers = {
|
|
'Authorization': f'Bearer {access_token}',
|
|
'Content-Type': 'application/json'
|
|
}
|
|
|
|
response = requests.post('https://api.dropboxapi.com/2/users/get_current_account', headers=headers)
|
|
|
|
if response.status_code == 200:
|
|
return response.json()
|
|
else:
|
|
current_app.logger.error(f"Error getting user info: {response.text}")
|
|
return None
|
|
|
|
def get_current_user_dropbox_token():
|
|
"""Get a valid Dropbox access token for the current user, refreshing if needed"""
|
|
if not current_user or not current_user.is_authenticated:
|
|
current_app.logger.error("No authenticated user")
|
|
return None
|
|
|
|
# Check if token exists and is valid
|
|
if (current_user.dropbox_token and
|
|
current_user.dropbox_token_expiry and
|
|
current_user.dropbox_token_expiry > datetime.now() + timedelta(minutes=5)):
|
|
# Token is valid and not about to expire
|
|
return current_user.dropbox_token
|
|
|
|
# Token is missing or about to expire - try to refresh
|
|
if current_user.dropbox_refresh_token:
|
|
from musicround.models import db
|
|
|
|
# Try to refresh the token
|
|
token_info = refresh_dropbox_token(current_user.dropbox_refresh_token)
|
|
|
|
if token_info and 'access_token' in token_info:
|
|
# Update token in database
|
|
current_user.dropbox_token = token_info['access_token']
|
|
expires_in = token_info.get('expires_in', 14400) # Default to 4 hours if not specified
|
|
current_user.dropbox_token_expiry = datetime.now() + timedelta(seconds=expires_in)
|
|
|
|
db.session.commit()
|
|
|
|
return current_user.dropbox_token
|
|
|
|
# If we get here, we couldn't refresh the token
|
|
current_app.logger.error("Failed to get valid Dropbox token")
|
|
return None
|
|
|
|
def upload_and_share(file_path, dropbox_path):
|
|
"""
|
|
Upload a file to Dropbox and create a shared link
|
|
|
|
Args:
|
|
file_path: Local path to the file to upload
|
|
dropbox_path: Destination path in Dropbox (including filename)
|
|
|
|
Returns:
|
|
Shared link URL or None if upload failed
|
|
"""
|
|
token = get_current_user_dropbox_token()
|
|
if not token:
|
|
current_app.logger.error("No valid Dropbox token available")
|
|
return None
|
|
|
|
# Make sure dropbox_path starts with /
|
|
if not dropbox_path.startswith('/'):
|
|
dropbox_path = '/' + dropbox_path
|
|
|
|
# First, upload the file
|
|
try:
|
|
# Check if file exists
|
|
if not os.path.exists(file_path):
|
|
current_app.logger.error(f"File not found: {file_path}")
|
|
return None
|
|
|
|
# Get file size
|
|
file_size = os.path.getsize(file_path)
|
|
|
|
# For small files (< 150 MB), use simple upload
|
|
if file_size < 150 * 1024 * 1024:
|
|
with open(file_path, 'rb') as f:
|
|
file_data = f.read()
|
|
|
|
headers = {
|
|
'Authorization': f'Bearer {token}',
|
|
'Dropbox-API-Arg': json.dumps({
|
|
'path': dropbox_path,
|
|
'mode': 'overwrite',
|
|
'autorename': True,
|
|
'mute': False
|
|
}),
|
|
'Content-Type': 'application/octet-stream'
|
|
}
|
|
|
|
response = requests.post(
|
|
'https://content.dropboxapi.com/2/files/upload',
|
|
headers=headers,
|
|
data=file_data
|
|
)
|
|
|
|
if response.status_code != 200:
|
|
current_app.logger.error(f"Error uploading file: {response.text}")
|
|
return None
|
|
|
|
file_metadata = response.json()
|
|
current_app.logger.info(f"File uploaded successfully: {file_metadata.get('path_display')}")
|
|
else:
|
|
# For larger files, we'd implement chunked upload here
|
|
current_app.logger.error(f"File too large for simple upload: {file_size} bytes")
|
|
return None
|
|
|
|
# Now create a shared link
|
|
headers = {
|
|
'Authorization': f'Bearer {token}',
|
|
'Content-Type': 'application/json'
|
|
}
|
|
|
|
data = {
|
|
'path': file_metadata.get('path_lower', dropbox_path),
|
|
'settings': {
|
|
'requested_visibility': 'public' # Make link publicly accessible
|
|
}
|
|
}
|
|
|
|
response = requests.post(
|
|
'https://api.dropboxapi.com/2/sharing/create_shared_link_with_settings',
|
|
headers=headers,
|
|
json=data
|
|
)
|
|
|
|
# If the link already exists, we'll get a 409 error with "shared_link_already_exists"
|
|
if response.status_code == 409 and "shared_link_already_exists" in response.text:
|
|
# Get existing links
|
|
list_data = {
|
|
'path': file_metadata.get('path_lower', dropbox_path)
|
|
}
|
|
|
|
list_response = requests.post(
|
|
'https://api.dropboxapi.com/2/sharing/list_shared_links',
|
|
headers=headers,
|
|
json=list_data
|
|
)
|
|
|
|
if list_response.status_code == 200:
|
|
links_data = list_response.json()
|
|
if links_data.get('links') and len(links_data['links']) > 0:
|
|
# Return the first link's URL
|
|
return links_data['links'][0].get('url')
|
|
|
|
elif response.status_code == 200:
|
|
share_data = response.json()
|
|
current_app.logger.info(f"Created shared link: {share_data.get('url')}")
|
|
return share_data.get('url')
|
|
|
|
current_app.logger.error(f"Error creating shared link: {response.text}")
|
|
return None
|
|
|
|
except Exception as e:
|
|
current_app.logger.error(f"Exception in upload_and_share: {str(e)}")
|
|
return None
|
|
|
|
def refresh_dropbox_token_if_needed(user):
|
|
"""
|
|
Check if user's Dropbox token needs refreshing and refresh it if needed
|
|
|
|
Args:
|
|
user: The User object with Dropbox token information
|
|
|
|
Returns:
|
|
dict: {'success': True/False, 'message': 'success or error message'}
|
|
"""
|
|
if not user.dropbox_token or not user.dropbox_refresh_token:
|
|
return {'success': False, 'message': 'No Dropbox token available'}
|
|
|
|
# If token is still valid, return success
|
|
if user.dropbox_token_expiry and user.dropbox_token_expiry > datetime.now() + timedelta(minutes=5):
|
|
return {'success': True, 'message': 'Token is still valid'}
|
|
|
|
# Token needs refreshing
|
|
from musicround.models import db
|
|
|
|
try:
|
|
token_info = refresh_dropbox_token(user.dropbox_refresh_token)
|
|
|
|
if token_info and 'access_token' in token_info:
|
|
# Update token in database
|
|
user.dropbox_token = token_info['access_token']
|
|
expires_in = token_info.get('expires_in', 14400) # Default to 4 hours if not specified
|
|
user.dropbox_token_expiry = datetime.now() + timedelta(seconds=expires_in)
|
|
|
|
# If we got a new refresh token, update that too
|
|
if token_info.get('refresh_token'):
|
|
user.dropbox_refresh_token = token_info['refresh_token']
|
|
|
|
db.session.commit()
|
|
|
|
return {'success': True, 'message': 'Token refreshed successfully'}
|
|
else:
|
|
return {'success': False, 'message': 'Failed to refresh token'}
|
|
|
|
except Exception as e:
|
|
current_app.logger.error(f"Error refreshing Dropbox token: {str(e)}")
|
|
return {'success': False, 'message': f'Error refreshing token: {str(e)}'}
|
|
|
|
def upload_to_dropbox(access_token, dropbox_path, data, mode='binary'):
|
|
"""
|
|
Upload data to Dropbox
|
|
|
|
Args:
|
|
access_token: Dropbox access token
|
|
dropbox_path: Destination path in Dropbox (including filename)
|
|
data: The data to upload (bytes for binary mode, string for text mode)
|
|
mode: 'binary' or 'text'
|
|
|
|
Returns:
|
|
dict: {'success': True/False, 'message': 'success or error message', 'metadata': file metadata if successful}
|
|
"""
|
|
# Make sure dropbox_path starts with /
|
|
if not dropbox_path.startswith('/'):
|
|
dropbox_path = '/' + dropbox_path
|
|
|
|
try:
|
|
# Convert string data to bytes if text mode
|
|
if mode == 'text' and isinstance(data, str):
|
|
data = data.encode('utf-8')
|
|
|
|
# Debug token information
|
|
token_preview = access_token[:10] + '...' if access_token else 'None'
|
|
current_app.logger.debug(f"Upload to Dropbox - Path: {dropbox_path}, Token preview: {token_preview}, Data size: {len(data) if data else 0} bytes")
|
|
|
|
headers = {
|
|
'Authorization': f'Bearer {access_token}',
|
|
'Dropbox-API-Arg': json.dumps({
|
|
'path': dropbox_path,
|
|
'mode': 'overwrite',
|
|
'autorename': True,
|
|
'mute': False
|
|
}),
|
|
'Content-Type': 'application/octet-stream'
|
|
}
|
|
|
|
current_app.logger.debug(f"Dropbox API headers: {headers}")
|
|
|
|
response = requests.post(
|
|
'https://content.dropboxapi.com/2/files/upload',
|
|
headers=headers,
|
|
data=data
|
|
)
|
|
|
|
current_app.logger.debug(f"Dropbox upload response code: {response.status_code}")
|
|
|
|
if response.status_code != 200:
|
|
current_app.logger.error(f"Error uploading to Dropbox: Status code {response.status_code}")
|
|
current_app.logger.error(f"Response headers: {response.headers}")
|
|
|
|
# Try to parse response body
|
|
try:
|
|
error_json = response.json()
|
|
current_app.logger.error(f"Error details: {json.dumps(error_json, indent=2)}")
|
|
error_message = error_json.get('error_summary', 'Unknown error')
|
|
except:
|
|
error_message = response.text[:500] # Limit to first 500 chars
|
|
current_app.logger.error(f"Raw error response: {error_message}")
|
|
|
|
return {
|
|
'success': False,
|
|
'message': f"Error uploading file: {response.status_code} - {error_message}",
|
|
'status_code': response.status_code
|
|
}
|
|
|
|
file_metadata = response.json()
|
|
current_app.logger.info(f"File uploaded successfully: {file_metadata.get('path_display')}")
|
|
current_app.logger.debug(f"Upload metadata: {json.dumps(file_metadata, indent=2)}")
|
|
|
|
return {
|
|
'success': True,
|
|
'message': 'File uploaded successfully',
|
|
'metadata': file_metadata
|
|
}
|
|
|
|
except Exception as e:
|
|
import traceback
|
|
current_app.logger.error(f"Exception in upload_to_dropbox: {str(e)}")
|
|
current_app.logger.error(traceback.format_exc())
|
|
return {
|
|
'success': False,
|
|
'message': f"Error uploading file: {str(e)}"
|
|
}
|
|
|
|
def create_shared_link(access_token, dropbox_path):
|
|
"""
|
|
Create a shared link for a file in Dropbox
|
|
|
|
Args:
|
|
access_token: Dropbox access token
|
|
dropbox_path: Path to the file in Dropbox
|
|
|
|
Returns:
|
|
dict: {'success': True/False, 'message': 'success or error message', 'url': shared link URL if successful}
|
|
"""
|
|
# Make sure dropbox_path starts with /
|
|
if not dropbox_path.startswith('/'):
|
|
dropbox_path = '/' + dropbox_path
|
|
|
|
try:
|
|
# Debug token information
|
|
token_preview = access_token[:10] + '...' if access_token else 'None'
|
|
current_app.logger.debug(f"Creating shared link - Path: {dropbox_path}, Token preview: {token_preview}")
|
|
|
|
headers = {
|
|
'Authorization': f'Bearer {access_token}',
|
|
'Content-Type': 'application/json'
|
|
}
|
|
|
|
data = {
|
|
'path': dropbox_path,
|
|
'settings': {
|
|
'requested_visibility': 'public' # Make link publicly accessible
|
|
}
|
|
}
|
|
|
|
current_app.logger.debug(f"Sharing API request data: {json.dumps(data, indent=2)}")
|
|
|
|
response = requests.post(
|
|
'https://api.dropboxapi.com/2/sharing/create_shared_link_with_settings',
|
|
headers=headers,
|
|
json=data
|
|
)
|
|
|
|
current_app.logger.debug(f"Sharing API response code: {response.status_code}")
|
|
|
|
# If the link already exists, we'll get a 409 error with "shared_link_already_exists"
|
|
if response.status_code == 409 and "shared_link_already_exists" in response.text:
|
|
current_app.logger.debug("Shared link already exists, retrieving existing link")
|
|
|
|
# Get existing links
|
|
list_data = {
|
|
'path': dropbox_path
|
|
}
|
|
|
|
list_response = requests.post(
|
|
'https://api.dropboxapi.com/2/sharing/list_shared_links',
|
|
headers=headers,
|
|
json=list_data
|
|
)
|
|
|
|
current_app.logger.debug(f"List shared links response code: {list_response.status_code}")
|
|
|
|
if list_response.status_code == 200:
|
|
links_data = list_response.json()
|
|
current_app.logger.debug(f"Existing links data: {json.dumps(links_data, indent=2)}")
|
|
|
|
if links_data.get('links') and len(links_data['links']) > 0:
|
|
# Return the first link's URL
|
|
url = links_data['links'][0].get('url')
|
|
current_app.logger.info(f"Retrieved existing shared link: {url}")
|
|
return {
|
|
'success': True,
|
|
'message': 'Existing shared link retrieved',
|
|
'url': url
|
|
}
|
|
else:
|
|
current_app.logger.error("No links found despite 'shared_link_already_exists' error")
|
|
else:
|
|
current_app.logger.error(f"Error listing shared links: {list_response.text}")
|
|
|
|
elif response.status_code == 200:
|
|
share_data = response.json()
|
|
current_app.logger.info(f"Created shared link: {share_data.get('url')}")
|
|
current_app.logger.debug(f"Shared link data: {json.dumps(share_data, indent=2)}")
|
|
return {
|
|
'success': True,
|
|
'message': 'Shared link created successfully',
|
|
'url': share_data.get('url')
|
|
}
|
|
|
|
# Try to parse error response
|
|
try:
|
|
error_json = response.json()
|
|
current_app.logger.error(f"Sharing API error details: {json.dumps(error_json, indent=2)}")
|
|
error_message = error_json.get('error_summary', 'Unknown error')
|
|
except:
|
|
error_message = response.text[:500] # Limit to first 500 chars
|
|
current_app.logger.error(f"Raw sharing API error response: {error_message}")
|
|
|
|
current_app.logger.error(f"Error creating shared link: {response.status_code} - {error_message}")
|
|
return {
|
|
'success': False,
|
|
'message': f"Error creating shared link: {response.status_code} - {error_message}",
|
|
'status_code': response.status_code
|
|
}
|
|
|
|
except Exception as e:
|
|
import traceback
|
|
current_app.logger.error(f"Exception in create_shared_link: {str(e)}")
|
|
current_app.logger.error(traceback.format_exc())
|
|
return {
|
|
'success': False,
|
|
'message': f"Error creating shared link: {str(e)}"
|
|
}
|
|
|
|
def get_dropbox_account_info(access_token):
|
|
"""
|
|
Get account information for a Dropbox user
|
|
|
|
Args:
|
|
access_token: Dropbox access token
|
|
|
|
Returns:
|
|
dict: Account information or None if failed
|
|
"""
|
|
headers = {
|
|
'Authorization': f'Bearer {access_token}'
|
|
}
|
|
|
|
try:
|
|
# According to the API documentation, this endpoint requires no request body
|
|
response = requests.post(
|
|
'https://api.dropboxapi.com/2/users/get_current_account',
|
|
headers=headers
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
return response.json()
|
|
else:
|
|
current_app.logger.error(f"Error getting Dropbox account info: {response.text}")
|
|
return None
|
|
|
|
except Exception as e:
|
|
current_app.logger.error(f"Exception in get_dropbox_account_info: {str(e)}")
|
|
return None |