[UPDT] HORILLA_BACKUP: Gdrive backup from google service to oauth

This commit is contained in:
Horilla
2025-12-23 15:03:04 +05:30
parent f4bedaca4d
commit 6f82c23844
8 changed files with 614 additions and 62 deletions

View File

@@ -9,8 +9,15 @@ class BackupConfig(AppConfig):
from django.urls import include, path
from horilla.urls import urlpatterns
from horilla_backup import views
urlpatterns.append(
path("backup/", include("horilla_backup.urls")),
)
# Add root-level callback URL to match OAuth redirect URI
urlpatterns.append(
path(
"google/callback/", views.gdrive_callback, name="gdrive_callback_root"
),
)
super().ready()

View File

@@ -100,7 +100,12 @@ class GdriveBackupSetupForm(ModelForm):
class Meta:
model = GoogleDriveBackup
exclude = ["active"]
exclude = [
"active",
"access_token",
"refresh_token",
"token_expiry",
] # Exclude token fields
def as_p(self):
"""
@@ -119,32 +124,64 @@ class GdriveBackupSetupForm(ModelForm):
seconds = cleaned_data.get("seconds")
hour = cleaned_data.get("hour")
minute = cleaned_data.get("minute")
service_account_file = cleaned_data.get("service_account_file")
oauth_credentials_file = cleaned_data.get("oauth_credentials_file")
try:
# Read file content from InMemoryUploadedFile or whatever you receive
file_data = service_account_file.read()
file_name = service_account_file.name
# Get instance if updating
instance = self.instance if hasattr(self, "instance") else None
# Save using Django's storage (optional, if you need to persist it later)
if not GoogleDriveBackup.objects.exists():
# Save to storage if no backup exists
relative_path = default_storage.save(file_name, ContentFile(file_data))
# Only validate file if it's provided (new file upload) or if creating new instance
if oauth_credentials_file:
try:
# Read file content from InMemoryUploadedFile
file_data = oauth_credentials_file.read()
file_name = oauth_credentials_file.name
# Always write to temp file for authentication (because .path isn't supported)
with tempfile.NamedTemporaryFile(delete=False, suffix=".json") as tmp_file:
tmp_file.write(file_data)
tmp_file.flush()
temp_path = tmp_file.name
# Always write to temp file for validation (because .path isn't supported)
with tempfile.NamedTemporaryFile(
delete=False, suffix=".json", mode="w+b"
) as tmp_file:
tmp_file.write(file_data)
tmp_file.flush()
temp_path = tmp_file.name
# Authenticate using temp file path
authenticate(temp_path)
# Validate OAuth credentials file format
import json
# Clean up temp file
os.remove(temp_path)
with open(temp_path, "r") as f:
oauth_config = json.load(f)
except Exception as e:
raise forms.ValidationError("Please provide a valid service account file.")
# Check if it's a valid OAuth 2.0 web application credentials file
if "web" not in oauth_config:
raise ValueError(
"OAuth credentials file must contain 'web' key for web application type."
)
if "client_id" not in oauth_config.get("web", {}):
raise ValueError(
"OAuth credentials file must contain 'client_id' in 'web' section."
)
if "client_secret" not in oauth_config.get("web", {}):
raise ValueError(
"OAuth credentials file must contain 'client_secret' in 'web' section."
)
# Clean up temp file
os.remove(temp_path)
except json.JSONDecodeError:
raise forms.ValidationError(
"Please provide a valid OAuth credentials file (must be valid JSON)."
)
except Exception as e:
raise forms.ValidationError(
f"Please provide a valid OAuth credentials file. Error: {str(e)}"
)
elif not instance or not instance.pk:
# If creating new instance and no file provided, raise error
raise forms.ValidationError(
"Please provide a valid OAuth credentials file."
)
if backup_db == False and backup_media == False:
raise forms.ValidationError("Please select any backup option.")
if interval == False and fixed == False:

View File

@@ -1,28 +1,218 @@
import json
import os
from datetime import datetime, timedelta
from google.oauth2 import service_account
from django.utils import timezone
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import Flow
from googleapiclient.discovery import build
from googleapiclient.http import MediaFileUpload
SCOPES = ["https://www.googleapis.com/auth/drive"]
def authenticate(service_account_file):
creds = service_account.Credentials.from_service_account_file(
service_account_file, scopes=SCOPES
def get_credentials_from_model(google_drive_backup):
"""
Get or refresh OAuth credentials from the model.
Returns credentials object or None if authorization is needed.
"""
if not google_drive_backup.access_token:
return None
if not google_drive_backup.oauth_credentials_file or not hasattr(
google_drive_backup.oauth_credentials_file, "path"
):
raise Exception(
"OAuth credentials file not found. Please upload it in the settings."
)
# Load client credentials from file
oauth_file_path = google_drive_backup.oauth_credentials_file.path
with open(oauth_file_path, "r") as f:
client_config = json.load(f)
client_id = client_config["web"]["client_id"]
client_secret = client_config["web"]["client_secret"]
token_uri = client_config["web"]["token_uri"]
creds = Credentials(
token=google_drive_backup.access_token,
refresh_token=google_drive_backup.refresh_token,
token_uri=token_uri,
client_id=client_id,
client_secret=client_secret,
)
# Check if token is expired or about to expire (within 5 minutes)
if google_drive_backup.token_expiry:
expiry_time = google_drive_backup.token_expiry
# Ensure both datetimes are timezone-aware for comparison
now = timezone.now()
if timezone.is_naive(expiry_time):
# If expiry is naive, make it timezone-aware (assume UTC)
expiry_time = timezone.make_aware(expiry_time, timezone.utc)
# Compare timezone-aware datetimes
if now >= expiry_time - timedelta(minutes=5):
# Token expired or about to expire, refresh it
creds = refresh_credentials(google_drive_backup, creds)
return creds
def upload_file(file_path, service_account_file, parent_folder_id):
creds = authenticate(service_account_file)
def refresh_credentials(google_drive_backup, creds):
"""
Refresh OAuth credentials and update the model.
"""
try:
# Load client credentials from file
oauth_file_path = google_drive_backup.oauth_credentials_file.path
with open(oauth_file_path, "r") as f:
client_config = json.load(f)
client_id = client_config["web"]["client_id"]
client_secret = client_config["web"]["client_secret"]
token_uri = client_config["web"]["token_uri"]
# Create new credentials object with client info for refresh
from google.oauth2.credentials import Credentials as OAuthCredentials
refresh_creds = OAuthCredentials(
token=None, # Will be refreshed
refresh_token=creds.refresh_token,
token_uri=token_uri,
client_id=client_id,
client_secret=client_secret,
)
# Refresh the token
refresh_creds.refresh(Request())
# Update model with new tokens
google_drive_backup.access_token = refresh_creds.token
if refresh_creds.refresh_token:
google_drive_backup.refresh_token = refresh_creds.refresh_token
if refresh_creds.expiry:
# Make datetime timezone-aware if it's naive
from django.utils import timezone as tz
if tz.is_naive(refresh_creds.expiry):
google_drive_backup.token_expiry = tz.make_aware(
refresh_creds.expiry, tz.utc
)
else:
google_drive_backup.token_expiry = refresh_creds.expiry
google_drive_backup.save()
return refresh_creds
except Exception as e:
raise Exception(f"Failed to refresh credentials: {str(e)}")
def get_authorization_url(oauth_credentials_file_path, redirect_uri):
"""
Generate OAuth authorization URL.
Returns (authorization_url, flow) tuple.
"""
with open(oauth_credentials_file_path, "r") as f:
client_config = json.load(f)
# Use Flow for web applications
flow = Flow.from_client_config(client_config, SCOPES, redirect_uri=redirect_uri)
authorization_url, state = flow.authorization_url(
access_type="offline", include_granted_scopes="true", prompt="consent"
)
return authorization_url, flow, state
def exchange_code_for_tokens(flow, authorization_response_url):
"""
Exchange authorization code for tokens.
Returns credentials object.
"""
flow.fetch_token(authorization_response_url=authorization_response_url)
return flow.credentials
def authenticate(oauth_credentials_file):
"""
Authenticate using OAuth credentials file.
This is used for validation in forms.
"""
try:
with open(oauth_credentials_file, "r") as f:
client_config = json.load(f)
# Validate that it's a web OAuth credentials file
if "web" not in client_config:
raise ValueError("Invalid OAuth credentials file. Expected 'web' key.")
required_keys = ["client_id", "client_secret", "auth_uri", "token_uri"]
for key in required_keys:
if key not in client_config["web"]:
raise ValueError(f"Missing required key: {key}")
return True
except json.JSONDecodeError:
raise ValueError("Invalid JSON file.")
except Exception as e:
raise ValueError(f"Invalid OAuth credentials file: {str(e)}")
def upload_file(file_path, google_drive_backup, parent_folder_id):
"""
Upload file to Google Drive using OAuth credentials.
"""
if not os.path.exists(file_path):
raise Exception(f"File does not exist: {file_path}")
file_size = os.path.getsize(file_path)
print(
f"Uploading file: {os.path.basename(file_path)} ({file_size} bytes) to folder: {parent_folder_id}"
)
creds = get_credentials_from_model(google_drive_backup)
if not creds or not creds.valid:
raise Exception("OAuth credentials are not valid. Please re-authorize.")
service = build("drive", "v3", credentials=creds)
parent_folder_id = parent_folder_id
file_metadata = {"name": os.path.basename(file_path), "parents": [parent_folder_id]}
media = MediaFileUpload(file_path, resumable=True)
file = (
service.files()
.create(body=file_metadata, media_body=media, fields="id")
.execute()
)
# Use resumable upload for large files (>5MB)
# For smaller files, regular upload is fine
if file_size > 5 * 1024 * 1024: # 5MB
print("Using resumable upload for large file")
media = MediaFileUpload(file_path, resumable=True, chunksize=1024 * 1024)
else:
print("Using regular upload for small file")
media = MediaFileUpload(file_path, resumable=False)
try:
file = (
service.files()
.create(body=file_metadata, media_body=media, fields="id")
.execute()
)
file_id = file.get("id")
print(f"File uploaded successfully. Google Drive file ID: {file_id}")
return file_id
except Exception as e:
error_msg = str(e)
print(f"Upload failed: {error_msg}")
# Check for specific errors
if "quota" in error_msg.lower() or "storageQuotaExceeded" in error_msg:
raise Exception(
"Google Drive storage quota exceeded. Please free up space or upgrade your plan."
)
elif "permission" in error_msg.lower() or "forbidden" in error_msg.lower():
raise Exception(
f"Permission denied. Please ensure the authenticated user has write access to folder ID: {parent_folder_id}"
)
else:
raise Exception(f"Upload failed: {error_msg}")

View File

@@ -41,15 +41,26 @@ class LocalBackup(models.Model):
class GoogleDriveBackup(models.Model):
service_account_file = models.FileField(
upload_to="gdrive_service_account_file",
verbose_name="Service Account File",
help_text="Make sure your file is in JSON format and contains your Google Service Account credentials",
oauth_credentials_file = models.FileField(
upload_to="gdrive_oauth_credentials_file",
verbose_name="OAuth Credentials File",
help_text="Make sure your file is in JSON format and contains your Google OAuth 2.0 client credentials (web application type)",
blank=True,
null=True,
)
gdrive_folder_id = models.CharField(
max_length=255,
verbose_name="Gdrive Folder ID",
help_text="Shared Gdrive folder Id with access granted to Gmail service account. Enable full permissions for seamless connection.",
help_text="Google Drive folder ID where backups will be stored. The authenticated user must have write access to this folder.",
)
access_token = models.TextField(
blank=True, null=True, help_text="OAuth access token (automatically managed)"
)
refresh_token = models.TextField(
blank=True, null=True, help_text="OAuth refresh token (automatically managed)"
)
token_expiry = models.DateTimeField(
blank=True, null=True, help_text="Token expiry time (automatically managed)"
)
backup_media = models.BooleanField(blank=True, null=True)
backup_db = models.BooleanField(blank=True, null=True)

View File

@@ -84,24 +84,144 @@ scheduler = BackgroundScheduler()
def google_drive_backup():
if GoogleDriveBackup.objects.exists():
google_drive = GoogleDriveBackup.objects.first()
service_account_file = google_drive.service_account_file.path
gdrive_folder_id = google_drive.gdrive_folder_id
if google_drive.backup_db:
db = settings.DATABASES["default"]
dump_postgres_db(
db_name=db["NAME"],
username=db["USER"],
output_file="backupdb.dump",
password=db["PASSWORD"],
# Check if OAuth tokens exist
if not google_drive.access_token:
# Log error or skip backup if not authorized
print(
"Google Drive backup skipped: OAuth tokens not found. Please authorize the application."
)
upload_file("backupdb.dump", service_account_file, gdrive_folder_id)
os.remove("backupdb.dump")
if google_drive.backup_media:
folder_to_zip = settings.MEDIA_ROOT
output_zip_file = "media.zip"
zip_folder(folder_to_zip, output_zip_file)
upload_file("media.zip", service_account_file, gdrive_folder_id)
os.remove("media.zip")
return
try:
db_backup_success = False
media_backup_success = False
if google_drive.backup_db:
try:
print("=== Starting Database Backup ===")
db = settings.DATABASES["default"]
dump_postgres_db(
db_name=db["NAME"],
username=db["USER"],
output_file="backupdb.dump",
password=db["PASSWORD"],
)
upload_file("backupdb.dump", google_drive, gdrive_folder_id)
os.remove("backupdb.dump")
db_backup_success = True
print("=== Database Backup Completed Successfully ===")
except Exception as db_error:
print(f"Database backup failed: {str(db_error)}")
import traceback
print(traceback.format_exc())
# Clean up dump file if it exists
if os.path.exists("backupdb.dump"):
try:
os.remove("backupdb.dump")
except:
pass
if google_drive.backup_media:
try:
print("=== Starting Media Backup ===")
folder_to_zip = settings.MEDIA_ROOT
output_zip_file = "media.zip"
# Check if media folder exists
if not os.path.exists(folder_to_zip):
raise Exception(f"Media folder does not exist: {folder_to_zip}")
# Check if media folder has any files
media_files = []
for root, dirs, files in os.walk(folder_to_zip):
media_files.extend(files)
if not media_files:
print(
f"Warning: Media folder appears to be empty: {folder_to_zip}"
)
# Still create an empty zip to indicate backup was attempted
print(
f"Zipping media folder: {folder_to_zip} ({len(media_files)} files found)"
)
zip_folder(folder_to_zip, output_zip_file)
# Check if zip file was created
if not os.path.exists(output_zip_file):
raise Exception("Failed to create media.zip file")
zip_size = os.path.getsize(output_zip_file)
print(f"Media zip created successfully. Size: {zip_size} bytes")
print(
f"Uploading media.zip to Google Drive (folder ID: {gdrive_folder_id})..."
)
upload_file(output_zip_file, google_drive, gdrive_folder_id)
print(f"Media backup uploaded successfully to Google Drive")
# Clean up zip file
os.remove(output_zip_file)
print(f"Temporary media.zip file removed")
media_backup_success = True
print("=== Media Backup Completed Successfully ===")
except Exception as media_error:
print(f"=== Media Backup Failed ===")
print(f"Error: {str(media_error)}")
import traceback
print(traceback.format_exc())
# Remove zip file if it exists
if os.path.exists("media.zip"):
try:
os.remove("media.zip")
except:
pass
# Don't re-raise - allow DB backup to continue even if media fails
# Summary
db_enabled = google_drive.backup_db
media_enabled = google_drive.backup_media
if db_enabled and not db_backup_success:
print("WARNING: Database backup failed")
if media_enabled and not media_backup_success:
print("WARNING: Media backup failed")
# Determine overall status
if db_enabled and media_enabled:
# Both enabled
if db_backup_success and media_backup_success:
print("=== Backup Job Completed Successfully ===")
elif db_backup_success or media_backup_success:
print("=== Backup Job Completed (with some failures) ===")
else:
print("=== Backup Job Failed ===")
elif db_enabled:
# Only DB enabled
if db_backup_success:
print("=== Database Backup Completed Successfully ===")
else:
print("=== Database Backup Failed ===")
elif media_enabled:
# Only Media enabled
if media_backup_success:
print("=== Media Backup Completed Successfully ===")
else:
print("=== Media Backup Failed ===")
except Exception as e:
# Log the error - in production you might want to use proper logging
print(f"=== Google Drive Backup Job Failed ===")
print(f"Error: {str(e)}")
import traceback
print(traceback.format_exc())
# Optionally, you could disable the backup if tokens are invalid
# google_drive.active = False
# google_drive.save()
def start_gdrive_backup_job():
@@ -137,7 +257,6 @@ def start_gdrive_backup_job():
# Start the scheduler if it's not already running
if not scheduler.running:
scheduler.start()
else:
stop_gdrive_backup_job()

View File

@@ -13,6 +13,15 @@
</div>
</div>
{% endif %}
{% if needs_auth %}
<div class="oh-wrapper">
<div class="oh-alert-container">
<div class="oh-alert oh-alert--animated oh-alert--warning">
{% trans "OAuth authorization required. Please click 'Authorize Google Drive' to grant access." %}
</div>
</div>
</div>
{% endif %}
<form method="post" action="{% url 'gdrive' %}" enctype="multipart/form-data">
{% csrf_token %}
{{form.as_p}}
@@ -23,11 +32,15 @@
$('.oh-payslip__header').append(`<div class="d-flex">
{% if show %}
{% if needs_auth %}
<a class="oh-btn oh-btn--info mr-2" href="{% url 'gdrive_authorize' %}" title="Authorize">{% trans "Authorize Google Drive" %}</a>
{% else %}
{% if active %}
<a class="oh-btn oh-btn--danger mr-2" href="{% url 'gdrive_start_stop' %}" title="Stop">Stop</a>
{% else %}
<a class="oh-btn oh-btn--success mr-2" href="{% url 'gdrive_start_stop' %}" title="Start">Start</a>
{% endif %}
{% endif %}
<a class="oh-btn oh-btn--danger-outline" href="{% url 'gdrive_delete' %}" title="Remove">
<ion-icon name="trash-outline" role="img" class="md hydrated" aria-label="trash outline"></ion-icon>
</a>
@@ -37,12 +50,12 @@
$('#id_seconds').parent().hide();
}
if (!$('#id_fixed').is(':checked')) {
$('#id_hour').parent().hide();``
$('#id_hour').parent().hide();
$('#id_minute').parent().hide();
}
$('#id_interval').change(function() {
if ($(this).is(':checked')) {
$('#id_fixed4').prop('checked', false);
$('#id_fixed').prop('checked', false);
$('#id_hour').parent().hide();
$('#id_minute').parent().hide();
$('#id_seconds').parent().show();

View File

@@ -9,4 +9,6 @@ urlpatterns = [
path("gdrive/", gdrive_setup, name="gdrive"),
path("gdrive-start-stop/", gdrive_Backup_stop_or_start, name="gdrive_start_stop"),
path("gdrive-delete/", gdrive_Backup_delete, name="gdrive_delete"),
path("gdrive-authorize/", gdrive_authorize, name="gdrive_authorize"),
path("google/callback/", gdrive_callback, name="gdrive_callback"),
]

View File

@@ -1,3 +1,7 @@
import json
import os
from django.conf import settings
from django.contrib import messages
from django.db import connection
from django.shortcuts import redirect, render
@@ -112,13 +116,25 @@ def gdrive_setup(request):
form = GdriveBackupSetupForm()
show = False
active = False
needs_auth = False
if connection.vendor != "postgresql":
return render(request, "backup/404.html")
if GoogleDriveBackup.objects.exists():
instance = GoogleDriveBackup.objects.first()
form = GdriveBackupSetupForm(instance=instance)
show = True
active = GoogleDriveBackup.objects.first().active
# Check if OAuth tokens exist - need both credentials file and access token
if instance.oauth_credentials_file:
if not instance.access_token:
needs_auth = True
else:
# No credentials file uploaded yet
needs_auth = False # Don't show auth button if no credentials file
if request.method == "POST":
form = GdriveBackupSetupForm(request.POST, request.FILES, instance=instance)
if form.is_valid():
@@ -126,24 +142,49 @@ def gdrive_setup(request):
google_drive.active = False
google_drive.save()
stop_gdrive_backup_job()
messages.success(request, _("gdrive backup automation setup updated."))
# If credentials file was updated, reset tokens
if "oauth_credentials_file" in request.FILES:
google_drive.access_token = None
google_drive.refresh_token = None
google_drive.token_expiry = None
google_drive.save()
messages.success(
request,
_(
"OAuth credentials file uploaded. Please authorize Google Drive access."
),
)
else:
messages.success(
request, _("gdrive backup automation setup updated.")
)
return redirect("gdrive")
# Re-check needs_auth after potential updates
if instance.oauth_credentials_file and not instance.access_token:
needs_auth = True
return render(
request,
"backup/gdrive_setup_form.html",
{"form": form, "show": show, "active": active},
{"form": form, "show": show, "active": active, "needs_auth": needs_auth},
)
if request.method == "POST":
form = GdriveBackupSetupForm(request.POST, request.FILES)
if form.is_valid():
form.save()
google_drive = form.save()
# After saving, check if tokens are needed
if not google_drive.access_token:
needs_auth = True
messages.success(request, _("gdrive backup automation setup Created."))
return redirect("gdrive")
return render(
request,
"backup/gdrive_setup_form.html",
{"form": form, "show": show, "active": active},
{"form": form, "show": show, "active": active, "needs_auth": needs_auth},
)
@@ -193,3 +234,135 @@ def gdrive_Backup_delete(request):
stop_gdrive_backup_job()
messages.success(request, _("Gdrive Backup Automation Removed Successfully."))
return redirect("gdrive")
@login_required
@permission_required("backup.add_localbackup")
def gdrive_authorize(request):
"""
Initiate OAuth authorization flow for Google Drive.
"""
os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1" # For local development with HTTP
if not GoogleDriveBackup.objects.exists():
messages.error(request, _("Please set up Google Drive backup first."))
return redirect("gdrive")
google_drive = GoogleDriveBackup.objects.first()
if not google_drive.oauth_credentials_file:
messages.error(request, _("Please upload OAuth credentials file first."))
return redirect("gdrive")
try:
# Read OAuth credentials file
oauth_file_path = google_drive.oauth_credentials_file.path
with open(oauth_file_path, "r") as f:
client_config = json.load(f)
# Use redirect URI from credentials file if available, otherwise construct it
if (
"web" in client_config
and "redirect_uris" in client_config["web"]
and len(client_config["web"]["redirect_uris"]) > 0
):
redirect_uri = client_config["web"]["redirect_uris"][0]
else:
scheme = "https" if not settings.DEBUG else "http"
host = request.get_host()
redirect_uri = f"{scheme}://{host}/google/callback/"
# Store client config and redirect URI in session for callback
request.session["gdrive_oauth_client_config"] = json.dumps(client_config)
request.session["gdrive_oauth_redirect_uri"] = redirect_uri
# Generate authorization URL
authorization_url, flow, state = get_authorization_url(
oauth_file_path, redirect_uri
)
# Store state in session for verification
request.session["gdrive_oauth_state"] = state
return redirect(authorization_url)
except Exception as e:
messages.error(request, _(f"Failed to initiate authorization: {str(e)}"))
return redirect("gdrive")
@login_required
@permission_required("backup.add_localbackup")
def gdrive_callback(request):
"""
Handle OAuth callback from Google.
"""
os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1" # For local development with HTTP
if not GoogleDriveBackup.objects.exists():
messages.error(request, _("Google Drive backup not configured."))
return redirect("gdrive")
google_drive = GoogleDriveBackup.objects.first()
# Check for error in callback
if "error" in request.GET:
error = request.GET.get("error")
messages.error(request, _(f"Authorization failed: {error}"))
return redirect("gdrive")
# Check for authorization code
if "code" not in request.GET:
messages.error(request, _("Authorization code not received."))
return redirect("gdrive")
# Verify session data exists
if (
"gdrive_oauth_client_config" not in request.session
or "gdrive_oauth_redirect_uri" not in request.session
):
messages.error(request, _("Session expired. Please try authorizing again."))
return redirect("gdrive")
try:
# Recreate flow from stored client config
client_config = json.loads(request.session["gdrive_oauth_client_config"])
redirect_uri = request.session["gdrive_oauth_redirect_uri"]
flow = Flow.from_client_config(client_config, SCOPES, redirect_uri=redirect_uri)
# Exchange authorization code for tokens
full_url = request.build_absolute_uri()
flow.fetch_token(authorization_response=full_url)
creds = flow.credentials
# Store tokens in model
google_drive.access_token = creds.token
if creds.refresh_token:
google_drive.refresh_token = creds.refresh_token
if creds.expiry:
from django.utils import timezone
if timezone.is_naive(creds.expiry):
google_drive.token_expiry = timezone.make_aware(
creds.expiry, timezone.utc
)
else:
google_drive.token_expiry = creds.expiry
google_drive.save()
# Clean up session
if "gdrive_oauth_client_config" in request.session:
del request.session["gdrive_oauth_client_config"]
if "gdrive_oauth_redirect_uri" in request.session:
del request.session["gdrive_oauth_redirect_uri"]
if "gdrive_oauth_state" in request.session:
del request.session["gdrive_oauth_state"]
messages.success(request, _("Google Drive authorization successful!"))
return redirect("gdrive")
except Exception as e:
import traceback
print(f"OAuth callback error: {traceback.format_exc()}")
messages.error(request, _(f"Authorization failed: {str(e)}"))
return redirect("gdrive")