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
+12 -11
View File
@@ -1,11 +1,11 @@
"""
Common utilities for API routes
"""
import logging
import os
from pathlib import Path
from sqlalchemy.orm import Session
from fastapi import Depends, HTTPException, status
from fastapi import HTTPException, status
from app.database import SessionLocal
from app.config import settings
@@ -13,6 +13,7 @@ from app.config import settings
# Set up logging
logger = logging.getLogger(__name__)
def get_db():
"""Database dependency injection for routes"""
db = SessionLocal()
@@ -21,22 +22,23 @@ def get_db():
finally:
db.close()
def resolve_file_path(file_path: str, subfolder: str = None) -> str:
"""
Resolves a file path to an absolute path with path traversal protection.
If the path is not absolute, it will be joined with the workdir path.
Optionally, can include a subfolder like 'processed'.
Security: Validates that the resolved path stays within the workdir
to prevent path traversal attacks (e.g., ../../etc/passwd).
Args:
file_path: The file path to resolve
subfolder: Optional subfolder within workdir
Returns:
The validated absolute file path
Raises:
HTTPException: If the path attempts to escape the workdir
"""
@@ -45,13 +47,13 @@ def resolve_file_path(file_path: str, subfolder: str = None) -> str:
base_dir = Path(settings.workdir) / subfolder
else:
base_dir = Path(settings.workdir)
# Resolve the file path
if not os.path.isabs(file_path):
resolved_path = (base_dir / file_path).resolve()
else:
resolved_path = Path(file_path).resolve()
# Ensure the resolved path is within the base directory (path traversal protection)
try:
resolved_path.relative_to(base_dir.resolve())
@@ -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
logger.warning(f"Path traversal attempt detected: {file_path} -> {resolved_path}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid file path: path traversal not allowed"
status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid file path: path traversal not allowed"
)
return str(resolved_path)
+66 -90
View File
@@ -1,13 +1,11 @@
"""
Dropbox API endpoints
"""
from fastapi import APIRouter, Request, HTTPException, status, Form
import logging
import os
import requests
import json
from datetime import datetime, timedelta
from typing import Optional
from app.auth import require_login
from app.config import settings
@@ -18,6 +16,7 @@ logger = logging.getLogger(__name__)
router = APIRouter()
@router.post("/dropbox/exchange-token")
@require_login
async def exchange_dropbox_token(
@@ -26,7 +25,7 @@ async def exchange_dropbox_token(
client_secret: str = Form(...),
redirect_uri: 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.
@@ -34,29 +33,26 @@ async def exchange_dropbox_token(
"""
# Prepare the token request
token_url = "https://api.dropboxapi.com/oauth2/token"
payload = {
'client_id': client_id,
'client_secret': client_secret,
'code': code,
'redirect_uri': redirect_uri,
'grant_type': 'authorization_code'
"client_id": client_id,
"client_secret": client_secret,
"code": code,
"redirect_uri": redirect_uri,
"grant_type": "authorization_code",
}
# Use shared OAuth helper (handles secure logging and error handling)
token_data = exchange_oauth_token(
provider_name="Dropbox",
token_url=token_url,
payload=payload
)
token_data = exchange_oauth_token(provider_name="Dropbox", token_url=token_url, payload=payload)
# Return just what's needed by the frontend
return {
"refresh_token": token_data["refresh_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")
@require_login
async def update_dropbox_settings(
@@ -64,45 +60,42 @@ async def update_dropbox_settings(
app_key: str = Form(None),
app_secret: str = Form(None),
refresh_token: str = Form(...),
folder_path: str = Form(None)
folder_path: str = Form(None),
):
"""
Update Dropbox settings in memory
"""
try:
logger.info("Updating Dropbox settings in memory")
# Update settings in memory
if refresh_token:
settings.dropbox_refresh_token = refresh_token
logger.info("Updated DROPBOX_REFRESH_TOKEN in memory")
if app_key:
settings.dropbox_app_key = app_key
logger.info("Updated DROPBOX_APP_KEY in memory")
if app_secret:
settings.dropbox_app_secret = app_secret
logger.info("Updated DROPBOX_APP_SECRET in memory")
if folder_path:
settings.dropbox_folder = folder_path
logger.info("Updated DROPBOX_FOLDER in memory")
# Test token validity would be here, but we'll skip it for now
return {
"status": "success",
"message": "Dropbox settings have been updated in memory"
}
return {"status": "success", "message": "Dropbox settings have been updated in memory"}
except Exception as e:
logger.exception(f"Unexpected error updating Dropbox settings: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to update Dropbox settings: {str(e)}"
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to update Dropbox settings: {str(e)}"
)
@router.get("/dropbox/test-token")
@require_login
async def test_dropbox_token(request: Request):
@@ -111,90 +104,78 @@ async def test_dropbox_token(request: Request):
"""
try:
logger.info("Testing Dropbox token validity")
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")
return {
"status": "error",
"message": "Dropbox credentials are not fully configured"
}
return {"status": "error", "message": "Dropbox credentials are not fully configured"}
# Check token validity by getting current account info
headers = {"Authorization": f"Bearer {settings.dropbox_refresh_token}"}
response = requests.post(
"https://api.dropboxapi.com/2/users/get_current_account",
headers=headers,
timeout=settings.http_request_timeout
timeout=settings.http_request_timeout,
)
# If token is invalid, try refreshing it
if response.status_code == 401:
logger.info("Dropbox access token invalid or expired, trying to refresh")
# Get a new access token using the refresh token
refresh_url = "https://api.dropbox.com/oauth2/token"
refresh_data = {
"grant_type": "refresh_token",
"refresh_token": settings.dropbox_refresh_token,
"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)
if refresh_response.status_code != 200:
logger.error(f"Failed to refresh Dropbox token: {refresh_response.text}")
return {
"status": "error",
"message": "Refresh token has expired or is invalid",
"needs_reauth": True
}
return {"status": "error", "message": "Refresh token has expired or is invalid", "needs_reauth": True}
token_info = refresh_response.json()
access_token = token_info.get("access_token")
# Try again with the new access token
headers = {"Authorization": f"Bearer {access_token}"}
response = requests.post(
"https://api.dropboxapi.com/2/users/get_current_account",
headers=headers,
timeout=settings.http_request_timeout
timeout=settings.http_request_timeout,
)
if response.status_code != 200:
logger.error(f"Dropbox token test failed: {response.status_code} {response.text}")
return {
"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
account_info = response.json()
account_email = account_info.get("email", "Unknown account")
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
token_info = {
"expires_in_human": "Never expires (perpetual token)",
"is_perpetual": True
}
token_info = {"expires_in_human": "Never expires (perpetual token)", "is_perpetual": True}
logger.info(f"Successfully connected to Dropbox as {account_email}")
return {
"status": "success",
"message": f"Dropbox connection successful",
"message": "Dropbox connection successful",
"account": account_email,
"account_name": account_name,
"token_info": token_info
"token_info": token_info,
}
except Exception as e:
logger.exception(f"Unexpected error testing Dropbox token: {str(e)}")
return {
"status": "error",
"message": f"Connection error: {str(e)}"
}
return {"status": "error", "message": f"Connection error: {str(e)}"}
@router.post("/dropbox/save-settings")
@require_login
@@ -203,7 +184,7 @@ async def save_dropbox_settings(
app_key: str = Form(None),
app_secret: str = Form(None),
refresh_token: str = Form(...),
folder_path: str = Form(None)
folder_path: str = Form(None),
):
"""
Save Dropbox settings to the .env file
@@ -211,25 +192,24 @@ async def save_dropbox_settings(
try:
# Get the path to the .env file
env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), ".env")
if not os.path.exists(env_path):
logger.error(f".env file not found at {env_path}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Could not find .env file to update"
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Could not find .env file to update"
)
logger.info(f"Updating Dropbox settings in {env_path}")
# Read the current .env file
with open(env_path, "r") as f:
env_lines = f.readlines()
# Define settings to update
dropbox_settings = {
"DROPBOX_REFRESH_TOKEN": refresh_token,
}
# Only update these if provided
if app_key:
dropbox_settings["DROPBOX_APP_KEY"] = app_key
@@ -237,7 +217,7 @@ async def save_dropbox_settings(
dropbox_settings["DROPBOX_APP_SECRET"] = app_secret
if folder_path:
dropbox_settings["DROPBOX_FOLDER"] = folder_path
# Process each line and update or add settings
updated = set()
new_env_lines = []
@@ -254,16 +234,16 @@ async def save_dropbox_settings(
break
if not is_updated:
new_env_lines.append(line)
# Add any settings that weren't updated (they weren't in the file)
for key, value in dropbox_settings.items():
if key not in updated:
new_env_lines.append(f"{key}={value}")
# Write the updated .env file
with open(env_path, "w") as f:
f.write("\n".join(new_env_lines) + "\n")
# Update the settings in memory
if refresh_token:
settings.dropbox_refresh_token = refresh_token
@@ -273,19 +253,15 @@ async def save_dropbox_settings(
settings.dropbox_app_secret = app_secret
if folder_path:
settings.dropbox_folder = folder_path
logger.info("Successfully updated Dropbox settings")
return {
"status": "success",
"message": "Dropbox settings have been saved"
}
return {"status": "success", "message": "Dropbox settings have been saved"}
except HTTPException:
raise
except Exception as e:
logger.exception(f"Unexpected error saving Dropbox settings: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to save Dropbox settings: {str(e)}"
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to save Dropbox settings: {str(e)}"
)
+115 -138
View File
@@ -1,13 +1,12 @@
"""
Google Drive API endpoints
"""
from fastapi import APIRouter, Request, HTTPException, status, Form
import logging
import os
import requests
import json
from typing import Optional
from datetime import datetime, timedelta
from datetime import datetime
from app.auth import require_login
from app.config import settings
@@ -18,6 +17,7 @@ logger = logging.getLogger(__name__)
router = APIRouter()
@router.post("/google-drive/exchange-token")
@require_login
async def exchange_google_drive_token(
@@ -26,7 +26,7 @@ async def exchange_google_drive_token(
client_secret: str = Form(...),
redirect_uri: 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.
@@ -34,29 +34,26 @@ async def exchange_google_drive_token(
"""
# Prepare the token request
token_url = "https://oauth2.googleapis.com/token"
payload = {
'client_id': client_id,
'client_secret': client_secret,
'code': code,
'redirect_uri': redirect_uri,
'grant_type': 'authorization_code'
"client_id": client_id,
"client_secret": client_secret,
"code": code,
"redirect_uri": redirect_uri,
"grant_type": "authorization_code",
}
# Use shared OAuth helper (handles secure logging and error handling)
token_data = exchange_oauth_token(
provider_name="Google Drive",
token_url=token_url,
payload=payload
)
token_data = exchange_oauth_token(provider_name="Google Drive", token_url=token_url, payload=payload)
# Return just what's needed by the frontend
return {
"refresh_token": token_data["refresh_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")
@require_login
async def update_google_drive_settings(
@@ -65,50 +62,48 @@ async def update_google_drive_settings(
client_secret: str = Form(None),
refresh_token: str = Form(...),
folder_id: str = Form(None),
use_oauth: str = Form("true")
use_oauth: str = Form("true"),
):
"""
Update Google Drive settings in memory
"""
try:
logger.info("Updating Google Drive settings in memory")
# Convert use_oauth string to boolean
use_oauth_bool = use_oauth.lower() in ("true", "1", "yes", "y", "t")
# Update settings in memory
if refresh_token:
settings.google_drive_refresh_token = refresh_token
logger.info("Updated GOOGLE_DRIVE_REFRESH_TOKEN in memory")
if client_id:
settings.google_drive_client_id = client_id
logger.info("Updated GOOGLE_DRIVE_CLIENT_ID in memory")
if client_secret:
settings.google_drive_client_secret = client_secret
logger.info("Updated GOOGLE_DRIVE_CLIENT_SECRET in memory")
if folder_id:
settings.google_drive_folder_id = folder_id
logger.info("Updated GOOGLE_DRIVE_FOLDER_ID in memory")
# Set the OAuth flag
settings.google_drive_use_oauth = use_oauth_bool
logger.info(f"Updated GOOGLE_DRIVE_USE_OAUTH in memory to {use_oauth_bool}")
return {
"status": "success",
"message": "Google Drive settings have been updated in memory"
}
return {"status": "success", "message": "Google Drive settings have been updated in memory"}
except Exception as e:
logger.exception(f"Unexpected error updating Google Drive settings: {str(e)}")
raise HTTPException(
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")
@require_login
async def test_google_drive_token(request: Request):
@@ -118,124 +113,112 @@ async def test_google_drive_token(request: Request):
"""
try:
from app.tasks.upload_to_google_drive import get_drive_service_oauth, get_google_drive_service
logger.info("Testing Google Drive token validity")
# Check if OAuth is enabled and configured
if getattr(settings, 'google_drive_use_oauth', False):
if not (settings.google_drive_client_id and
settings.google_drive_client_secret and
settings.google_drive_refresh_token):
if getattr(settings, "google_drive_use_oauth", False):
if not (
settings.google_drive_client_id
and settings.google_drive_client_secret
and settings.google_drive_refresh_token
):
logger.warning("Google Drive OAuth credentials not fully configured")
return {
"status": "error",
"message": "Google Drive OAuth credentials are not fully configured"
}
return {"status": "error", "message": "Google Drive OAuth credentials are not fully configured"}
try:
# Test OAuth connection
service = get_drive_service_oauth()
# Get credentials for checking token validity
import google.oauth2.credentials
from google.auth.transport.requests import Request
credentials = google.oauth2.credentials.Credentials(
token=None,
refresh_token=settings.google_drive_refresh_token,
token_uri="https://oauth2.googleapis.com/token",
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
if not credentials.valid:
credentials.refresh(Request())
# Get token expiration info
expiration_info = {}
if hasattr(credentials, 'expiry') and credentials.expiry:
if hasattr(credentials, "expiry") and credentials.expiry:
now = datetime.now()
expiry = credentials.expiry
time_left = expiry - now
expiration_info = {
"expires_at": expiry.isoformat(),
"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
about = service.about().get(fields="user").execute()
user_email = about.get("user", {}).get("emailAddress", "Unknown")
logger.info(f"Successfully connected to Google Drive as {user_email}")
return {
"status": "success",
"message": f"OAuth token is valid! Connected as {user_email}",
"account": user_email,
"auth_type": "oauth",
"token_info": expiration_info
"token_info": expiration_info,
}
except Exception as e:
error_msg = str(e)
logger.error(f"Google Drive OAuth token test failed: {error_msg}")
# Check if this is a token-related error
if "invalid_grant" in error_msg.lower() or "token" in error_msg.lower():
return {
"status": "error",
"message": f"OAuth token validation failed: {error_msg}",
"needs_reauth": True
"needs_reauth": True,
}
return {
"status": "error",
"message": f"Connection error: {error_msg}"
}
return {"status": "error", "message": f"Connection error: {error_msg}"}
else:
# Test service account connection
if not settings.google_drive_credentials_json:
logger.warning("Google Drive service account credentials not configured")
return {
"status": "error",
"message": "Google Drive service account credentials are not configured"
}
return {"status": "error", "message": "Google Drive service account credentials are not configured"}
try:
service = get_google_drive_service()
about = service.about().get(fields="user").execute()
# For service accounts, try to show the delegated user if available
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:
user_display = f"{user_email} (delegating as {delegated_user})"
else:
user_display = user_email
logger.info(f"Successfully connected to Google Drive using service account as {user_display}")
return {
"status": "success",
"message": f"Service account is valid! Connected as {user_display}",
"account": user_email,
"auth_type": "service_account"
"auth_type": "service_account",
}
except Exception as e:
error_msg = str(e)
logger.error(f"Google Drive service account test failed: {error_msg}")
return {
"status": "error",
"message": f"Service account validation failed: {error_msg}"
}
return {"status": "error", "message": f"Service account validation failed: {error_msg}"}
except Exception as e:
logger.exception("Unexpected error testing Google Drive token")
return {
"status": "error",
"message": f"Unexpected error: {str(e)}"
}
return {"status": "error", "message": f"Unexpected error: {str(e)}"}
@router.get("/google-drive/get-token-info")
@require_login
@@ -247,94 +230,88 @@ async def get_google_drive_token_info(request: Request):
"""
try:
logger.info("Getting Google Drive token information")
# 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")
return {
"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
settings.google_drive_client_secret and
settings.google_drive_refresh_token):
if not (
settings.google_drive_client_id
and settings.google_drive_client_secret
and settings.google_drive_refresh_token
):
logger.warning("Google Drive OAuth credentials not fully configured")
return {
"status": "error",
"message": "Google Drive OAuth credentials are not fully configured"
}
return {"status": "error", "message": "Google Drive OAuth credentials are not fully configured"}
try:
# Get credentials and access token
import google.oauth2.credentials
from google.auth.transport.requests import Request
credentials = google.oauth2.credentials.Credentials(
token=None,
refresh_token=settings.google_drive_refresh_token,
token_uri="https://oauth2.googleapis.com/token",
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
if not credentials.valid:
credentials.refresh(Request())
# Get token expiration info
expiration_info = {}
if hasattr(credentials, 'expiry') and credentials.expiry:
if hasattr(credentials, "expiry") and credentials.expiry:
now = datetime.now()
expiry = credentials.expiry
time_left = expiry - now
expiration_info = {
"expires_at": expiry.isoformat(),
"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
logger.info("Successfully retrieved Google Drive access token")
return {
"status": "success",
"message": "Access token successfully retrieved",
"access_token": credentials.token,
"token_info": expiration_info
"token_info": expiration_info,
}
except Exception as e:
error_msg = str(e)
logger.error(f"Failed to get Google Drive token: {error_msg}")
# Check if this is a token-related error
if "invalid_grant" in error_msg.lower() or "token" in error_msg.lower():
return {
"status": "error",
"message": f"OAuth token retrieval failed: {error_msg}",
"needs_reauth": True
"needs_reauth": True,
}
return {
"status": "error",
"message": f"Token retrieval error: {error_msg}"
}
return {"status": "error", "message": f"Token retrieval error: {error_msg}"}
except Exception as e:
logger.exception("Unexpected error getting Google Drive token info")
return {
"status": "error",
"message": f"Unexpected error: {str(e)}"
}
return {"status": "error", "message": f"Unexpected error: {str(e)}"}
def format_time_remaining(time_delta):
"""Format a timedelta into a human-readable string."""
if time_delta.total_seconds() <= 0:
return "Expired"
days = time_delta.days
hours, remainder = divmod(time_delta.seconds, 3600)
minutes, seconds = divmod(remainder, 60)
parts = []
if days > 0:
parts.append(f"{days} day{'s' if days != 1 else ''}")
@@ -342,9 +319,10 @@ def format_time_remaining(time_delta):
parts.append(f"{hours} hour{'s' if hours != 1 else ''}")
if minutes > 0 and days == 0: # Only show minutes if less than a day
parts.append(f"{minutes} minute{'s' if minutes != 1 else ''}")
return ", ".join(parts)
@router.post("/google-drive/save-settings")
@require_login
async def save_dropbox_settings(
@@ -353,7 +331,7 @@ async def save_dropbox_settings(
client_secret: str = Form(None),
refresh_token: str = Form(...),
folder_id: str = Form(None),
use_oauth: str = Form("true")
use_oauth: str = Form("true"),
):
"""
Save Google Drive settings to the .env file
@@ -361,15 +339,13 @@ async def save_dropbox_settings(
try:
# Get the path to the .env file
env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), ".env")
# Convert use_oauth string to boolean
use_oauth_bool = use_oauth.lower() in ("true", "1", "yes", "y", "t")
# Define settings to update
drive_settings = {
"GOOGLE_DRIVE_USE_OAUTH": str(use_oauth_bool).lower()
}
drive_settings = {"GOOGLE_DRIVE_USE_OAUTH": str(use_oauth_bool).lower()}
# Only update these if provided
if use_oauth_bool:
if refresh_token:
@@ -378,20 +354,20 @@ async def save_dropbox_settings(
drive_settings["GOOGLE_DRIVE_CLIENT_ID"] = client_id
if client_secret:
drive_settings["GOOGLE_DRIVE_CLIENT_SECRET"] = client_secret
# Always include folder ID if provided
if folder_id:
drive_settings["GOOGLE_DRIVE_FOLDER_ID"] = folder_id
# Try to update the .env file, but don't fail if it doesn't exist (for Docker containers)
if os.path.exists(env_path):
try:
logger.info(f"Updating Google Drive settings in {env_path}")
# Read the current .env file
with open(env_path, "r") as f:
env_lines = f.readlines()
# Process each line and update or add settings
updated = set()
new_env_lines = []
@@ -408,22 +384,24 @@ async def save_dropbox_settings(
break
if not is_updated:
new_env_lines.append(line)
# Add any settings that weren't updated (they weren't in the file)
for key, value in drive_settings.items():
if key not in updated:
new_env_lines.append(f"{key}={value}")
# Write the updated .env file
with open(env_path, "w") as f:
f.write("\n".join(new_env_lines) + "\n")
logger.info("Successfully updated Google Drive settings in .env file")
except Exception as e:
logger.warning(f"Failed to update .env file: {str(e)}, but will continue with in-memory update")
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)
if refresh_token:
settings.google_drive_refresh_token = refresh_token
@@ -433,21 +411,20 @@ async def save_dropbox_settings(
settings.google_drive_client_secret = client_secret
if folder_id:
settings.google_drive_folder_id = folder_id
# Set OAuth flag
settings.google_drive_use_oauth = use_oauth_bool
logger.info("Successfully updated Google Drive settings in memory")
return {
"status": "success",
"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:
logger.exception(f"Unexpected error saving Google Drive settings: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to save Google Drive settings: {str(e)}"
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to save Google Drive settings: {str(e)}"
)
+103 -127
View File
@@ -1,13 +1,12 @@
"""
OneDrive API endpoints
"""
from fastapi import APIRouter, Request, HTTPException, status, Form
import logging
import os
import requests
import json
from datetime import datetime, timedelta
from typing import Optional
from app.auth import require_login
from app.config import settings
@@ -18,6 +17,7 @@ logger = logging.getLogger(__name__)
router = APIRouter()
@router.post("/onedrive/exchange-token")
@require_login
async def exchange_onedrive_token(
@@ -26,7 +26,7 @@ async def exchange_onedrive_token(
client_secret: str = Form(...),
redirect_uri: str = Form(...),
code: str = Form(...),
tenant_id: str = Form(...)
tenant_id: str = Form(...),
):
"""
Exchange an authorization code for a refresh token.
@@ -34,28 +34,22 @@ async def exchange_onedrive_token(
"""
# Prepare the token request
token_url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token"
payload = {
'client_id': client_id,
'scope': 'https://graph.microsoft.com/.default offline_access',
'code': code,
'redirect_uri': redirect_uri,
'grant_type': 'authorization_code',
'client_secret': client_secret
"client_id": client_id,
"scope": "https://graph.microsoft.com/.default offline_access",
"code": code,
"redirect_uri": redirect_uri,
"grant_type": "authorization_code",
"client_secret": client_secret,
}
# Use shared OAuth helper (handles secure logging and error handling)
token_data = exchange_oauth_token(
provider_name="OneDrive",
token_url=token_url,
payload=payload
)
token_data = exchange_oauth_token(provider_name="OneDrive", token_url=token_url, payload=payload)
# Return just what's needed by the frontend
return {
"refresh_token": token_data["refresh_token"],
"expires_in": token_data.get("expires_in", 3600)
}
return {"refresh_token": token_data["refresh_token"], "expires_in": token_data.get("expires_in", 3600)}
@router.get("/onedrive/test-token")
@require_login
@@ -65,133 +59,128 @@ async def test_onedrive_token(request: Request):
"""
try:
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")
return {
"status": "error",
"message": "OneDrive credentials are not fully configured"
}
return {"status": "error", "message": "OneDrive credentials are not fully configured"}
# Refresh token to get a new access token and expiration info
tenant_id = settings.onedrive_tenant_id or "common"
token_url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token"
refresh_data = {
"client_id": settings.onedrive_client_id,
"client_secret": settings.onedrive_client_secret,
"refresh_token": settings.onedrive_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)
if response.status_code != 200:
logger.error(f"Failed to refresh OneDrive token: {response.text}")
return {
"status": "error",
"message": "Refresh token has expired or is invalid",
"needs_reauth": True
}
return {"status": "error", "message": "Refresh token has expired or is invalid", "needs_reauth": True}
token_data = response.json()
access_token = token_data.get("access_token")
expires_in = token_data.get("expires_in", 3600) # Default to 1 hour if not specified
# Check if we got a new refresh token (Microsoft sometimes issues a new one)
new_refresh_token = token_data.get("refresh_token")
if new_refresh_token and new_refresh_token != settings.onedrive_refresh_token:
logger.info("Received new refresh token from Microsoft - will update configuration")
# Update refresh token in memory
settings.onedrive_refresh_token = new_refresh_token
# Also try to update .env file if it exists
try:
env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), ".env")
if os.path.exists(env_path):
with open(env_path, "r") as f:
env_lines = f.readlines()
updated_lines = []
updated = False
for line in env_lines:
if line.startswith("ONEDRIVE_REFRESH_TOKEN="):
updated_lines.append(f"ONEDRIVE_REFRESH_TOKEN={new_refresh_token}\n")
updated = True
else:
updated_lines.append(line)
if not updated:
updated_lines.append(f"ONEDRIVE_REFRESH_TOKEN={new_refresh_token}\n")
with open(env_path, "w") as f:
f.writelines(updated_lines)
logger.info("Updated refresh token in .env file")
except Exception as e:
logger.warning(f"Failed to update refresh token in .env file: {e}")
# Test the access token by getting user information
user_info_url = "https://graph.microsoft.com/v1.0/me"
headers = {"Authorization": f"Bearer {access_token}"}
user_response = requests.get(user_info_url, headers=headers, timeout=settings.http_request_timeout)
if user_response.status_code != 200:
logger.error(f"OneDrive token test failed: {user_response.status_code} {user_response.text}")
return {
"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
user_info = user_response.json()
display_name = user_info.get("displayName", "Unknown user")
email = user_info.get("userPrincipalName", "Unknown email")
# Calculate expiration time
now = datetime.now()
expiry_time = now + timedelta(seconds=expires_in)
# Format expiration info
time_left = expiry_time - now
token_info = {
"expires_at": expiry_time.isoformat(),
"expires_in_seconds": expires_in,
"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}")
return {
"status": "success",
"message": f"OneDrive connection successful",
"message": "OneDrive connection successful",
"account": email,
"account_name": display_name,
"token_info": token_info
"token_info": token_info,
}
except Exception as e:
logger.exception(f"Unexpected error testing OneDrive token: {str(e)}")
return {
"status": "error",
"message": f"Connection error: {str(e)}"
}
return {"status": "error", "message": f"Connection error: {str(e)}"}
def format_time_remaining(time_delta):
"""Format a timedelta into a human-readable string."""
if time_delta.total_seconds() <= 0:
return "Expired"
days = time_delta.days
hours, remainder = divmod(time_delta.seconds, 3600)
minutes, seconds = divmod(remainder, 60)
parts = []
if days > 0:
parts.append(f"{days} day{'s' if days != 1 else ''}")
@@ -199,9 +188,10 @@ def format_time_remaining(time_delta):
parts.append(f"{hours} hour{'s' if hours != 1 else ''}")
if minutes > 0 and days == 0: # Only show minutes if less than a day
parts.append(f"{minutes} minute{'s' if minutes != 1 else ''}")
return ", ".join(parts)
@router.post("/onedrive/save-settings")
@require_login
async def save_onedrive_settings(
@@ -210,7 +200,7 @@ async def save_onedrive_settings(
client_secret: str = Form(None),
refresh_token: str = Form(...),
tenant_id: str = Form("common"),
folder_path: str = Form(None)
folder_path: str = Form(None),
):
"""
Save OneDrive settings to the .env file
@@ -218,25 +208,24 @@ async def save_onedrive_settings(
try:
# Get the path to the .env file
env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), ".env")
if not os.path.exists(env_path):
logger.error(f".env file not found at {env_path}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Could not find .env file to update"
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Could not find .env file to update"
)
logger.info(f"Updating OneDrive settings in {env_path}")
# Read the current .env file
with open(env_path, "r") as f:
env_lines = f.readlines()
# Define settings to update
onedrive_settings = {
"ONEDRIVE_REFRESH_TOKEN": refresh_token,
}
# Only update these if provided
if client_id:
onedrive_settings["ONEDRIVE_CLIENT_ID"] = client_id
@@ -246,7 +235,7 @@ async def save_onedrive_settings(
onedrive_settings["ONEDRIVE_TENANT_ID"] = tenant_id
if folder_path:
onedrive_settings["ONEDRIVE_FOLDER_PATH"] = folder_path
# Process each line and update or add settings
updated = set()
new_env_lines = []
@@ -263,16 +252,16 @@ async def save_onedrive_settings(
break
if not is_updated:
new_env_lines.append(line)
# Add any settings that weren't updated (they weren't in the file)
for key, value in onedrive_settings.items():
if key not in updated:
new_env_lines.append(f"{key}={value}")
# Write the updated .env file
with open(env_path, "w") as f:
f.write("\n".join(new_env_lines) + "\n")
# Update the settings in memory
if refresh_token:
settings.onedrive_refresh_token = refresh_token
@@ -284,23 +273,20 @@ async def save_onedrive_settings(
settings.onedrive_tenant_id = tenant_id
if folder_path:
settings.onedrive_folder_path = folder_path
logger.info("Successfully updated OneDrive settings")
return {
"status": "success",
"message": "OneDrive settings have been saved"
}
return {"status": "success", "message": "OneDrive settings have been saved"}
except HTTPException:
raise
except Exception as e:
logger.exception(f"Unexpected error saving OneDrive settings: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to save OneDrive settings: {str(e)}"
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to save OneDrive settings: {str(e)}"
)
@router.post("/onedrive/update-settings")
@require_login
async def update_onedrive_settings(
@@ -309,59 +295,54 @@ async def update_onedrive_settings(
client_secret: str = Form(None),
refresh_token: str = Form(...),
tenant_id: str = Form("common"),
folder_path: str = Form(None)
folder_path: str = Form(None),
):
"""
Update OneDrive settings in memory (without modifying .env file)
"""
try:
logger.info("Updating OneDrive settings in memory")
# Update settings in memory
if refresh_token:
settings.onedrive_refresh_token = refresh_token
logger.info("Updated ONEDRIVE_REFRESH_TOKEN in memory")
if client_id:
settings.onedrive_client_id = client_id
logger.info("Updated ONEDRIVE_CLIENT_ID in memory")
if client_secret:
settings.onedrive_client_secret = client_secret
logger.info("Updated ONEDRIVE_CLIENT_SECRET in memory")
if tenant_id:
settings.onedrive_tenant_id = tenant_id
logger.info("Updated ONEDRIVE_TENANT_ID in memory")
if folder_path:
settings.onedrive_folder_path = folder_path
logger.info("Updated ONEDRIVE_FOLDER_PATH in memory")
# Test the token to make sure it works
try:
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")
except Exception as e:
logger.error(f"Token test failed after updating settings: {str(e)}")
return {
"status": "warning",
"message": "Settings updated but token test failed: " + str(e)
}
return {
"status": "success",
"message": "OneDrive settings have been updated in memory"
}
return {"status": "warning", "message": "Settings updated but token test failed: " + str(e)}
return {"status": "success", "message": "OneDrive settings have been updated in memory"}
except Exception as e:
logger.exception(f"Unexpected error updating OneDrive settings: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to update OneDrive settings: {str(e)}"
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to update OneDrive settings: {str(e)}"
)
@router.get("/onedrive/get-full-config")
@require_login
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 "",
"tenant_id": settings.onedrive_tenant_id or "common",
"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
env_format = "\n".join([
f"ONEDRIVE_CLIENT_ID={config['client_id']}",
f"ONEDRIVE_CLIENT_SECRET={config['client_secret']}",
f"ONEDRIVE_TENANT_ID={config['tenant_id']}",
f"ONEDRIVE_REFRESH_TOKEN={config['refresh_token']}",
f"ONEDRIVE_FOLDER_PATH={config['folder_path']}"
])
return {
"status": "success",
"config": config,
"env_format": env_format
}
env_format = "\n".join(
[
f"ONEDRIVE_CLIENT_ID={config['client_id']}",
f"ONEDRIVE_CLIENT_SECRET={config['client_secret']}",
f"ONEDRIVE_TENANT_ID={config['tenant_id']}",
f"ONEDRIVE_REFRESH_TOKEN={config['refresh_token']}",
f"ONEDRIVE_FOLDER_PATH={config['folder_path']}",
]
)
return {"status": "success", "config": config, "env_format": env_format}
except Exception as e:
logger.exception("Error getting OneDrive configuration")
return {
"status": "error",
"message": str(e)
}
return {"status": "error", "message": str(e)}