Download google-workspace/bin/drive-cli.py from smlflg/Connector: direct link, hf CLI and curl.
- Browser
- Download file 10.7 kB
-
https://huggingface.co/smlflg/Connector/resolve/main/google-workspace/bin/drive-cli.py
- Command line
-
hf download hf://smlflg/Connector/google-workspace/bin/drive-cli.py
-
curl -L -o drive-cli.py https://huggingface.co/smlflg/Connector/resolve/main/google-workspace/bin/drive-cli.py
10.7 kB
| #!/usr/bin/env python3 | |
| """ | |
| Google Drive CLI — direkte REST API ohne MCP-Server. | |
| Token-Lifecycle: | |
| 1. Token aus credentials-Datei lesen | |
| 2. Prüfen ob expiry erreicht → wenn ja, refresh via oauth2.googleapis.com/token | |
| 3. Drive API via curl aufrufen | |
| Fallback-Pfade: | |
| - taylorwilsdon: /home/smlflg/Projekte/Connector/google-workspace/state/credentials/smlflg@gmail.com.json | |
| - gdrive MCP: /home/smlflg/.hermes/gdrive-server-credentials.json | |
| """ | |
| import argparse | |
| import json | |
| import os | |
| import sys | |
| import subprocess | |
| from datetime import datetime, timedelta, timezone | |
| from urllib.request import Request, urlopen | |
| from urllib.parse import urlencode | |
| from urllib.error import HTTPError | |
| # Hardcoded paths (like token-keeper.py) | |
| TOKEN_PATHS = [ | |
| "/home/smlflg/Projekte/Connector/google-workspace/state/credentials/smlflg@gmail.com.json", | |
| "/home/smlflg/.hermes/gdrive-server-credentials.json", | |
| ] | |
| OAUTH_ENDPOINT = "https://oauth2.googleapis.com/token" | |
| DRIVE_API = "https://www.googleapis.com/drive/v3" | |
| REFRESH_MARGIN_MINUTES = 15 | |
| def load_credentials(): | |
| """Load credentials from first available token file.""" | |
| for path in TOKEN_PATHS: | |
| if os.path.exists(path): | |
| with open(path) as f: | |
| return json.load(f), path | |
| return None, None | |
| def parse_expiry(expiry_str): | |
| """Parse expiry string as naive local datetime (matching upstream format).""" | |
| if not expiry_str: | |
| return None | |
| try: | |
| dt = datetime.fromisoformat(expiry_str) | |
| # Strip timezone if present (match upstream behavior) | |
| if dt.tzinfo is not None: | |
| dt = dt.replace(tzinfo=None) | |
| return dt | |
| except (ValueError, TypeError): | |
| return None | |
| def is_token_valid(creds): | |
| """Check if token is valid or will expire within margin.""" | |
| expiry = parse_expiry(creds.get("expiry")) | |
| if expiry is None: | |
| return False | |
| now = datetime.now() | |
| margin = timedelta(minutes=REFRESH_MARGIN_MINUTES) | |
| return now < (expiry - margin) | |
| def refresh_access_token(creds): | |
| """Refresh the access token using refresh_token.""" | |
| refresh_tok = creds.get("refresh_token") | |
| if not refresh_tok: | |
| return None | |
| # Try to get client_id/secret from local.env first, then from creds | |
| env = {} | |
| env_file = "/home/smlflg/Projekte/Connector/google-workspace/config/local.env" | |
| if os.path.exists(env_file): | |
| with open(env_file) as f: | |
| for line in f: | |
| line = line.strip() | |
| if "=" in line: | |
| k, _, v = line.partition("=") | |
| env[k.strip()] = v.strip().strip('"').strip("'") | |
| client_id = env.get("GOOGLE_OAUTH_CLIENT_ID") or creds.get("client_id") | |
| client_secret = env.get("GOOGLE_OAUTH_CLIENT_SECRET") or creds.get("client_secret") | |
| if not client_id or not client_secret: | |
| print("ERROR: Missing client_id or client_secret", file=sys.stderr) | |
| return None | |
| data = urlencode({ | |
| "client_id": client_id, | |
| "client_secret": client_secret, | |
| "refresh_token": refresh_tok, | |
| "grant_type": "refresh_token", | |
| }).encode() | |
| req = Request(OAUTH_ENDPOINT, data=data, method="POST") | |
| req.add_header("Content-Type", "application/x-www-form-urlencoded") | |
| try: | |
| with urlopen(req, timeout=30) as resp: | |
| result = json.loads(resp.read().decode()) | |
| except HTTPError as e: | |
| body = e.read().decode("utf-8", errors="replace") | |
| print(f"ERROR: Token refresh failed (HTTP {e.code}): {body[:200]}", file=sys.stderr) | |
| return None | |
| if "access_token" not in result: | |
| print("ERROR: No access_token in refresh response", file=sys.stderr) | |
| return None | |
| return result | |
| def get_valid_token(creds_path=None): | |
| """Get a valid access token, refreshing if necessary. Returns (token, creds_dict, path).""" | |
| if creds_path is None: | |
| creds, path = load_credentials() | |
| else: | |
| if os.path.exists(creds_path): | |
| with open(creds_path) as f: | |
| creds = json.load(f) | |
| path = creds_path | |
| else: | |
| creds, path = load_credentials() | |
| if not creds: | |
| print("ERROR: No credentials found", file=sys.stderr) | |
| return None, None, None | |
| # Check if refresh needed | |
| if not is_token_valid(creds): | |
| result = refresh_access_token(creds) | |
| if not result: | |
| return None, None, None | |
| # Update credentials | |
| creds["token"] = result["access_token"] | |
| new_expiry = datetime.now() + timedelta(seconds=result.get("expires_in", 3600)) | |
| # Store as naive local datetime (no timezone suffix) | |
| creds["expiry"] = new_expiry.replace(tzinfo=None).isoformat() | |
| if path: # Only write if we have a valid path | |
| with open(path, "w") as f: | |
| json.dump(creds, f, indent=2) | |
| return creds.get("token"), creds, path | |
| def curl_drive(endpoint, token, output=None): | |
| """Make a curl request to Drive API. Returns JSON dict or None.""" | |
| url = f"{DRIVE_API}/{endpoint}" | |
| env = os.environ.copy() | |
| env["TOKEN"] = token # For curl to pick up | |
| # Use curl with Authorization header | |
| cmd = ["curl", "-s", "-H", f"Authorization: Bearer {token}", url] | |
| result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) | |
| if result.returncode != 0: | |
| print(f"ERROR: curl failed: {result.stderr}", file=sys.stderr) | |
| return None | |
| try: | |
| return json.loads(result.stdout) | |
| except json.JSONDecodeError: | |
| return result.stdout # Return raw text for non-JSON responses | |
| def cmd_status(args): | |
| """Show token status.""" | |
| creds, path = load_credentials() | |
| if not creds: | |
| print("No credentials found") | |
| return 1 | |
| expiry = parse_expiry(creds.get("expiry")) | |
| now = datetime.now() | |
| print(f"Credentials file: {path}") | |
| print(f"Has access_token: {'Yes' if creds.get('token') else 'No'}") | |
| print(f"Has refresh_token: {'Yes' if creds.get('refresh_token') else 'No'}") | |
| if expiry: | |
| valid = now < expiry | |
| remaining = expiry - now | |
| mins = int(remaining.total_seconds() / 60) | |
| print(f"Expiry: {expiry.strftime('%Y-%m-%d %H:%M:%S')}") | |
| print(f"Valid: {'Yes' if valid else 'No (expired)'}") | |
| if valid: | |
| print(f"Remaining: ~{mins} minutes") | |
| else: | |
| print("Expiry: Unknown") | |
| return 0 | |
| def cmd_list(args): | |
| """List files in a folder.""" | |
| token, _, path = get_valid_token() | |
| if not token: | |
| return 1 | |
| folder_id = args.folder_id or "1Lii9ILFarnqX8_RMxjn2qwoOeGup6VfT" | |
| fields = "files(id,name,mimeType,size,modifiedTime)" | |
| query = urlencode({ | |
| "q": f"'{folder_id}' in parents and trashed=false", | |
| "fields": fields, | |
| "pageSize": 100, | |
| }) | |
| result = curl_drive(f"files?{query}", token) | |
| if result is None or isinstance(result, str): | |
| print("ERROR: Failed to list files", file=sys.stderr) | |
| return 1 | |
| files = result.get("files", []) | |
| if args.json: | |
| print(json.dumps(files, indent=2)) | |
| else: | |
| print(f"{len(files)} files in folder {folder_id}:") | |
| for f in files: | |
| size = f.get("size", "?") | |
| modified = f.get("modifiedTime", "?")[:10] | |
| mime = f.get("mimeType", "?")[:40] | |
| print(f" {f['name'][:50]:<50} {size:>8} bytes {modified} {mime}") | |
| return 0 | |
| def cmd_read(args): | |
| """Read file content.""" | |
| token, _, path = get_valid_token() | |
| if not token: | |
| return 1 | |
| file_id = args.file_id | |
| if not file_id: | |
| print("ERROR: --file-id required", file=sys.stderr) | |
| return 1 | |
| url = f"{DRIVE_API}/files/{file_id}?alt=media" | |
| cmd = ["curl", "-s", "-H", f"Authorization: Bearer {token}", url] | |
| result = subprocess.run(cmd, capture_output=True, text=True, timeout=60) | |
| if result.returncode != 0: | |
| print(f"ERROR: curl failed: {result.stderr}", file=sys.stderr) | |
| return 1 | |
| if args.output: | |
| with open(args.output, "w") as f: | |
| f.write(result.stdout) | |
| print(f"Saved to {args.output}") | |
| else: | |
| print(result.stdout) | |
| return 0 | |
| def cmd_meta(args): | |
| """Get file metadata.""" | |
| token, _, path = get_valid_token() | |
| if not token: | |
| return 1 | |
| file_id = args.file_id | |
| if not file_id: | |
| print("ERROR: --file-id required", file=sys.stderr) | |
| return 1 | |
| fields = "id,name,mimeType,size,modifiedTime,webViewLink" | |
| result = curl_drive(f"files/{file_id}?fields={fields}", token) | |
| if result is None: | |
| print("ERROR: Failed to get metadata", file=sys.stderr) | |
| return 1 | |
| if args.json: | |
| print(json.dumps(result, indent=2)) | |
| else: | |
| if isinstance(result, dict): | |
| for k, v in result.items(): | |
| print(f"{k}: {v}") | |
| else: | |
| print(result) | |
| return 0 | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description="Google Drive CLI — direkte REST API ohne MCP-Server", | |
| formatter_class=argparse.RawDescriptionHelpFormatter, | |
| epilog=""" | |
| Commands: | |
| status Show token status | |
| list List files in folder | |
| read Read file content | |
| meta Get file metadata | |
| Examples: | |
| drive-cli.py status | |
| drive-cli.py list --folder-id 1Lii9ILFarnqX8_RMxjn2qwoOeGup6VfT | |
| drive-cli.py read --file-id <id> --output /tmp/file.txt | |
| drive-cli.py meta --file-id <id> | |
| """ | |
| ) | |
| subparsers = parser.add_subparsers(dest="command", required=True) | |
| # status | |
| subparsers.add_parser("status", help="Show token status") | |
| # list | |
| p_list = subparsers.add_parser("list", help="List files in folder") | |
| p_list.add_argument("--folder-id", help="Drive folder ID") | |
| p_list.add_argument("--json", action="store_true", help="JSON output") | |
| # read | |
| p_read = subparsers.add_parser("read", help="Read file content") | |
| p_read.add_argument("--file-id", required=True, help="Drive file ID") | |
| p_read.add_argument("--output", help="Output file (default: stdout)") | |
| # meta | |
| p_meta = subparsers.add_parser("meta", help="Get file metadata") | |
| p_meta.add_argument("--file-id", required=True, help="Drive file ID") | |
| p_meta.add_argument("--json", action="store_true", help="JSON output") | |
| args = parser.parse_args() | |
| if args.command == "status": | |
| return cmd_status(args) | |
| elif args.command == "list": | |
| return cmd_list(args) | |
| elif args.command == "read": | |
| return cmd_read(args) | |
| elif args.command == "meta": | |
| return cmd_meta(args) | |
| else: | |
| parser.print_help() | |
| return 1 | |
| if __name__ == "__main__": | |
| sys.exit(main()) |