Created
December 9, 2025 08:46
-
-
Save slhck/03e7f06bca36947adc444abba6344857 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env -S uv run --script | |
| # /// script | |
| # requires-python = ">=3.11" | |
| # dependencies = [ | |
| # "google-auth", | |
| # "google-auth-oauthlib", | |
| # "google-auth-httplib2", | |
| # "google-api-python-client", | |
| # ] | |
| # /// | |
| # | |
| # ============================================================================ | |
| # Google Drive File Uploader & Sharer | |
| # ============================================================================ | |
| # | |
| # A standalone script to upload and share files on Google Drive, supporting | |
| # both personal drives and Shared Drives (formerly Team Drives). | |
| # | |
| # SETUP INSTRUCTIONS: | |
| # ------------------- | |
| # 1. Go to https://console.cloud.google.com/ | |
| # 2. Create a new project (or select an existing one) | |
| # 3. Enable the Google Drive API: | |
| # - Go to "APIs & Services" > "Library" | |
| # - Search for "Google Drive API" and enable it | |
| # 4. Create OAuth 2.0 credentials: | |
| # - Go to "APIs & Services" > "Credentials" | |
| # - Click "Create Credentials" > "OAuth client ID" | |
| # - Choose "Desktop app" as the application type | |
| # - Download the JSON file and save it as "credentials.json" | |
| # in the same directory as this script | |
| # 5. Configure the OAuth consent screen: | |
| # - Go to "APIs & Services" > "OAuth consent screen" | |
| # - Add your email as a test user (for "External" user type) | |
| # | |
| # FIRST RUN: | |
| # ---------- | |
| # On first run, a browser window will open for OAuth authentication. | |
| # After granting permissions, a token.json file is created for future use. | |
| # | |
| # UPLOAD USAGE: | |
| # ------------- | |
| # # Upload all PDFs from a directory: | |
| # ./google-drive-upload.py --folder-id YOUR_FOLDER_ID --source ./my-pdfs | |
| # | |
| # # Upload specific file types: | |
| # ./google-drive-upload.py --folder-id YOUR_FOLDER_ID --source ./docs --pattern "*.docx" | |
| # | |
| # # Upload a single file: | |
| # ./google-drive-upload.py --folder-id YOUR_FOLDER_ID --file ./report.pdf | |
| # | |
| # SHARING USAGE: | |
| # -------------- | |
| # # Upload and share with a user (writer access): | |
| # ./google-drive-upload.py --folder-id ID --file ./doc.pdf \ | |
| # --share-user [email protected] --share-role writer | |
| # | |
| # # Upload and share with multiple users: | |
| # ./google-drive-upload.py --folder-id ID --file ./doc.pdf \ | |
| # --share-user [email protected] --share-user [email protected] | |
| # | |
| # # Upload and share with entire domain (reader access): | |
| # ./google-drive-upload.py --folder-id ID --file ./doc.pdf \ | |
| # --share-domain example.com --share-role reader | |
| # | |
| # # Share existing file by ID (no upload): | |
| # ./google-drive-upload.py --share-file-id FILE_ID \ | |
| # --share-user [email protected] --share-role writer | |
| # | |
| # # Make file publicly accessible (anyone with link): | |
| # ./google-drive-upload.py --share-file-id FILE_ID --share-anyone | |
| # | |
| # SHARE ROLES: | |
| # ------------ | |
| # reader - Can view | |
| # commenter - Can view and comment | |
| # writer - Can edit | |
| # | |
| # FINDING YOUR FOLDER ID: | |
| # ----------------------- | |
| # Open the folder in Google Drive in your browser. The URL will look like: | |
| # https://drive.google.com/drive/folders/1ABC123xyz... | |
| # The folder ID is the part after "folders/": 1ABC123xyz... | |
| # | |
| # For Shared Drives, navigate to the folder and copy the ID from the URL. | |
| # | |
| # ============================================================================ | |
| import argparse | |
| import mimetypes | |
| import sys | |
| from pathlib import Path | |
| from google.auth.transport.requests import Request | |
| from google.oauth2.credentials import Credentials | |
| from google_auth_oauthlib.flow import InstalledAppFlow | |
| from googleapiclient.discovery import build | |
| from googleapiclient.errors import HttpError | |
| from googleapiclient.http import MediaFileUpload | |
| # Full Drive access scope (required for Shared Drives) | |
| # For more restrictive access, use: https://www.googleapis.com/auth/drive.file | |
| SCOPES = ["https://www.googleapis.com/auth/drive"] | |
| def get_credentials(credentials_path: Path, token_path: Path) -> Credentials: | |
| """Get or refresh Google Drive API credentials. | |
| Args: | |
| credentials_path: Path to the OAuth credentials JSON file | |
| token_path: Path to store/retrieve the user token | |
| Returns: | |
| Valid Google OAuth credentials | |
| """ | |
| creds = None | |
| if not credentials_path.exists(): | |
| print(f"Error: {credentials_path} not found.") | |
| print("Please download OAuth 2.0 credentials from Google Cloud Console") | |
| print("and save them in the specified location.") | |
| print("\nSee setup instructions at the top of this script.") | |
| sys.exit(1) | |
| # Load existing token | |
| if token_path.exists(): | |
| creds = Credentials.from_authorized_user_file(str(token_path), SCOPES) | |
| # If no valid credentials, initiate OAuth flow | |
| if not creds or not creds.valid: | |
| if creds and creds.expired and creds.refresh_token: | |
| print("Refreshing expired credentials...") | |
| creds.refresh(Request()) | |
| else: | |
| print("Opening browser for authentication...") | |
| flow = InstalledAppFlow.from_client_secrets_file( | |
| str(credentials_path), SCOPES | |
| ) | |
| creds = flow.run_local_server(port=0) | |
| # Save credentials for future runs | |
| with open(token_path, "w") as token: | |
| token.write(creds.to_json()) | |
| print(f"Credentials saved to {token_path}") | |
| return creds | |
| def get_mimetype(file_path: Path) -> str: | |
| """Determine the MIME type of a file. | |
| Args: | |
| file_path: Path to the file | |
| Returns: | |
| MIME type string, defaults to 'application/octet-stream' | |
| """ | |
| mime_type, _ = mimetypes.guess_type(str(file_path)) | |
| return mime_type or "application/octet-stream" | |
| def upload_file_with_id( | |
| service, file_path: Path, folder_id: str, update_existing: bool = True | |
| ) -> str | None: | |
| """Upload a file to the specified Google Drive folder. | |
| Args: | |
| service: Google Drive API service instance | |
| file_path: Path to the file to upload | |
| folder_id: Target folder ID in Google Drive | |
| update_existing: If True, update existing files; if False, skip them | |
| Returns: | |
| File ID if upload succeeded, None otherwise | |
| """ | |
| try: | |
| # Check if file already exists in the folder | |
| query = f"name='{file_path.name}' and '{folder_id}' in parents and trashed=false" | |
| results = service.files().list( | |
| q=query, | |
| fields="files(id, name)", | |
| spaces="drive", | |
| includeItemsFromAllDrives=True, | |
| supportsAllDrives=True, | |
| ).execute() | |
| existing_files = results.get("files", []) | |
| mime_type = get_mimetype(file_path) | |
| media = MediaFileUpload(str(file_path), mimetype=mime_type, resumable=True) | |
| if existing_files: | |
| file_id = existing_files[0]["id"] | |
| if not update_existing: | |
| print(f" Skipped (exists): {file_path.name} (ID: {file_id})") | |
| return file_id | |
| # Update existing file | |
| updated = service.files().update( | |
| fileId=file_id, | |
| media_body=media, | |
| fields="id, name", | |
| supportsAllDrives=True, | |
| ).execute() | |
| result_id = updated.get("id") | |
| print(f" Updated: {file_path.name} (ID: {result_id})") | |
| return result_id | |
| else: | |
| # Create new file | |
| file_metadata = { | |
| "name": file_path.name, | |
| "parents": [folder_id], | |
| } | |
| created = service.files().create( | |
| body=file_metadata, | |
| media_body=media, | |
| fields="id, name", | |
| supportsAllDrives=True, | |
| ).execute() | |
| result_id = created.get("id") | |
| print(f" Uploaded: {file_path.name} (ID: {result_id})") | |
| return result_id | |
| except HttpError as error: | |
| print(f" Error uploading {file_path.name}: {error}") | |
| return None | |
| def share_file( | |
| service, | |
| file_id: str, | |
| users: list[str] | None = None, | |
| domain: str | None = None, | |
| anyone: bool = False, | |
| role: str = "reader", | |
| send_notification: bool = True, | |
| ) -> list[str]: | |
| """Share a file with users, a domain, or make it publicly accessible. | |
| Args: | |
| service: Google Drive API service instance | |
| file_id: ID of the file to share | |
| users: List of email addresses to share with | |
| domain: Domain to share with (e.g., "example.com") | |
| anyone: If True, make the file accessible to anyone with the link | |
| role: Permission role - "reader", "commenter", or "writer" | |
| send_notification: Whether to send email notifications to users | |
| Returns: | |
| List of permission IDs that were created | |
| """ | |
| permission_ids = [] | |
| def batch_callback(request_id, response, exception): | |
| if exception: | |
| print(f" Error sharing ({request_id}): {exception}") | |
| else: | |
| perm_id = response.get("id") | |
| permission_ids.append(perm_id) | |
| print(f" Shared ({request_id}): permission ID {perm_id}") | |
| batch = service.new_batch_http_request(callback=batch_callback) | |
| # Share with specific users | |
| if users: | |
| for email in users: | |
| user_permission = { | |
| "type": "user", | |
| "role": role, | |
| "emailAddress": email, | |
| } | |
| batch.add( | |
| service.permissions().create( | |
| fileId=file_id, | |
| body=user_permission, | |
| fields="id", | |
| sendNotificationEmail=send_notification, | |
| supportsAllDrives=True, | |
| ), | |
| request_id=f"user:{email}", | |
| ) | |
| # Share with a domain | |
| if domain: | |
| domain_permission = { | |
| "type": "domain", | |
| "role": role, | |
| "domain": domain, | |
| } | |
| batch.add( | |
| service.permissions().create( | |
| fileId=file_id, | |
| body=domain_permission, | |
| fields="id", | |
| supportsAllDrives=True, | |
| ), | |
| request_id=f"domain:{domain}", | |
| ) | |
| # Make publicly accessible (anyone with the link) | |
| if anyone: | |
| anyone_permission = { | |
| "type": "anyone", | |
| "role": role, | |
| } | |
| batch.add( | |
| service.permissions().create( | |
| fileId=file_id, | |
| body=anyone_permission, | |
| fields="id", | |
| supportsAllDrives=True, | |
| ), | |
| request_id="anyone", | |
| ) | |
| if users or domain or anyone: | |
| batch.execute() | |
| return permission_ids | |
| def verify_folder(service, folder_id: str) -> str | None: | |
| """Verify that a folder exists and is accessible. | |
| Args: | |
| service: Google Drive API service instance | |
| folder_id: Folder ID to verify | |
| Returns: | |
| Folder name if accessible, None otherwise | |
| """ | |
| try: | |
| folder = service.files().get( | |
| fileId=folder_id, | |
| fields="id, name", | |
| supportsAllDrives=True, | |
| ).execute() | |
| return folder.get("name") | |
| except HttpError as error: | |
| if error.resp.status == 404: | |
| print(f"Error: Folder '{folder_id}' not found or not accessible.") | |
| print("\nPossible causes:") | |
| print(" - The folder ID is incorrect") | |
| print(" - The folder isn't shared with your Google account") | |
| print(" - You need to request access to the folder") | |
| else: | |
| print(f"Error accessing folder: {error}") | |
| return None | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description="Upload and share files on Google Drive", | |
| formatter_class=argparse.RawDescriptionHelpFormatter, | |
| epilog=""" | |
| Examples: | |
| # Upload files | |
| %(prog)s --folder-id ABC123 --source ./pdfs | |
| %(prog)s --folder-id ABC123 --file ./report.pdf | |
| # Upload and share with users | |
| %(prog)s --folder-id ABC123 --file ./doc.pdf --share-user [email protected] | |
| # Upload and share with domain | |
| %(prog)s --folder-id ABC123 --file ./doc.pdf --share-domain example.com --share-role reader | |
| # Share existing file (no upload) | |
| %(prog)s --share-file-id FILE_ID --share-user [email protected] --share-role writer | |
| # Make file public | |
| %(prog)s --share-file-id FILE_ID --share-anyone | |
| """, | |
| ) | |
| # Upload options | |
| upload_group = parser.add_argument_group("Upload options") | |
| upload_group.add_argument( | |
| "--folder-id", | |
| help="Target Google Drive folder ID (from the folder URL)", | |
| ) | |
| upload_group.add_argument( | |
| "--source", | |
| type=Path, | |
| help="Directory containing files to upload", | |
| ) | |
| upload_group.add_argument( | |
| "--file", | |
| type=Path, | |
| help="Single file to upload", | |
| ) | |
| upload_group.add_argument( | |
| "--pattern", | |
| default="*.pdf", | |
| help="Glob pattern for files to upload (default: *.pdf)", | |
| ) | |
| upload_group.add_argument( | |
| "--no-update", | |
| action="store_true", | |
| help="Skip existing files instead of updating them", | |
| ) | |
| # Sharing options | |
| share_group = parser.add_argument_group("Sharing options") | |
| share_group.add_argument( | |
| "--share-file-id", | |
| help="Share an existing file by ID (no upload, just share)", | |
| ) | |
| share_group.add_argument( | |
| "--share-user", | |
| action="append", | |
| dest="share_users", | |
| metavar="EMAIL", | |
| help="Share with user email (can be specified multiple times)", | |
| ) | |
| share_group.add_argument( | |
| "--share-domain", | |
| metavar="DOMAIN", | |
| help="Share with entire domain (e.g., example.com)", | |
| ) | |
| share_group.add_argument( | |
| "--share-anyone", | |
| action="store_true", | |
| help="Make file accessible to anyone with the link", | |
| ) | |
| share_group.add_argument( | |
| "--share-role", | |
| choices=["reader", "commenter", "writer"], | |
| default="reader", | |
| help="Permission role for sharing (default: reader)", | |
| ) | |
| share_group.add_argument( | |
| "--no-notification", | |
| action="store_true", | |
| help="Don't send email notifications when sharing with users", | |
| ) | |
| # Authentication options | |
| auth_group = parser.add_argument_group("Authentication options") | |
| auth_group.add_argument( | |
| "--credentials", | |
| type=Path, | |
| default=Path("credentials.json"), | |
| help="Path to OAuth credentials file (default: credentials.json)", | |
| ) | |
| auth_group.add_argument( | |
| "--token", | |
| type=Path, | |
| default=Path("token.json"), | |
| help="Path to store/retrieve user token (default: token.json)", | |
| ) | |
| args = parser.parse_args() | |
| # Determine mode: share-only or upload (optionally with sharing) | |
| share_only = args.share_file_id is not None | |
| has_sharing = args.share_users or args.share_domain or args.share_anyone | |
| # Validate arguments | |
| if share_only: | |
| if args.folder_id or args.source or args.file: | |
| parser.error("--share-file-id cannot be used with upload options") | |
| if not has_sharing: | |
| parser.error("--share-file-id requires at least one sharing option") | |
| else: | |
| if not args.folder_id: | |
| parser.error("--folder-id is required for uploading") | |
| if not args.source and not args.file: | |
| parser.error("--source or --file is required for uploading") | |
| # Authenticate and build service | |
| creds = get_credentials(args.credentials, args.token) | |
| service = build("drive", "v3", credentials=creds) | |
| # Mode: Share existing file only | |
| if share_only: | |
| print(f"Sharing file ID: {args.share_file_id}") | |
| share_file( | |
| service, | |
| args.share_file_id, | |
| users=args.share_users, | |
| domain=args.share_domain, | |
| anyone=args.share_anyone, | |
| role=args.share_role, | |
| send_notification=not args.no_notification, | |
| ) | |
| print("\nDone!") | |
| return | |
| # Mode: Upload files (optionally with sharing) | |
| # Collect files to upload | |
| if args.file: | |
| if not args.file.exists(): | |
| print(f"Error: File not found: {args.file}") | |
| sys.exit(1) | |
| files_to_upload = [args.file] | |
| else: | |
| if not args.source.exists(): | |
| print(f"Error: Directory not found: {args.source}") | |
| sys.exit(1) | |
| files_to_upload = list(args.source.glob(args.pattern)) | |
| if not files_to_upload: | |
| print(f"No files matching '{args.pattern}' found in {args.source}") | |
| sys.exit(1) | |
| # Verify folder access | |
| folder_name = verify_folder(service, args.folder_id) | |
| if not folder_name: | |
| sys.exit(1) | |
| print(f"\nTarget folder: {folder_name}") | |
| print(f"Files to upload: {len(files_to_upload)}") | |
| for f in files_to_upload: | |
| print(f" - {f.name}") | |
| # Upload files and collect IDs for sharing | |
| print("\nUploading...") | |
| success_count = 0 | |
| uploaded_file_ids = [] | |
| for file_path in files_to_upload: | |
| file_id = upload_file_with_id(service, file_path, args.folder_id, not args.no_update) | |
| if file_id: | |
| success_count += 1 | |
| uploaded_file_ids.append((file_path.name, file_id)) | |
| print(f"\nUploaded: {success_count}/{len(files_to_upload)} files") | |
| # Share uploaded files if sharing options were provided | |
| if has_sharing and uploaded_file_ids: | |
| print("\nSharing uploaded files...") | |
| for file_name, file_id in uploaded_file_ids: | |
| print(f"\n{file_name}:") | |
| share_file( | |
| service, | |
| file_id, | |
| users=args.share_users, | |
| domain=args.share_domain, | |
| anyone=args.share_anyone, | |
| role=args.share_role, | |
| send_notification=not args.no_notification, | |
| ) | |
| print("\nDone!") | |
| if success_count < len(files_to_upload): | |
| sys.exit(1) | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment