| |
| """ |
| Google Drive backup for Alpha MC — uses a Service Account (headless, no browser needed). |
| |
| Setup (one-time): |
| 1. Go to https://console.cloud.google.com |
| 2. Create a project → Enable "Google Drive API" |
| 3. IAM & Admin → Service Accounts → Create service account |
| 4. Create a JSON key for the service account → download it |
| 5. In Google Drive, create a folder called "Minecraft-Bedrock-Backups" |
| and share it with the service account email (e.g. mc@project.iam.gserviceaccount.com) |
| 6. Base64-encode the JSON key: |
| base64 -w 0 your-key.json |
| 7. Add the result as HF Space Secret named: GDRIVE_SA_KEY |
| |
| Usage: |
| python3 google_drive_backup.py <path_to_backup.tar.gz> |
| """ |
|
|
| import os |
| import sys |
| import json |
| import base64 |
| import logging |
| from pathlib import Path |
|
|
| logging.basicConfig( |
| level=logging.INFO, |
| format="%(asctime)s [DRIVE] %(message)s", |
| datefmt="%Y-%m-%d %H:%M:%S" |
| ) |
| log = logging.getLogger(__name__) |
|
|
| BACKUP_FOLDER_NAME = "Minecraft-Bedrock-Backups" |
| MAX_BACKUPS = 10 |
| SA_KEY_ENV = "GDRIVE_SA_KEY" |
| SA_KEY_FILE = "/tmp/gdrive_sa.json" |
|
|
|
|
| def load_service_account_credentials(): |
| """ |
| Loads credentials from the GDRIVE_SA_KEY environment variable. |
| The variable must contain the base64-encoded service account JSON key. |
| Returns a google.oauth2.service_account.Credentials object. |
| """ |
| from google.oauth2 import service_account |
|
|
| raw = os.environ.get(SA_KEY_ENV, "").strip() |
| if not raw: |
| raise EnvironmentError( |
| f"Environment variable '{SA_KEY_ENV}' is not set.\n" |
| " 1. Follow the setup steps at the top of google_drive_backup.py\n" |
| " 2. Add the base64 key as an HF Space Secret named GDRIVE_SA_KEY" |
| ) |
|
|
| |
| try: |
| key_json = base64.b64decode(raw).decode("utf-8") |
| key_dict = json.loads(key_json) |
| except Exception as e: |
| raise ValueError(f"Failed to decode GDRIVE_SA_KEY — make sure it's base64-encoded JSON: {e}") |
|
|
| |
| with open(SA_KEY_FILE, "w") as f: |
| json.dump(key_dict, f) |
|
|
| scopes = ["https://www.googleapis.com/auth/drive"] |
| creds = service_account.Credentials.from_service_account_file(SA_KEY_FILE, scopes=scopes) |
| return creds |
|
|
|
|
| def get_or_create_folder(service, folder_name): |
| """Find or create the backup folder in Drive, return its ID.""" |
| query = ( |
| f"name='{folder_name}' " |
| f"and mimeType='application/vnd.google-apps.folder' " |
| f"and trashed=false" |
| ) |
| results = service.files().list(q=query, spaces="drive", fields="files(id, name)").execute() |
| folders = results.get("files", []) |
|
|
| if folders: |
| log.info(f"Using existing Drive folder: {folder_name} (ID: {folders[0]['id']})") |
| return folders[0]["id"] |
|
|
| log.info(f"Creating new Drive folder: {folder_name}") |
| metadata = { |
| "name": folder_name, |
| "mimeType": "application/vnd.google-apps.folder" |
| } |
| folder = service.files().create(body=metadata, fields="id").execute() |
| return folder.get("id") |
|
|
|
|
| def prune_old_backups(service, folder_id): |
| """Delete oldest backups beyond MAX_BACKUPS.""" |
| results = service.files().list( |
| q=f"'{folder_id}' in parents and trashed=false", |
| orderBy="createdTime desc", |
| fields="files(id, name, createdTime)" |
| ).execute() |
| files = results.get("files", []) |
|
|
| if len(files) > MAX_BACKUPS: |
| to_delete = files[MAX_BACKUPS:] |
| for f in to_delete: |
| service.files().delete(fileId=f["id"]).execute() |
| log.info(f"Deleted old backup: {f['name']}") |
| else: |
| log.info(f"Backup count: {len(files)}/{MAX_BACKUPS} — no pruning needed.") |
|
|
|
|
| def upload_backup(backup_path: str): |
| from googleapiclient.discovery import build |
| from googleapiclient.http import MediaFileUpload |
|
|
| path = Path(backup_path) |
| if not path.exists(): |
| raise FileNotFoundError(f"Backup file not found: {backup_path}") |
|
|
| size_mb = path.stat().st_size / 1024 / 1024 |
| log.info(f"Uploading {path.name} ({size_mb:.1f} MB) to Google Drive...") |
|
|
| creds = load_service_account_credentials() |
| service = build("drive", "v3", credentials=creds) |
|
|
| folder_id = get_or_create_folder(service, BACKUP_FOLDER_NAME) |
|
|
| media = MediaFileUpload( |
| str(path), |
| mimetype="application/gzip", |
| resumable=True, |
| chunksize=5 * 1024 * 1024 |
| ) |
| file_metadata = {"name": path.name, "parents": [folder_id]} |
| request = service.files().create(body=file_metadata, media_body=media, fields="id, name, size") |
|
|
| |
| response = None |
| while response is None: |
| status, response = request.next_chunk() |
| if status: |
| pct = int(status.progress() * 100) |
| log.info(f" Upload progress: {pct}%") |
|
|
| log.info(f"Upload complete! File ID: {response.get('id')}") |
| prune_old_backups(service, folder_id) |
|
|
| |
| try: |
| os.remove(SA_KEY_FILE) |
| except Exception: |
| pass |
|
|
|
|
| if __name__ == "__main__": |
| if len(sys.argv) != 2: |
| print("Usage: python3 google_drive_backup.py <path_to_backup.tar.gz>") |
| sys.exit(1) |
|
|
| try: |
| upload_backup(sys.argv[1]) |
| except EnvironmentError as e: |
| log.error(str(e)) |
| sys.exit(2) |
| except Exception as e: |
| log.error(f"Upload failed: {e}") |
| sys.exit(1) |
|
|