[UPDT] HORILLA_BACKUP: Gdrive backup from google service to oauth
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"),
|
||||
]
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user