style: fix linting issues and remove unused imports

- Remove unused imports from all modified files
- Fix flake8 violations (unused variables, f-strings without placeholders)
- Apply Black formatting consistently
- Shorten long line in google_drive.py

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-02-08 08:29:38 +00:00
parent 551b23a80c
commit d2eb9846d3
6 changed files with 350 additions and 452 deletions
+5 -4
View File
@@ -1,11 +1,11 @@
""" """
Common utilities for API routes Common utilities for API routes
""" """
import logging import logging
import os import os
from pathlib import Path from pathlib import Path
from sqlalchemy.orm import Session from fastapi import HTTPException, status
from fastapi import Depends, HTTPException, status
from app.database import SessionLocal from app.database import SessionLocal
from app.config import settings from app.config import settings
@@ -13,6 +13,7 @@ from app.config import settings
# Set up logging # Set up logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def get_db(): def get_db():
"""Database dependency injection for routes""" """Database dependency injection for routes"""
db = SessionLocal() db = SessionLocal()
@@ -21,6 +22,7 @@ def get_db():
finally: finally:
db.close() db.close()
def resolve_file_path(file_path: str, subfolder: str = None) -> str: def resolve_file_path(file_path: str, subfolder: str = None) -> str:
""" """
Resolves a file path to an absolute path with path traversal protection. Resolves a file path to an absolute path with path traversal protection.
@@ -59,8 +61,7 @@ def resolve_file_path(file_path: str, subfolder: str = None) -> str:
# Path is outside the base directory - potential path traversal attack # Path is outside the base directory - potential path traversal attack
logger.warning(f"Path traversal attempt detected: {file_path} -> {resolved_path}") logger.warning(f"Path traversal attempt detected: {file_path} -> {resolved_path}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid file path: path traversal not allowed"
detail="Invalid file path: path traversal not allowed"
) )
return str(resolved_path) return str(resolved_path)
+30 -54
View File
@@ -1,13 +1,11 @@
""" """
Dropbox API endpoints Dropbox API endpoints
""" """
from fastapi import APIRouter, Request, HTTPException, status, Form from fastapi import APIRouter, Request, HTTPException, status, Form
import logging import logging
import os import os
import requests import requests
import json
from datetime import datetime, timedelta
from typing import Optional
from app.auth import require_login from app.auth import require_login
from app.config import settings from app.config import settings
@@ -18,6 +16,7 @@ logger = logging.getLogger(__name__)
router = APIRouter() router = APIRouter()
@router.post("/dropbox/exchange-token") @router.post("/dropbox/exchange-token")
@require_login @require_login
async def exchange_dropbox_token( async def exchange_dropbox_token(
@@ -26,7 +25,7 @@ async def exchange_dropbox_token(
client_secret: str = Form(...), client_secret: str = Form(...),
redirect_uri: str = Form(...), redirect_uri: str = Form(...),
code: str = Form(...), code: str = Form(...),
folder_path: str = Form(None) folder_path: str = Form(None),
): ):
""" """
Exchange an authorization code for a refresh token from Dropbox. Exchange an authorization code for a refresh token from Dropbox.
@@ -36,27 +35,24 @@ async def exchange_dropbox_token(
token_url = "https://api.dropboxapi.com/oauth2/token" token_url = "https://api.dropboxapi.com/oauth2/token"
payload = { payload = {
'client_id': client_id, "client_id": client_id,
'client_secret': client_secret, "client_secret": client_secret,
'code': code, "code": code,
'redirect_uri': redirect_uri, "redirect_uri": redirect_uri,
'grant_type': 'authorization_code' "grant_type": "authorization_code",
} }
# Use shared OAuth helper (handles secure logging and error handling) # Use shared OAuth helper (handles secure logging and error handling)
token_data = exchange_oauth_token( token_data = exchange_oauth_token(provider_name="Dropbox", token_url=token_url, payload=payload)
provider_name="Dropbox",
token_url=token_url,
payload=payload
)
# Return just what's needed by the frontend # Return just what's needed by the frontend
return { return {
"refresh_token": token_data["refresh_token"], "refresh_token": token_data["refresh_token"],
"access_token": token_data["access_token"], "access_token": token_data["access_token"],
"expires_in": token_data.get("expires_in", 14400) "expires_in": token_data.get("expires_in", 14400),
} }
@router.post("/dropbox/update-settings") @router.post("/dropbox/update-settings")
@require_login @require_login
async def update_dropbox_settings( async def update_dropbox_settings(
@@ -64,7 +60,7 @@ async def update_dropbox_settings(
app_key: str = Form(None), app_key: str = Form(None),
app_secret: str = Form(None), app_secret: str = Form(None),
refresh_token: str = Form(...), refresh_token: str = Form(...),
folder_path: str = Form(None) folder_path: str = Form(None),
): ):
""" """
Update Dropbox settings in memory Update Dropbox settings in memory
@@ -91,18 +87,15 @@ async def update_dropbox_settings(
# Test token validity would be here, but we'll skip it for now # Test token validity would be here, but we'll skip it for now
return { return {"status": "success", "message": "Dropbox settings have been updated in memory"}
"status": "success",
"message": "Dropbox settings have been updated in memory"
}
except Exception as e: except Exception as e:
logger.exception(f"Unexpected error updating Dropbox settings: {str(e)}") logger.exception(f"Unexpected error updating Dropbox settings: {str(e)}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to update Dropbox settings: {str(e)}"
detail=f"Failed to update Dropbox settings: {str(e)}"
) )
@router.get("/dropbox/test-token") @router.get("/dropbox/test-token")
@require_login @require_login
async def test_dropbox_token(request: Request): async def test_dropbox_token(request: Request):
@@ -114,17 +107,14 @@ async def test_dropbox_token(request: Request):
if not settings.dropbox_refresh_token or not settings.dropbox_app_key or not settings.dropbox_app_secret: if not settings.dropbox_refresh_token or not settings.dropbox_app_key or not settings.dropbox_app_secret:
logger.warning("Dropbox credentials not fully configured") logger.warning("Dropbox credentials not fully configured")
return { return {"status": "error", "message": "Dropbox credentials are not fully configured"}
"status": "error",
"message": "Dropbox credentials are not fully configured"
}
# Check token validity by getting current account info # Check token validity by getting current account info
headers = {"Authorization": f"Bearer {settings.dropbox_refresh_token}"} headers = {"Authorization": f"Bearer {settings.dropbox_refresh_token}"}
response = requests.post( response = requests.post(
"https://api.dropboxapi.com/2/users/get_current_account", "https://api.dropboxapi.com/2/users/get_current_account",
headers=headers, headers=headers,
timeout=settings.http_request_timeout timeout=settings.http_request_timeout,
) )
# If token is invalid, try refreshing it # If token is invalid, try refreshing it
@@ -137,18 +127,14 @@ async def test_dropbox_token(request: Request):
"grant_type": "refresh_token", "grant_type": "refresh_token",
"refresh_token": settings.dropbox_refresh_token, "refresh_token": settings.dropbox_refresh_token,
"client_id": settings.dropbox_app_key, "client_id": settings.dropbox_app_key,
"client_secret": settings.dropbox_app_secret "client_secret": settings.dropbox_app_secret,
} }
refresh_response = requests.post(refresh_url, data=refresh_data, timeout=settings.http_request_timeout) refresh_response = requests.post(refresh_url, data=refresh_data, timeout=settings.http_request_timeout)
if refresh_response.status_code != 200: if refresh_response.status_code != 200:
logger.error(f"Failed to refresh Dropbox token: {refresh_response.text}") logger.error(f"Failed to refresh Dropbox token: {refresh_response.text}")
return { return {"status": "error", "message": "Refresh token has expired or is invalid", "needs_reauth": True}
"status": "error",
"message": "Refresh token has expired or is invalid",
"needs_reauth": True
}
token_info = refresh_response.json() token_info = refresh_response.json()
access_token = token_info.get("access_token") access_token = token_info.get("access_token")
@@ -158,14 +144,14 @@ async def test_dropbox_token(request: Request):
response = requests.post( response = requests.post(
"https://api.dropboxapi.com/2/users/get_current_account", "https://api.dropboxapi.com/2/users/get_current_account",
headers=headers, headers=headers,
timeout=settings.http_request_timeout timeout=settings.http_request_timeout,
) )
if response.status_code != 200: if response.status_code != 200:
logger.error(f"Dropbox token test failed: {response.status_code} {response.text}") logger.error(f"Dropbox token test failed: {response.status_code} {response.text}")
return { return {
"status": "error", "status": "error",
"message": f"Token validation failed with status {response.status_code}: {response.text}" "message": f"Token validation failed with status {response.status_code}: {response.text}",
} }
# Get account info # Get account info
@@ -174,27 +160,22 @@ async def test_dropbox_token(request: Request):
account_name = account_info.get("name", {}).get("display_name", "Unknown user") account_name = account_info.get("name", {}).get("display_name", "Unknown user")
# Dropbox refresh tokens don't expire, but we should note that in our response # Dropbox refresh tokens don't expire, but we should note that in our response
token_info = { token_info = {"expires_in_human": "Never expires (perpetual token)", "is_perpetual": True}
"expires_in_human": "Never expires (perpetual token)",
"is_perpetual": True
}
logger.info(f"Successfully connected to Dropbox as {account_email}") logger.info(f"Successfully connected to Dropbox as {account_email}")
return { return {
"status": "success", "status": "success",
"message": f"Dropbox connection successful", "message": "Dropbox connection successful",
"account": account_email, "account": account_email,
"account_name": account_name, "account_name": account_name,
"token_info": token_info "token_info": token_info,
} }
except Exception as e: except Exception as e:
logger.exception(f"Unexpected error testing Dropbox token: {str(e)}") logger.exception(f"Unexpected error testing Dropbox token: {str(e)}")
return { return {"status": "error", "message": f"Connection error: {str(e)}"}
"status": "error",
"message": f"Connection error: {str(e)}"
}
@router.post("/dropbox/save-settings") @router.post("/dropbox/save-settings")
@require_login @require_login
@@ -203,7 +184,7 @@ async def save_dropbox_settings(
app_key: str = Form(None), app_key: str = Form(None),
app_secret: str = Form(None), app_secret: str = Form(None),
refresh_token: str = Form(...), refresh_token: str = Form(...),
folder_path: str = Form(None) folder_path: str = Form(None),
): ):
""" """
Save Dropbox settings to the .env file Save Dropbox settings to the .env file
@@ -215,8 +196,7 @@ async def save_dropbox_settings(
if not os.path.exists(env_path): if not os.path.exists(env_path):
logger.error(f".env file not found at {env_path}") logger.error(f".env file not found at {env_path}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Could not find .env file to update"
detail="Could not find .env file to update"
) )
logger.info(f"Updating Dropbox settings in {env_path}") logger.info(f"Updating Dropbox settings in {env_path}")
@@ -276,16 +256,12 @@ async def save_dropbox_settings(
logger.info("Successfully updated Dropbox settings") logger.info("Successfully updated Dropbox settings")
return { return {"status": "success", "message": "Dropbox settings have been saved"}
"status": "success",
"message": "Dropbox settings have been saved"
}
except HTTPException: except HTTPException:
raise raise
except Exception as e: except Exception as e:
logger.exception(f"Unexpected error saving Dropbox settings: {str(e)}") logger.exception(f"Unexpected error saving Dropbox settings: {str(e)}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to save Dropbox settings: {str(e)}"
detail=f"Failed to save Dropbox settings: {str(e)}"
) )
+59 -82
View File
@@ -1,13 +1,12 @@
""" """
Google Drive API endpoints Google Drive API endpoints
""" """
from fastapi import APIRouter, Request, HTTPException, status, Form from fastapi import APIRouter, Request, HTTPException, status, Form
import logging import logging
import os import os
import requests
import json
from typing import Optional from typing import Optional
from datetime import datetime, timedelta from datetime import datetime
from app.auth import require_login from app.auth import require_login
from app.config import settings from app.config import settings
@@ -18,6 +17,7 @@ logger = logging.getLogger(__name__)
router = APIRouter() router = APIRouter()
@router.post("/google-drive/exchange-token") @router.post("/google-drive/exchange-token")
@require_login @require_login
async def exchange_google_drive_token( async def exchange_google_drive_token(
@@ -26,7 +26,7 @@ async def exchange_google_drive_token(
client_secret: str = Form(...), client_secret: str = Form(...),
redirect_uri: str = Form(...), redirect_uri: str = Form(...),
code: str = Form(...), code: str = Form(...),
folder_id: Optional[str] = Form(None) folder_id: Optional[str] = Form(None),
): ):
""" """
Exchange an authorization code for refresh and access tokens from Google. Exchange an authorization code for refresh and access tokens from Google.
@@ -36,27 +36,24 @@ async def exchange_google_drive_token(
token_url = "https://oauth2.googleapis.com/token" token_url = "https://oauth2.googleapis.com/token"
payload = { payload = {
'client_id': client_id, "client_id": client_id,
'client_secret': client_secret, "client_secret": client_secret,
'code': code, "code": code,
'redirect_uri': redirect_uri, "redirect_uri": redirect_uri,
'grant_type': 'authorization_code' "grant_type": "authorization_code",
} }
# Use shared OAuth helper (handles secure logging and error handling) # Use shared OAuth helper (handles secure logging and error handling)
token_data = exchange_oauth_token( token_data = exchange_oauth_token(provider_name="Google Drive", token_url=token_url, payload=payload)
provider_name="Google Drive",
token_url=token_url,
payload=payload
)
# Return just what's needed by the frontend # Return just what's needed by the frontend
return { return {
"refresh_token": token_data["refresh_token"], "refresh_token": token_data["refresh_token"],
"access_token": token_data["access_token"], "access_token": token_data["access_token"],
"expires_in": token_data.get("expires_in", 3600) "expires_in": token_data.get("expires_in", 3600),
} }
@router.post("/google-drive/update-settings") @router.post("/google-drive/update-settings")
@require_login @require_login
async def update_google_drive_settings( async def update_google_drive_settings(
@@ -65,7 +62,7 @@ async def update_google_drive_settings(
client_secret: str = Form(None), client_secret: str = Form(None),
refresh_token: str = Form(...), refresh_token: str = Form(...),
folder_id: str = Form(None), folder_id: str = Form(None),
use_oauth: str = Form("true") use_oauth: str = Form("true"),
): ):
""" """
Update Google Drive settings in memory Update Google Drive settings in memory
@@ -97,18 +94,16 @@ async def update_google_drive_settings(
settings.google_drive_use_oauth = use_oauth_bool settings.google_drive_use_oauth = use_oauth_bool
logger.info(f"Updated GOOGLE_DRIVE_USE_OAUTH in memory to {use_oauth_bool}") logger.info(f"Updated GOOGLE_DRIVE_USE_OAUTH in memory to {use_oauth_bool}")
return { return {"status": "success", "message": "Google Drive settings have been updated in memory"}
"status": "success",
"message": "Google Drive settings have been updated in memory"
}
except Exception as e: except Exception as e:
logger.exception(f"Unexpected error updating Google Drive settings: {str(e)}") logger.exception(f"Unexpected error updating Google Drive settings: {str(e)}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to update Google Drive settings: {str(e)}" detail=f"Failed to update Google Drive settings: {str(e)}",
) )
@router.get("/google-drive/test-token") @router.get("/google-drive/test-token")
@require_login @require_login
async def test_google_drive_token(request: Request): async def test_google_drive_token(request: Request):
@@ -122,15 +117,14 @@ async def test_google_drive_token(request: Request):
logger.info("Testing Google Drive token validity") logger.info("Testing Google Drive token validity")
# Check if OAuth is enabled and configured # Check if OAuth is enabled and configured
if getattr(settings, 'google_drive_use_oauth', False): if getattr(settings, "google_drive_use_oauth", False):
if not (settings.google_drive_client_id and if not (
settings.google_drive_client_secret and settings.google_drive_client_id
settings.google_drive_refresh_token): and settings.google_drive_client_secret
and settings.google_drive_refresh_token
):
logger.warning("Google Drive OAuth credentials not fully configured") logger.warning("Google Drive OAuth credentials not fully configured")
return { return {"status": "error", "message": "Google Drive OAuth credentials are not fully configured"}
"status": "error",
"message": "Google Drive OAuth credentials are not fully configured"
}
try: try:
# Test OAuth connection # Test OAuth connection
@@ -145,7 +139,7 @@ async def test_google_drive_token(request: Request):
refresh_token=settings.google_drive_refresh_token, refresh_token=settings.google_drive_refresh_token,
token_uri="https://oauth2.googleapis.com/token", token_uri="https://oauth2.googleapis.com/token",
client_id=settings.google_drive_client_id, client_id=settings.google_drive_client_id,
client_secret=settings.google_drive_client_secret client_secret=settings.google_drive_client_secret,
) )
# Force a refresh to update the token expiration # Force a refresh to update the token expiration
@@ -154,14 +148,14 @@ async def test_google_drive_token(request: Request):
# Get token expiration info # Get token expiration info
expiration_info = {} expiration_info = {}
if hasattr(credentials, 'expiry') and credentials.expiry: if hasattr(credentials, "expiry") and credentials.expiry:
now = datetime.now() now = datetime.now()
expiry = credentials.expiry expiry = credentials.expiry
time_left = expiry - now time_left = expiry - now
expiration_info = { expiration_info = {
"expires_at": expiry.isoformat(), "expires_at": expiry.isoformat(),
"expires_in_seconds": max(0, int(time_left.total_seconds())), "expires_in_seconds": max(0, int(time_left.total_seconds())),
"expires_in_human": format_time_remaining(time_left) "expires_in_human": format_time_remaining(time_left),
} }
# Test basic API operation # Test basic API operation
@@ -175,7 +169,7 @@ async def test_google_drive_token(request: Request):
"message": f"OAuth token is valid! Connected as {user_email}", "message": f"OAuth token is valid! Connected as {user_email}",
"account": user_email, "account": user_email,
"auth_type": "oauth", "auth_type": "oauth",
"token_info": expiration_info "token_info": expiration_info,
} }
except Exception as e: except Exception as e:
error_msg = str(e) error_msg = str(e)
@@ -186,20 +180,14 @@ async def test_google_drive_token(request: Request):
return { return {
"status": "error", "status": "error",
"message": f"OAuth token validation failed: {error_msg}", "message": f"OAuth token validation failed: {error_msg}",
"needs_reauth": True "needs_reauth": True,
} }
return { return {"status": "error", "message": f"Connection error: {error_msg}"}
"status": "error",
"message": f"Connection error: {error_msg}"
}
else: else:
# Test service account connection # Test service account connection
if not settings.google_drive_credentials_json: if not settings.google_drive_credentials_json:
logger.warning("Google Drive service account credentials not configured") logger.warning("Google Drive service account credentials not configured")
return { return {"status": "error", "message": "Google Drive service account credentials are not configured"}
"status": "error",
"message": "Google Drive service account credentials are not configured"
}
try: try:
service = get_google_drive_service() service = get_google_drive_service()
@@ -207,7 +195,7 @@ async def test_google_drive_token(request: Request):
# For service accounts, try to show the delegated user if available # For service accounts, try to show the delegated user if available
user_email = about.get("user", {}).get("emailAddress", "Unknown") user_email = about.get("user", {}).get("emailAddress", "Unknown")
delegated_user = getattr(settings, 'google_drive_delegate_to', None) delegated_user = getattr(settings, "google_drive_delegate_to", None)
if delegated_user: if delegated_user:
user_display = f"{user_email} (delegating as {delegated_user})" user_display = f"{user_email} (delegating as {delegated_user})"
@@ -220,22 +208,17 @@ async def test_google_drive_token(request: Request):
"status": "success", "status": "success",
"message": f"Service account is valid! Connected as {user_display}", "message": f"Service account is valid! Connected as {user_display}",
"account": user_email, "account": user_email,
"auth_type": "service_account" "auth_type": "service_account",
} }
except Exception as e: except Exception as e:
error_msg = str(e) error_msg = str(e)
logger.error(f"Google Drive service account test failed: {error_msg}") logger.error(f"Google Drive service account test failed: {error_msg}")
return { return {"status": "error", "message": f"Service account validation failed: {error_msg}"}
"status": "error",
"message": f"Service account validation failed: {error_msg}"
}
except Exception as e: except Exception as e:
logger.exception("Unexpected error testing Google Drive token") logger.exception("Unexpected error testing Google Drive token")
return { return {"status": "error", "message": f"Unexpected error: {str(e)}"}
"status": "error",
"message": f"Unexpected error: {str(e)}"
}
@router.get("/google-drive/get-token-info") @router.get("/google-drive/get-token-info")
@require_login @require_login
@@ -249,21 +232,20 @@ async def get_google_drive_token_info(request: Request):
logger.info("Getting Google Drive token information") logger.info("Getting Google Drive token information")
# Check if OAuth is enabled and configured # Check if OAuth is enabled and configured
if not getattr(settings, 'google_drive_use_oauth', False): if not getattr(settings, "google_drive_use_oauth", False):
logger.warning("OAuth is not enabled, using service account instead") logger.warning("OAuth is not enabled, using service account instead")
return { return {
"status": "error", "status": "error",
"message": "OAuth is not enabled. Service accounts don't support user-facing features like folder picker." "message": "OAuth is not enabled. Service accounts don't support user-facing features.",
} }
if not (settings.google_drive_client_id and if not (
settings.google_drive_client_secret and settings.google_drive_client_id
settings.google_drive_refresh_token): and settings.google_drive_client_secret
and settings.google_drive_refresh_token
):
logger.warning("Google Drive OAuth credentials not fully configured") logger.warning("Google Drive OAuth credentials not fully configured")
return { return {"status": "error", "message": "Google Drive OAuth credentials are not fully configured"}
"status": "error",
"message": "Google Drive OAuth credentials are not fully configured"
}
try: try:
# Get credentials and access token # Get credentials and access token
@@ -275,7 +257,7 @@ async def get_google_drive_token_info(request: Request):
refresh_token=settings.google_drive_refresh_token, refresh_token=settings.google_drive_refresh_token,
token_uri="https://oauth2.googleapis.com/token", token_uri="https://oauth2.googleapis.com/token",
client_id=settings.google_drive_client_id, client_id=settings.google_drive_client_id,
client_secret=settings.google_drive_client_secret client_secret=settings.google_drive_client_secret,
) )
# Force a refresh to get a fresh access token # Force a refresh to get a fresh access token
@@ -284,14 +266,14 @@ async def get_google_drive_token_info(request: Request):
# Get token expiration info # Get token expiration info
expiration_info = {} expiration_info = {}
if hasattr(credentials, 'expiry') and credentials.expiry: if hasattr(credentials, "expiry") and credentials.expiry:
now = datetime.now() now = datetime.now()
expiry = credentials.expiry expiry = credentials.expiry
time_left = expiry - now time_left = expiry - now
expiration_info = { expiration_info = {
"expires_at": expiry.isoformat(), "expires_at": expiry.isoformat(),
"expires_in_seconds": max(0, int(time_left.total_seconds())), "expires_in_seconds": max(0, int(time_left.total_seconds())),
"expires_in_human": format_time_remaining(time_left) "expires_in_human": format_time_remaining(time_left),
} }
# Return the token info # Return the token info
@@ -301,7 +283,7 @@ async def get_google_drive_token_info(request: Request):
"status": "success", "status": "success",
"message": "Access token successfully retrieved", "message": "Access token successfully retrieved",
"access_token": credentials.token, "access_token": credentials.token,
"token_info": expiration_info "token_info": expiration_info,
} }
except Exception as e: except Exception as e:
error_msg = str(e) error_msg = str(e)
@@ -312,19 +294,14 @@ async def get_google_drive_token_info(request: Request):
return { return {
"status": "error", "status": "error",
"message": f"OAuth token retrieval failed: {error_msg}", "message": f"OAuth token retrieval failed: {error_msg}",
"needs_reauth": True "needs_reauth": True,
} }
return { return {"status": "error", "message": f"Token retrieval error: {error_msg}"}
"status": "error",
"message": f"Token retrieval error: {error_msg}"
}
except Exception as e: except Exception as e:
logger.exception("Unexpected error getting Google Drive token info") logger.exception("Unexpected error getting Google Drive token info")
return { return {"status": "error", "message": f"Unexpected error: {str(e)}"}
"status": "error",
"message": f"Unexpected error: {str(e)}"
}
def format_time_remaining(time_delta): def format_time_remaining(time_delta):
"""Format a timedelta into a human-readable string.""" """Format a timedelta into a human-readable string."""
@@ -345,6 +322,7 @@ def format_time_remaining(time_delta):
return ", ".join(parts) return ", ".join(parts)
@router.post("/google-drive/save-settings") @router.post("/google-drive/save-settings")
@require_login @require_login
async def save_dropbox_settings( async def save_dropbox_settings(
@@ -353,7 +331,7 @@ async def save_dropbox_settings(
client_secret: str = Form(None), client_secret: str = Form(None),
refresh_token: str = Form(...), refresh_token: str = Form(...),
folder_id: str = Form(None), folder_id: str = Form(None),
use_oauth: str = Form("true") use_oauth: str = Form("true"),
): ):
""" """
Save Google Drive settings to the .env file Save Google Drive settings to the .env file
@@ -366,9 +344,7 @@ async def save_dropbox_settings(
use_oauth_bool = use_oauth.lower() in ("true", "1", "yes", "y", "t") use_oauth_bool = use_oauth.lower() in ("true", "1", "yes", "y", "t")
# Define settings to update # Define settings to update
drive_settings = { drive_settings = {"GOOGLE_DRIVE_USE_OAUTH": str(use_oauth_bool).lower()}
"GOOGLE_DRIVE_USE_OAUTH": str(use_oauth_bool).lower()
}
# Only update these if provided # Only update these if provided
if use_oauth_bool: if use_oauth_bool:
@@ -422,7 +398,9 @@ async def save_dropbox_settings(
except Exception as e: except Exception as e:
logger.warning(f"Failed to update .env file: {str(e)}, but will continue with in-memory update") logger.warning(f"Failed to update .env file: {str(e)}, but will continue with in-memory update")
else: else:
logger.warning(f".env file not found at {env_path}, skipping file update but continuing with in-memory update") logger.warning(
f".env file not found at {env_path}, skipping file update but continuing with in-memory update"
)
# Update the settings in memory (this always happens) # Update the settings in memory (this always happens)
if refresh_token: if refresh_token:
@@ -442,12 +420,11 @@ async def save_dropbox_settings(
return { return {
"status": "success", "status": "success",
"message": "Google Drive settings have been saved", "message": "Google Drive settings have been saved",
"in_memory_only": not os.path.exists(env_path) "in_memory_only": not os.path.exists(env_path),
} }
except Exception as e: except Exception as e:
logger.exception(f"Unexpected error saving Google Drive settings: {str(e)}") logger.exception(f"Unexpected error saving Google Drive settings: {str(e)}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to save Google Drive settings: {str(e)}"
detail=f"Failed to save Google Drive settings: {str(e)}"
) )
+51 -75
View File
@@ -1,13 +1,12 @@
""" """
OneDrive API endpoints OneDrive API endpoints
""" """
from fastapi import APIRouter, Request, HTTPException, status, Form from fastapi import APIRouter, Request, HTTPException, status, Form
import logging import logging
import os import os
import requests import requests
import json
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Optional
from app.auth import require_login from app.auth import require_login
from app.config import settings from app.config import settings
@@ -18,6 +17,7 @@ logger = logging.getLogger(__name__)
router = APIRouter() router = APIRouter()
@router.post("/onedrive/exchange-token") @router.post("/onedrive/exchange-token")
@require_login @require_login
async def exchange_onedrive_token( async def exchange_onedrive_token(
@@ -26,7 +26,7 @@ async def exchange_onedrive_token(
client_secret: str = Form(...), client_secret: str = Form(...),
redirect_uri: str = Form(...), redirect_uri: str = Form(...),
code: str = Form(...), code: str = Form(...),
tenant_id: str = Form(...) tenant_id: str = Form(...),
): ):
""" """
Exchange an authorization code for a refresh token. Exchange an authorization code for a refresh token.
@@ -36,26 +36,20 @@ async def exchange_onedrive_token(
token_url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token" token_url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token"
payload = { payload = {
'client_id': client_id, "client_id": client_id,
'scope': 'https://graph.microsoft.com/.default offline_access', "scope": "https://graph.microsoft.com/.default offline_access",
'code': code, "code": code,
'redirect_uri': redirect_uri, "redirect_uri": redirect_uri,
'grant_type': 'authorization_code', "grant_type": "authorization_code",
'client_secret': client_secret "client_secret": client_secret,
} }
# Use shared OAuth helper (handles secure logging and error handling) # Use shared OAuth helper (handles secure logging and error handling)
token_data = exchange_oauth_token( token_data = exchange_oauth_token(provider_name="OneDrive", token_url=token_url, payload=payload)
provider_name="OneDrive",
token_url=token_url,
payload=payload
)
# Return just what's needed by the frontend # Return just what's needed by the frontend
return { return {"refresh_token": token_data["refresh_token"], "expires_in": token_data.get("expires_in", 3600)}
"refresh_token": token_data["refresh_token"],
"expires_in": token_data.get("expires_in", 3600)
}
@router.get("/onedrive/test-token") @router.get("/onedrive/test-token")
@require_login @require_login
@@ -66,12 +60,13 @@ async def test_onedrive_token(request: Request):
try: try:
logger.info("Testing OneDrive token validity") logger.info("Testing OneDrive token validity")
if not settings.onedrive_refresh_token or not settings.onedrive_client_id or not settings.onedrive_client_secret: if (
not settings.onedrive_refresh_token
or not settings.onedrive_client_id
or not settings.onedrive_client_secret
):
logger.warning("OneDrive credentials not fully configured") logger.warning("OneDrive credentials not fully configured")
return { return {"status": "error", "message": "OneDrive credentials are not fully configured"}
"status": "error",
"message": "OneDrive credentials are not fully configured"
}
# Refresh token to get a new access token and expiration info # Refresh token to get a new access token and expiration info
tenant_id = settings.onedrive_tenant_id or "common" tenant_id = settings.onedrive_tenant_id or "common"
@@ -82,18 +77,14 @@ async def test_onedrive_token(request: Request):
"client_secret": settings.onedrive_client_secret, "client_secret": settings.onedrive_client_secret,
"refresh_token": settings.onedrive_refresh_token, "refresh_token": settings.onedrive_refresh_token,
"grant_type": "refresh_token", "grant_type": "refresh_token",
"scope": "offline_access Files.ReadWrite" "scope": "offline_access Files.ReadWrite",
} }
response = requests.post(token_url, data=refresh_data, timeout=settings.http_request_timeout) response = requests.post(token_url, data=refresh_data, timeout=settings.http_request_timeout)
if response.status_code != 200: if response.status_code != 200:
logger.error(f"Failed to refresh OneDrive token: {response.text}") logger.error(f"Failed to refresh OneDrive token: {response.text}")
return { return {"status": "error", "message": "Refresh token has expired or is invalid", "needs_reauth": True}
"status": "error",
"message": "Refresh token has expired or is invalid",
"needs_reauth": True
}
token_data = response.json() token_data = response.json()
access_token = token_data.get("access_token") access_token = token_data.get("access_token")
@@ -145,7 +136,7 @@ async def test_onedrive_token(request: Request):
logger.error(f"OneDrive token test failed: {user_response.status_code} {user_response.text}") logger.error(f"OneDrive token test failed: {user_response.status_code} {user_response.text}")
return { return {
"status": "error", "status": "error",
"message": f"Token validation failed with status {user_response.status_code}: {user_response.text}" "message": f"Token validation failed with status {user_response.status_code}: {user_response.text}",
} }
# Get user info # Get user info
@@ -163,25 +154,23 @@ async def test_onedrive_token(request: Request):
"expires_at": expiry_time.isoformat(), "expires_at": expiry_time.isoformat(),
"expires_in_seconds": expires_in, "expires_in_seconds": expires_in,
"expires_in_human": format_time_remaining(time_left), "expires_in_human": format_time_remaining(time_left),
"refresh_token_validity": "Refresh token is valid for 90 days of inactivity" "refresh_token_validity": "Refresh token is valid for 90 days of inactivity",
} }
logger.info(f"Successfully connected to OneDrive as {email}") logger.info(f"Successfully connected to OneDrive as {email}")
return { return {
"status": "success", "status": "success",
"message": f"OneDrive connection successful", "message": "OneDrive connection successful",
"account": email, "account": email,
"account_name": display_name, "account_name": display_name,
"token_info": token_info "token_info": token_info,
} }
except Exception as e: except Exception as e:
logger.exception(f"Unexpected error testing OneDrive token: {str(e)}") logger.exception(f"Unexpected error testing OneDrive token: {str(e)}")
return { return {"status": "error", "message": f"Connection error: {str(e)}"}
"status": "error",
"message": f"Connection error: {str(e)}"
}
def format_time_remaining(time_delta): def format_time_remaining(time_delta):
"""Format a timedelta into a human-readable string.""" """Format a timedelta into a human-readable string."""
@@ -202,6 +191,7 @@ def format_time_remaining(time_delta):
return ", ".join(parts) return ", ".join(parts)
@router.post("/onedrive/save-settings") @router.post("/onedrive/save-settings")
@require_login @require_login
async def save_onedrive_settings( async def save_onedrive_settings(
@@ -210,7 +200,7 @@ async def save_onedrive_settings(
client_secret: str = Form(None), client_secret: str = Form(None),
refresh_token: str = Form(...), refresh_token: str = Form(...),
tenant_id: str = Form("common"), tenant_id: str = Form("common"),
folder_path: str = Form(None) folder_path: str = Form(None),
): ):
""" """
Save OneDrive settings to the .env file Save OneDrive settings to the .env file
@@ -222,8 +212,7 @@ async def save_onedrive_settings(
if not os.path.exists(env_path): if not os.path.exists(env_path):
logger.error(f".env file not found at {env_path}") logger.error(f".env file not found at {env_path}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Could not find .env file to update"
detail="Could not find .env file to update"
) )
logger.info(f"Updating OneDrive settings in {env_path}") logger.info(f"Updating OneDrive settings in {env_path}")
@@ -287,20 +276,17 @@ async def save_onedrive_settings(
logger.info("Successfully updated OneDrive settings") logger.info("Successfully updated OneDrive settings")
return { return {"status": "success", "message": "OneDrive settings have been saved"}
"status": "success",
"message": "OneDrive settings have been saved"
}
except HTTPException: except HTTPException:
raise raise
except Exception as e: except Exception as e:
logger.exception(f"Unexpected error saving OneDrive settings: {str(e)}") logger.exception(f"Unexpected error saving OneDrive settings: {str(e)}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to save OneDrive settings: {str(e)}"
detail=f"Failed to save OneDrive settings: {str(e)}"
) )
@router.post("/onedrive/update-settings") @router.post("/onedrive/update-settings")
@require_login @require_login
async def update_onedrive_settings( async def update_onedrive_settings(
@@ -309,7 +295,7 @@ async def update_onedrive_settings(
client_secret: str = Form(None), client_secret: str = Form(None),
refresh_token: str = Form(...), refresh_token: str = Form(...),
tenant_id: str = Form("common"), tenant_id: str = Form("common"),
folder_path: str = Form(None) folder_path: str = Form(None),
): ):
""" """
Update OneDrive settings in memory (without modifying .env file) Update OneDrive settings in memory (without modifying .env file)
@@ -341,27 +327,22 @@ async def update_onedrive_settings(
# Test the token to make sure it works # Test the token to make sure it works
try: try:
from app.tasks.upload_to_onedrive import get_onedrive_token from app.tasks.upload_to_onedrive import get_onedrive_token
access_token = get_onedrive_token()
get_onedrive_token() # Test that token can be retrieved
logger.info("Successfully tested OneDrive token") logger.info("Successfully tested OneDrive token")
except Exception as e: except Exception as e:
logger.error(f"Token test failed after updating settings: {str(e)}") logger.error(f"Token test failed after updating settings: {str(e)}")
return { return {"status": "warning", "message": "Settings updated but token test failed: " + str(e)}
"status": "warning",
"message": "Settings updated but token test failed: " + str(e)
}
return { return {"status": "success", "message": "OneDrive settings have been updated in memory"}
"status": "success",
"message": "OneDrive settings have been updated in memory"
}
except Exception as e: except Exception as e:
logger.exception(f"Unexpected error updating OneDrive settings: {str(e)}") logger.exception(f"Unexpected error updating OneDrive settings: {str(e)}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to update OneDrive settings: {str(e)}"
detail=f"Failed to update OneDrive settings: {str(e)}"
) )
@router.get("/onedrive/get-full-config") @router.get("/onedrive/get-full-config")
@require_login @require_login
async def get_onedrive_full_config(request: Request): async def get_onedrive_full_config(request: Request):
@@ -375,26 +356,21 @@ async def get_onedrive_full_config(request: Request):
"client_secret": settings.onedrive_client_secret or "", "client_secret": settings.onedrive_client_secret or "",
"tenant_id": settings.onedrive_tenant_id or "common", "tenant_id": settings.onedrive_tenant_id or "common",
"refresh_token": settings.onedrive_refresh_token or "", "refresh_token": settings.onedrive_refresh_token or "",
"folder_path": settings.onedrive_folder_path or "Documents/Uploads" "folder_path": settings.onedrive_folder_path or "Documents/Uploads",
} }
# Generate environment variable format # Generate environment variable format
env_format = "\n".join([ env_format = "\n".join(
f"ONEDRIVE_CLIENT_ID={config['client_id']}", [
f"ONEDRIVE_CLIENT_SECRET={config['client_secret']}", f"ONEDRIVE_CLIENT_ID={config['client_id']}",
f"ONEDRIVE_TENANT_ID={config['tenant_id']}", f"ONEDRIVE_CLIENT_SECRET={config['client_secret']}",
f"ONEDRIVE_REFRESH_TOKEN={config['refresh_token']}", f"ONEDRIVE_TENANT_ID={config['tenant_id']}",
f"ONEDRIVE_FOLDER_PATH={config['folder_path']}" f"ONEDRIVE_REFRESH_TOKEN={config['refresh_token']}",
]) f"ONEDRIVE_FOLDER_PATH={config['folder_path']}",
]
)
return { return {"status": "success", "config": config, "env_format": env_format}
"status": "success",
"config": config,
"env_format": env_format
}
except Exception as e: except Exception as e:
logger.exception("Error getting OneDrive configuration") logger.exception("Error getting OneDrive configuration")
return { return {"status": "error", "message": str(e)}
"status": "error",
"message": str(e)
}
+10 -38
View File
@@ -2,16 +2,14 @@
import os import os
import subprocess import subprocess
import json
import tempfile
import logging import logging
from pathlib import Path
from app.config import settings from app.config import settings
from app.tasks.retry_config import BaseTaskWithRetry from app.tasks.retry_config import BaseTaskWithRetry
from app.celery_app import celery from app.celery_app import celery
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@celery.task(base=BaseTaskWithRetry) @celery.task(base=BaseTaskWithRetry)
def upload_with_rclone(file_path: str, destination: str): def upload_with_rclone(file_path: str, destination: str):
""" """
@@ -36,7 +34,7 @@ def upload_with_rclone(file_path: str, destination: str):
remote, remote_path = destination.split(":", 1) remote, remote_path = destination.split(":", 1)
# Validate remote name (alphanumeric, underscore, hyphen only) # Validate remote name (alphanumeric, underscore, hyphen only)
if not remote or not all(c.isalnum() or c in ('_', '-') for c in remote): if not remote or not all(c.isalnum() or c in ("_", "-") for c in remote):
raise ValueError(f"Invalid remote name: {remote}") raise ValueError(f"Invalid remote name: {remote}")
# Check if rclone is installed and config exists # Check if rclone is installed and config exists
@@ -48,24 +46,12 @@ def upload_with_rclone(file_path: str, destination: str):
try: try:
# Ensure the remote path exists (create folders if needed) # Ensure the remote path exists (create folders if needed)
mkdir_cmd = [ mkdir_cmd = ["rclone", "mkdir", "--config", rclone_config_path, destination]
"rclone",
"mkdir",
"--config", rclone_config_path,
destination
]
subprocess.run(mkdir_cmd, check=True, capture_output=True) subprocess.run(mkdir_cmd, check=True, capture_output=True)
# Construct the upload command # Construct the upload command
upload_cmd = [ upload_cmd = ["rclone", "copy", "--config", rclone_config_path, file_path, destination, "--progress"]
"rclone",
"copy",
"--config", rclone_config_path,
file_path,
destination,
"--progress"
]
# Execute the upload command # Execute the upload command
result = subprocess.run(upload_cmd, check=True, capture_output=True, text=True) result = subprocess.run(upload_cmd, check=True, capture_output=True, text=True)
@@ -74,12 +60,7 @@ def upload_with_rclone(file_path: str, destination: str):
if result.returncode == 0: if result.returncode == 0:
# Try to get a public link if possible # Try to get a public link if possible
try: try:
link_cmd = [ link_cmd = ["rclone", "link", "--config", rclone_config_path, f"{destination}/{filename}"]
"rclone",
"link",
"--config", rclone_config_path,
f"{destination}/{filename}"
]
link_result = subprocess.run(link_cmd, capture_output=True, text=True) link_result = subprocess.run(link_cmd, capture_output=True, text=True)
public_url = link_result.stdout.strip() if link_result.returncode == 0 else None public_url = link_result.stdout.strip() if link_result.returncode == 0 else None
except (subprocess.SubprocessError, OSError) as e: except (subprocess.SubprocessError, OSError) as e:
@@ -87,12 +68,7 @@ def upload_with_rclone(file_path: str, destination: str):
public_url = None public_url = None
logger.info(f"Successfully uploaded {filename} to {destination}") logger.info(f"Successfully uploaded {filename} to {destination}")
return { return {"status": "Completed", "file": file_path, "destination": destination, "public_url": public_url}
"status": "Completed",
"file": file_path,
"destination": destination,
"public_url": public_url
}
else: else:
error_msg = f"Failed to upload {filename} to {destination}: {result.stderr}" error_msg = f"Failed to upload {filename} to {destination}: {result.stderr}"
logger.error(error_msg) logger.error(error_msg)
@@ -140,7 +116,7 @@ def send_to_all_rclone_destinations(file_path: str):
# Target directories for each remote (from settings) # Target directories for each remote (from settings)
remote_paths = {} remote_paths = {}
for remote in remotes: for remote in remotes:
remote_name = remote.rstrip(':') remote_name = remote.rstrip(":")
path_setting_name = f"rclone_{remote_name}_path" path_setting_name = f"rclone_{remote_name}_path"
if hasattr(settings, path_setting_name) and getattr(settings, path_setting_name): if hasattr(settings, path_setting_name) and getattr(settings, path_setting_name):
remote_paths[remote] = getattr(settings, path_setting_name) remote_paths[remote] = getattr(settings, path_setting_name)
@@ -152,18 +128,14 @@ def send_to_all_rclone_destinations(file_path: str):
results = {} results = {}
for remote, path in remote_paths.items(): for remote, path in remote_paths.items():
full_destination = f"{remote}{path}" full_destination = f"{remote}{path}"
if path and not path.endswith('/'): if path and not path.endswith("/"):
full_destination += '/' full_destination += "/"
logger.info(f"Queueing {file_path} for upload to {full_destination}") logger.info(f"Queueing {file_path} for upload to {full_destination}")
task = upload_with_rclone.delay(file_path, full_destination) task = upload_with_rclone.delay(file_path, full_destination)
results[f"rclone_{remote.rstrip(':')}_task_id"] = task.id results[f"rclone_{remote.rstrip(':')}_task_id"] = task.id
return { return {"status": "Queued", "file_path": file_path, "tasks": results}
"status": "Queued",
"file_path": file_path,
"tasks": results
}
else: else:
error_msg = f"Failed to list rclone remotes: {result.stderr}" error_msg = f"Failed to list rclone remotes: {result.stderr}"
logger.error(error_msg) logger.error(error_msg)
+7 -11
View File
@@ -2,8 +2,9 @@
OAuth helper utilities for token exchange operations. OAuth helper utilities for token exchange operations.
Shared across multiple OAuth providers to reduce code duplication. Shared across multiple OAuth providers to reduce code duplication.
""" """
import logging import logging
from typing import Dict, Any, Optional from typing import Dict, Any
import requests import requests
from fastapi import HTTPException, status from fastapi import HTTPException, status
@@ -13,10 +14,7 @@ logger = logging.getLogger(__name__)
def exchange_oauth_token( def exchange_oauth_token(
provider_name: str, provider_name: str, token_url: str, payload: Dict[str, str], timeout: int = None
token_url: str,
payload: Dict[str, str],
timeout: int = None
) -> Dict[str, Any]: ) -> Dict[str, Any]:
""" """
Exchange an authorization code for tokens from an OAuth provider. Exchange an authorization code for tokens from an OAuth provider.
@@ -70,8 +68,7 @@ def exchange_oauth_token(
error_detail = {"error": "Unknown error", "status_code": response.status_code} error_detail = {"error": "Unknown error", "status_code": response.status_code}
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST, detail=f"Token exchange failed: {error_detail}"
detail=f"Token exchange failed: {error_detail}"
) )
# Parse the token response # Parse the token response
@@ -82,7 +79,7 @@ def exchange_oauth_token(
logger.error(f"{provider_name} returned success but no refresh_token found in response") logger.error(f"{provider_name} returned success but no refresh_token found in response")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY, status_code=status.HTTP_502_BAD_GATEWAY,
detail=f"{provider_name} OAuth server returned success but no refresh token was included" detail=f"{provider_name} OAuth server returned success but no refresh token was included",
) )
# Log success with non-sensitive metadata only # Log success with non-sensitive metadata only
@@ -97,11 +94,10 @@ def exchange_oauth_token(
logger.exception(f"Network error during {provider_name} token exchange: {str(e)}") logger.exception(f"Network error during {provider_name} token exchange: {str(e)}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail=f"Failed to connect to {provider_name} OAuth service: {str(e)}" detail=f"Failed to connect to {provider_name} OAuth service: {str(e)}",
) )
except Exception as e: except Exception as e:
logger.exception(f"Unexpected error during {provider_name} token exchange: {str(e)}") logger.exception(f"Unexpected error during {provider_name} token exchange: {str(e)}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to exchange token: {str(e)}"
detail=f"Failed to exchange token: {str(e)}"
) )