Major update: OAuth support for personal Gmail accounts

- Added OAuth 2.0 authentication with auto-refresh tokens
- OAuth tokens refresh automatically on pod startup (works like DropSend Node)
- Service Account method now clearly marked as Google Workspace only
- Updated README with clear auth requirements
- Added wait_for_complete_write() for reliable file uploads
- Removed broken ownership transfer code
- Default auth method changed to OAuth
- Better error messages for quota and auth issues
This commit is contained in:
MachinePainting
2026-02-01 02:34:43 -08:00
parent 9c253d25d3
commit 2c49196561
5 changed files with 859 additions and 786 deletions
+206 -231
View File
@@ -1,48 +1,41 @@
# ComfyUI DriveSend Node
A ComfyUI custom node for seamless Google Drive uploads with **optional** encryption capabilities. Automatically upload your ComfyUI output files (images and videos) to Google Drive cloud storage.
## ⚠️ Authentication Methods - READ FIRST
| Method | Lifespan | Best For | Effort |
|--------|----------|----------|--------|
| **Service Account** | ✅ **Permanent** (never expires) | Cloud/RunPod | Medium setup |
| **OAuth 2.0** | ❌ **7 days** (must re-auth weekly) | Not recommended | Easy setup |
**We strongly recommend Service Account** for any persistent use. OAuth tokens expire every 7 days in Google's "testing mode" and there is no workaround without paying for Google Workspace or going through Google's app verification process.
A ComfyUI custom node for uploading output files to Google Drive with optional encryption. Automatically monitors your output folder and uploads images/videos to your Google Drive.
---
## 🔄 How It Works
## ⚠️ Important: Authentication Requirements
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ CLOUD (RunPod, etc.) │
│ │
│ ComfyUI generates files ──→ DriveSend Node ──→ Uploads to Google Drive │
│ (png, mp4, etc.) │ │
│ │ │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ Encryption OPTIONAL │ │
│ │ ☐ OFF: file.png │ │
│ │ ☑ ON: file.png.enc │ │
│ └──────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
📁 GOOGLE DRIVE
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ YOUR LOCAL MACHINE │
│ │
│ Google Drive syncs/downloads ──→ If encrypted: Run decrypt script (local) │
│ ──→ file.png (viewable!) │
│ │
│ If not encrypted: Ready to use! │
└─────────────────────────────────────────────────────────────────────────────┘
```
DriveSend supports two authentication methods, each with different requirements:
| Method | Account Type | Cost | Token Expiry | Recommended For |
|--------|-------------|------|--------------|-----------------|
| **Service Account** | Google Workspace only | ~$7+/month | Never expires | Business/paid users |
| **OAuth 2.0** | Personal Gmail | Free | Auto-refreshes* | Personal/free users |
**\* OAuth tokens auto-refresh on each pod startup, effectively working like permanent credentials.**
### Why These Limitations?
- **Service Accounts** have 0 GB storage quota on personal Gmail accounts. Google blocks uploads entirely.
- **Google Workspace** ($7+/month) provides Shared Drives where service accounts can upload.
- **OAuth 2.0** works with personal Gmail but requires initial browser authorization.
---
## 📤📦 Features
- **DriveSend AutoUploader Node** - Automatically uploads new files to Google Drive
- Monitors output folder in real-time
- Supports: `.png`, `.jpg`, `.jpeg`, `.webp`, `.gif`, `.mp4`, `.avi`, `.mov`
- Optional AES-128 encryption before upload
- SHA256 checksum verification
- Queue system for reliable uploads
- **DriveSend Setup Node** - Configures authentication and credentials
- Generates OAuth refresh tokens
- Outputs credentials for RunPod environment variables
- Optional encryption key generation
---
@@ -51,243 +44,225 @@ A ComfyUI custom node for seamless Google Drive uploads with **optional** encryp
```bash
cd ComfyUI/custom_nodes/
git clone https://github.com/machinepainting/ComfyUI_DriveSendNode.git
cd ComfyUI_DriveSendNode
pip install -r requirements.txt
pip install -r ComfyUI_DriveSendNode/requirements.txt
```
Restart ComfyUI after installation.
---
## 🔧 Google Cloud Setup (Service Account - Recommended)
## 🔧 Google Cloud Setup (Required for Both Methods)
### Step 1: Create Google Cloud Project
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
2. Sign in with a **personal Gmail account** (not work/school - they often have restrictions)
3. Click **Select a project** → **New Project**
4. Name it (e.g., `ComfyUI-DriveSend`) → **Create**
2. Sign in with your Google account
3. Click **Select a project** (top, next to "Google Cloud" logo) → **New Project**
4. Name it (e.g., `ComfyUI-DriveSend`) → Click **Create**
5. Make sure your new project is selected (check dropdown at top)
### Step 2: Enable Google Drive API
1. Go to **APIs & Services** → **Library**
1. Go to **APIs & Services** → **Library** (left sidebar)
2. Search for **Google Drive API**
3. Click **Enable**
3. Click on it → Click **Enable**
### Step 3: Create Service Account
### Step 3: Create OAuth Credentials
1. Go to **APIs & Services** → **Credentials** (left sidebar)
2. Click **+ Create Credentials** → **OAuth client ID**
3. If prompted, configure the OAuth consent screen:
- User Type: **External** → Click **Create**
- App name: `DriveSend`
- User support email: Your email
- Developer contact: Your email
- Click **Save and Continue** through all steps
4. Back in Credentials, click **+ Create Credentials** → **OAuth client ID**
5. Application type: **Desktop app**
6. Name: `DriveSend`
7. Click **Create**
8. **Download JSON** and save as `client_secret.json` in the DriveSend node folder:
```
ComfyUI/custom_nodes/ComfyUI_DriveSendNode/client_secret.json
```
### Step 4: Add Yourself as Test User
1. Go to **APIs & Services** → **OAuth consent screen**
2. Under **Test users**, click **+ Add Users**
3. Add your Gmail address
4. Click **Save**
---
## 🔐 Option A: OAuth 2.0 Setup (Personal Gmail - FREE)
**Best for:** Personal Gmail accounts, RunPod users who want free Google Drive uploads.
### How It Works
OAuth uses a refresh token that auto-refreshes on every pod startup. You authorize once, and it works indefinitely (as long as you don't revoke access).
### Local Setup
1. Add the **DriveSend Setup** node to your workflow
2. Configure:
- `auth_method`: **oauth**
- `storage_method`: **display_only** (for cloud) or **env_file** (for local)
- `encryption_key_method`: Your preference
3. Run the node
4. A browser window opens - sign in and authorize the app
5. Copy the credentials from the console output
### RunPod Setup
1. Complete local setup first to get your refresh token
2. In RunPod, create these **Secrets**:
| Secret Name | Value |
|-------------|-------|
| `GOOGLE_CLIENT_ID` | Your OAuth client ID |
| `GOOGLE_CLIENT_SECRET` | Your OAuth client secret |
| `GOOGLE_REFRESH_TOKEN` | The refresh token from setup |
| `GOOGLE_DRIVE_FOLDER_ID` | Your Drive folder ID |
| `comfyui_encryption_key` | (Optional) Your encryption key |
3. Add **Environment Variables** to your pod template:
- `GOOGLE_CLIENT_ID` → `{{ RUNPOD_SECRET_GOOGLE_CLIENT_ID }}`
- `GOOGLE_CLIENT_SECRET` → `{{ RUNPOD_SECRET_GOOGLE_CLIENT_SECRET }}`
- `GOOGLE_REFRESH_TOKEN` → `{{ RUNPOD_SECRET_GOOGLE_REFRESH_TOKEN }}`
- `GOOGLE_DRIVE_FOLDER_ID` → `{{ RUNPOD_SECRET_GOOGLE_DRIVE_FOLDER_ID }}`
- `comfyui_encryption_key` → `{{ RUNPOD_SECRET_comfyui_encryption_key }}`
4. Deploy your pod and add the **DriveSend AutoUploader** node with:
- `auth_method`: **oauth**
- `run_process`: **True**
---
## 🏢 Option B: Service Account Setup (Google Workspace - PAID)
**Best for:** Google Workspace subscribers who want simpler, permanent authentication.
**Requirements:**
- Google Workspace account (~$7+/month)
- Custom domain (e.g., yourbusiness.com)
### Why Service Account Requires Google Workspace
Service accounts have 0 GB storage quota. On personal Gmail, uploads fail immediately with "storageQuotaExceeded". Google Workspace provides Shared Drives where service accounts CAN upload.
### Setup Steps
1. Go to **IAM & Admin** → **Service Accounts**
2. Click **+ Create Service Account**
3. Name it (e.g., `comfyui-uploader`)
4. Click **Create and Continue**
5. Skip optional steps → **Done**
3. Name it (e.g., `comfyui-uploader`) → Click **Create and Continue** → **Done**
4. Click on the service account → **Keys** tab → **Add Key** → **Create new key** → **JSON**
5. Rename downloaded file to `service_account.json`
6. Place in: `ComfyUI/custom_nodes/ComfyUI_DriveSendNode/service_account.json`
### Step 4: Fix Organization Policy (IMPORTANT - New Google Accounts)
### Create a Shared Drive
Google now blocks service account key creation by default. You MUST disable this policy first.
1. In Google Drive, click **Shared drives** (left sidebar)
2. Click **+ New** to create a Shared Drive
3. Add the service account email as a **Content Manager**
4. Get the Shared Drive ID from the URL
#### 4a. Grant Yourself Policy Admin Role
### RunPod Configuration
1. Go to **IAM & Admin** → **IAM**
2. Click the dropdown at top-left and select your **organization** (your email domain), not the project
3. Click **+ Grant Access**
4. Principal: **your email**
5. Role: search for **Organization Policy Administrator**
6. Click **Save**
Create secrets and environment variables similar to OAuth setup, but use:
- `GOOGLE_SERVICE_ACCOUNT_JSON` (base64-encoded JSON)
- `GOOGLE_DRIVE_FOLDER_ID` (Shared Drive ID)
#### 4b. Enable the Organization Policy API
---
Open **Cloud Shell** (terminal icon at top-right) and run:
## 📁 Getting Your Folder ID
1. Open Google Drive
2. Navigate to your target folder (or Shared Drive)
3. Look at the URL:
```
https://drive.google.com/drive/folders/XXXXXXXXXXXXXXXXX
```
4. Copy the long string after `/folders/` - that's your **Folder ID**
---
## 🔒 Encryption (Optional)
DriveSend supports AES-128 encryption for files before upload.
### Enable Encryption
1. In **DriveSend Setup** node, set `encryption_key_method` to **Display Only** or **save to .env**
2. Run the setup node to generate a key
3. Save the key securely (you'll need it to decrypt files)
4. In **DriveSend AutoUploader**, set `enable_encryption` to **True**
### Decrypt Files Locally
Use the scripts in the `/scripts/` folder:
**macOS:**
```bash
gcloud services enable orgpolicy.googleapis.com --project=YOUR_PROJECT_ID
./scripts/decrypt_folder_mac.sh
```
Replace `YOUR_PROJECT_ID` with your actual project ID.
#### 4c. Disable the Key Creation Restrictions
Run these commands in Cloud Shell:
**Windows:**
```bash
gcloud org-policies reset iam.disableServiceAccountKeyCreation --project=YOUR_PROJECT_ID
gcloud org-policies reset iam.managed.disableServiceAccountKeyCreation --project=YOUR_PROJECT_ID
python scripts/decrypt_folder_win.py
```
### Step 5: Create and Download the Key
1. Switch back to your **project** (click dropdown top-left, select your project)
2. Go to **IAM & Admin** → **Service Accounts**
3. Click on your service account
4. Go to **Keys** tab
5. Click **Add Key** → **Create new key** → **JSON** → **Create**
6. **IMPORTANT:** Rename the downloaded file to exactly: `service_account.json`
### Step 6: Share Your Google Drive Folder
1. Go to [Google Drive](https://drive.google.com)
2. Create a new folder (e.g., `ComfyUI_Uploads`)
3. Right-click the folder → **Share**
4. Open your `service_account.json` file and find the `client_email` value (looks like `name@project-id.iam.gserviceaccount.com`)
5. Paste that email in the Share dialog
6. Set permission to **Editor**
7. Click **Share** (uncheck "Notify people" if prompted)
### Step 7: Get Your Folder ID
1. Open the folder in Google Drive
2. Look at the URL: `https://drive.google.com/drive/folders/XXXXXXXXXXXXXXXXX`
3. Copy the long string after `/folders/` — that's your **Folder ID**
---
## 🚀 Local Setup (Quick Test)
1. Place `service_account.json` in the `ComfyUI_DriveSendNode` folder
2. Add the **DriveSend Setup** node to ComfyUI
3. Configure:
- `auth_method`: **service_account**
- `folder_id`: your folder ID
- `owner_email`: **your Gmail address** (required!)
- `storage_method`: **env_file**
- `encryption_key_method`: **off** (for testing)
4. Run the node
5. Add **DriveSend AutoUploader** node and run a workflow
---
## ☁️ RunPod / Cloud Setup (Persistent)
### Option A: Custom Template (Recommended - One-Time Setup)
1. Run the **DriveSend Setup** node locally with `storage_method`: **display_only**
2. Copy the output values from the console
3. In RunPod, go to **Secrets** and create:
| Secret Name | Value |
|-------------|-------|
| `GOOGLE_DRIVE_FOLDER_ID` | Your folder ID |
| `GOOGLE_SERVICE_ACCOUNT_JSON` | The base64 string from setup |
| `GOOGLE_OWNER_EMAIL` | Your Gmail address |
| `comfyui_encryption_key` | (only if using encryption) |
4. Create or edit a **Pod Template**:
- Click **Edit Template** → **Environment Variables**
- Add each variable, linking to secrets:
- Key: `GOOGLE_DRIVE_FOLDER_ID` → Value: `{{ RUNPOD_SECRET_GOOGLE_DRIVE_FOLDER_ID }}`
- Key: `GOOGLE_SERVICE_ACCOUNT_JSON` → Value: `{{ RUNPOD_SECRET_GOOGLE_SERVICE_ACCOUNT_JSON }}`
- Key: `GOOGLE_OWNER_EMAIL` → Value: `{{ RUNPOD_SECRET_GOOGLE_OWNER_EMAIL }}`
- Key: `comfyui_encryption_key` → Value: `{{ RUNPOD_SECRET_comfyui_encryption_key }}`
- Click **Set Overrides**
5. Deploy pods using this template — credentials persist automatically!
### Option B: Manual Environment Variables (Each Pod)
If you don't want to create a template, manually add the environment variables each time you create a pod. This is more tedious but works the same way.
---
## 📋 Node Settings Reference
### DriveSend Setup Node
| Field | Description |
|-------|-------------|
| `auth_method` | `service_account` (recommended) or `oauth` |
| `folder_id` | Google Drive folder ID from URL |
| `owner_email` | **Your Gmail** - required for service account! |
| `storage_method` | `display_only` (cloud) or `env_file` (local) |
| `encryption_key_method` | `off`, `Display Only`, or `save to .env` |
| `service_account_path` | Default: `service_account.json` |
### DriveSend AutoUploader Node
| Field | Description |
|-------|-------------|
| `watch_directory` | Folder to monitor (default: ComfyUI output) |
| `auth_method` | Must match setup node |
| `folder_id` | Override folder ID (or leave blank to use env var) |
| `owner_email` | Override owner email (or leave blank to use env var) |
| `enable_encryption` | Encrypt files before upload |
| `Post_Delete_Enc` | Delete .enc files after upload |
| `Subfolder_Monitor` | Watch subfolders too |
| `run_process` | Start/stop the monitor |
---
## ❓ Why is `owner_email` Required?
**Service accounts have 0 GB storage quota.** When a service account uploads a file, it owns that file — but it has no storage space!
The `owner_email` setting transfers ownership to your personal Gmail account after upload, so the file uses YOUR storage quota (15 GB free).
Without this, uploads will fail with: `403 storageQuotaExceeded`
---
## 🔐 Encryption (Optional)
Enable encryption to protect files in cloud storage:
1. In Setup node: set `encryption_key_method` to **Display Only** or **save to .env**
2. In AutoUploader: set `enable_encryption` to **True**
3. Save your encryption key securely — you need it to decrypt files!
See the `/scripts/` folder for decryption scripts (run on your local machine after downloading).
**Linux:**
```bash
./scripts/decrypt_folder_linux.sh
```
---
## 🛠️ Troubleshooting
### "service_account.json NOT FOUND"
- Rename your downloaded key file to exactly `service_account.json`
- Place it in the `ComfyUI_DriveSendNode` folder
### "storageQuotaExceeded" Error
- **Cause:** Using service account with personal Gmail
- **Fix:** Use OAuth instead, or upgrade to Google Workspace
### "403 storageQuotaExceeded"
- Set `owner_email` to your Gmail address
- Make sure the folder is shared with your service account email
### "invalid_grant" Error
- **Cause:** Refresh token expired or revoked
- **Fix:** Run setup node again to get new refresh token
### "Organization Policy blocks key creation"
- Follow Step 4 in the setup guide to disable the policy restrictions
- Run the gcloud commands in Cloud Shell
### "Access blocked: App not verified"
- **Cause:** OAuth consent screen in testing mode
- **Fix:** Add your email as a test user (Step 4 in Google Cloud Setup)
### "Permission denied" on upload
- Make sure you shared the Google Drive folder with the service account email
- The service account needs **Editor** access
### Files not uploading
- **Check:** Console output for error messages
- **Check:** Folder ID is correct
- **Check:** Auth credentials are set in environment variables
### Browser doesn't open for OAuth
- **Cause:** Running on headless server (RunPod)
- **Fix:** Complete OAuth setup locally first, then copy credentials to RunPod
---
## 📁 Repository Structure
## 📊 Comparison: DriveSend vs DropSend
```
ComfyUI_DriveSendNode/
├── __init__.py
├── drivesend_uploader_node.py
├── drivesend_setup_node.py
├── gdrive_upload.py
├── gdrive_auth_manager.py
├── encrypt_file.py
├── monitor_output.py
├── requirements.txt
├── README.md
├── .gitignore
└── scripts/
├── decrypt_folder.py
├── decrypt_folder_mac.sh
├── encrypt_folder_mac.sh
├── decrypt_folder_win.py
├── encrypt_folder_win.py
├── decrypt_folder_linux.sh
└── encrypt_folder_linux.sh
```
| Feature | DriveSend (Google Drive) | DropSend (Dropbox) |
|---------|-------------------------|-------------------|
| Free with personal account | ✅ OAuth only | ✅ Yes |
| Service account support | ⚠️ Workspace only | N/A |
| Storage (free tier) | 15 GB | 2 GB |
| Token management | Auto-refresh | Auto-refresh |
| Setup complexity | Medium | Easy |
---
## 🧪 Status
## 🧪 Tested On
**Testing in Progress** — Based on the working [DropSend Node](https://github.com/machinepainting/ComfyUI_DropSendNode).
Please report issues on GitHub!
- Python 3.10 / 3.11
- ComfyUI (Feb 2026)
- RunPod GPU instances
- Google Drive API v3
---
+201 -179
View File
@@ -1,235 +1,257 @@
"""
DriveSend Setup Node
Handles Google Drive API authentication setup and encryption key generation
Handles Google Drive API authentication setup for both OAuth and Service Account methods
"""
import os
import json
import base64
import webbrowser
from pathlib import Path
from .encrypt_file import generate_key
from cryptography.fernet import Fernet
NODE_DIR = Path(__file__).parent
class DriveSendSetupNode:
"""
ComfyUI node for setting up Google Drive API access and encryption.
"""
"""Setup node for configuring Google Drive authentication."""
CATEGORY = "DriveSend"
FUNCTION = "setup"
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("status",)
OUTPUT_NODE = True
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"auth_method": (["service_account", "oauth"],),
"auth_method": (["oauth", "service_account"], {"default": "oauth"}),
"folder_id": ("STRING", {
"default": "",
"default": "",
"multiline": False,
"placeholder": "Google Drive Folder ID (from URL)"
"tooltip": "Google Drive folder ID from URL (after /folders/)"
}),
"owner_email": ("STRING", {
"default": "",
"multiline": False,
"placeholder": "Your Gmail address (for ownership transfer)"
}),
"storage_method": (["display_only", "env_file"],),
"encryption_key_method": (["off", "Display Only", "save to .env"],),
"storage_method": (["display_only", "env_file"], {"default": "display_only"}),
"encryption_key_method": (["off", "display_only", "save_to_env"], {"default": "off"}),
},
"optional": {
"service_account_path": ("STRING", {
"default": "service_account.json",
"client_id": ("STRING", {
"default": "",
"multiline": False,
"placeholder": "Path to service_account.json"
"tooltip": "OAuth Client ID (from Google Cloud Console)"
}),
"client_secret": ("STRING", {
"default": "",
"multiline": False,
"tooltip": "OAuth Client Secret (from Google Cloud Console)"
}),
"auth_code": ("STRING", {
"default": "",
"multiline": False,
"tooltip": "Authorization code from OAuth flow (leave blank first run)"
}),
"reconnect": ("BOOLEAN", {"default": False}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("status",)
FUNCTION = "setup"
CATEGORY = "DriveSend"
OUTPUT_NODE = True
def setup(
self,
auth_method,
folder_id,
owner_email,
storage_method,
encryption_key_method,
service_account_path="service_account.json",
reconnect=False
):
output_lines = []
def setup(self, auth_method, folder_id, storage_method, encryption_key_method,
client_id="", client_secret="", auth_code=""):
"""
Set up Google Drive authentication.
For OAuth:
- First run: Opens browser for authorization, displays auth URL
- Second run: Exchanges auth code for refresh token
For Service Account:
- Validates service_account.json exists
- Displays environment variable format for RunPod
"""
results = []
env_vars = {}
print("\n" + "="*60)
print("[DriveSend Setup] Starting configuration...")
print("="*60)
# Validate folder_id
if not folder_id:
return ("❌ Error: folder_id is required. Get it from your Google Drive folder URL.",)
# Handle folder ID
if folder_id:
env_vars['GOOGLE_DRIVE_FOLDER_ID'] = folder_id
output_lines.append(f"✓ Folder ID configured")
else:
output_lines.append("⚠ No Folder ID provided - will upload to Drive root")
# Handle owner email (required for service account)
if owner_email:
env_vars['GOOGLE_OWNER_EMAIL'] = owner_email
output_lines.append(f"✓ Owner Email: {owner_email}")
elif auth_method == "service_account":
output_lines.append("⚠ WARNING: No owner_email set!")
output_lines.append(" Service accounts have 0 GB storage quota.")
output_lines.append(" Files will fail to upload without ownership transfer.")
# Handle authentication based on method
if auth_method == "service_account":
# Try to find service account file
sa_path = None
# Check provided path first
if service_account_path:
check_path = Path(service_account_path)
if check_path.exists():
sa_path = check_path
elif (NODE_DIR / service_account_path).exists():
sa_path = NODE_DIR / service_account_path
# Fall back to default name in node directory
if not sa_path and (NODE_DIR / 'service_account.json').exists():
sa_path = NODE_DIR / 'service_account.json'
if sa_path:
try:
with open(sa_path, 'r') as f:
sa_json = f.read()
# Base64 encode for environment variable storage
sa_base64 = base64.b64encode(sa_json.encode()).decode()
env_vars['GOOGLE_SERVICE_ACCOUNT_JSON'] = sa_base64
# Parse to get client email for display
sa_data = json.loads(sa_json)
client_email = sa_data.get('client_email', 'unknown')
output_lines.append(f"✓ Service Account loaded: {client_email}")
output_lines.append(f" Make sure your Drive folder is shared with this email!")
print(f"\n[DriveSend Setup] Service Account: {client_email}")
print(f"[DriveSend Setup] Share your Google Drive folder with: {client_email}")
except Exception as e:
output_lines.append(f"✗ Error loading service account: {e}")
else:
output_lines.append("✗ service_account.json NOT FOUND")
output_lines.append("")
output_lines.append("Please rename your downloaded JSON key file to:")
output_lines.append(" service_account.json")
output_lines.append("")
output_lines.append("And place it in:")
output_lines.append(f" {NODE_DIR}/")
return ("\n".join(output_lines),)
elif auth_method == "oauth":
output_lines.append("")
output_lines.append("⚠ OAuth WARNING:")
output_lines.append(" OAuth tokens expire every 7 DAYS in testing mode.")
output_lines.append(" You will need to re-authenticate weekly.")
output_lines.append(" Service Account is recommended for persistent use.")
output_lines.append("")
creds_path = NODE_DIR / 'credentials.json'
token_path = NODE_DIR / 'token.json'
if creds_path.exists():
output_lines.append("✓ OAuth credentials.json found")
if token_path.exists() and not reconnect:
output_lines.append("✓ Existing token.json found - already authenticated")
else:
output_lines.append("→ Run the AutoUploader node to complete OAuth authentication")
if reconnect:
try:
token_path.unlink()
output_lines.append(" Deleted existing token for reconnection")
except:
pass
else:
output_lines.append("✗ credentials.json NOT FOUND")
output_lines.append(" Download OAuth credentials from Google Cloud Console")
env_vars['GOOGLE_DRIVE_FOLDER_ID'] = folder_id
# Handle encryption key
encryption_key = None
if encryption_key_method != "off":
# Check for existing key
existing_key = os.environ.get('comfyui_encryption_key')
if existing_key and not reconnect:
output_lines.append("✓ Encryption key already configured")
env_vars['comfyui_encryption_key'] = existing_key
if existing_key:
encryption_key = existing_key
results.append("Using existing encryption key from environment")
else:
new_key = generate_key()
env_vars['comfyui_encryption_key'] = new_key
output_lines.append("✓ New encryption key generated")
# Generate new key
encryption_key = Fernet.generate_key().decode('utf-8')
results.append("Generated new encryption key")
env_vars['comfyui_encryption_key'] = encryption_key
# Output based on storage method
if storage_method == "env_file" and env_vars:
env_path = NODE_DIR / '.env'
# === OAuth Setup ===
if auth_method == 'oauth':
if not client_id or not client_secret:
return (
"❌ Error: OAuth requires client_id and client_secret.\n\n"
"To get these:\n"
"1. Go to Google Cloud Console → APIs & Services → Credentials\n"
"2. Create OAuth 2.0 Client ID (Desktop app)\n"
"3. Copy Client ID and Client Secret here",
)
# Load existing .env if present
existing_vars = {}
if env_path.exists():
with open(env_path, 'r') as f:
for line in f:
line = line.strip()
if '=' in line and not line.startswith('#'):
key, value = line.split('=', 1)
existing_vars[key] = value
from .gdrive_auth_manager import get_oauth_credentials_for_setup
# Merge with new vars
existing_vars.update(env_vars)
if not auth_code:
# First run - generate auth URL
result = get_oauth_credentials_for_setup(client_id, client_secret)
if 'auth_url' in result:
auth_url = result['auth_url']
# Try to open browser
try:
webbrowser.open(auth_url)
browser_msg = "Browser opened!"
except:
browser_msg = "Could not open browser automatically."
print("\n" + "="*60)
print("[DriveSend Setup] OAuth Authorization Required")
print("="*60)
print(f"\n{browser_msg}\n")
print("If browser didn't open, visit this URL:")
print(f"\n{auth_url}\n")
print("After authorizing, copy the code and paste it in the")
print("'auth_code' field, then run this node again.")
print("="*60 + "\n")
return (
f"🔐 OAuth Step 1: Authorization Required\n\n"
f"{browser_msg}\n\n"
f"1. Sign in to Google and authorize the app\n"
f"2. Copy the authorization code\n"
f"3. Paste it in the 'auth_code' field\n"
f"4. Run this node again",
)
else:
return (f"❌ Error generating auth URL: {result.get('error', 'Unknown error')}",)
# Write .env file
with open(env_path, 'w') as f:
for key, value in existing_vars.items():
f.write(f"{key}={value}\n")
output_lines.append(f"\n✓ Credentials saved to: {env_path}")
else:
# Second run - exchange code for tokens
result = get_oauth_credentials_for_setup(client_id, client_secret, auth_code)
if 'error' in result:
return (f"❌ Error: {result['error']}\n\nTry generating a new auth code.",)
if 'credentials' in result:
creds = result['credentials']
env_vars['GOOGLE_CLIENT_ID'] = creds['client_id']
env_vars['GOOGLE_CLIENT_SECRET'] = creds['client_secret']
env_vars['GOOGLE_REFRESH_TOKEN'] = creds['refresh_token']
results.append("✓ OAuth authorization successful!")
results.append(f"✓ Refresh token obtained")
elif storage_method == "display_only":
output_lines.append("")
output_lines.append("="*50)
output_lines.append("COPY THESE TO RUNPOD SECRETS / ENV VARIABLES:")
output_lines.append("="*50)
# === Service Account Setup ===
elif auth_method == 'service_account':
sa_file = NODE_DIR / 'service_account.json'
# Print to console for copying
print("\n" + "="*60)
print("COPY THESE VALUES TO YOUR ENVIRONMENT VARIABLES:")
print("="*60)
if not sa_file.exists():
return (
"❌ Error: service_account.json not found.\n\n"
"To create one:\n"
"1. Go to Google Cloud Console → IAM & Admin → Service Accounts\n"
"2. Create a service account\n"
"3. Create a JSON key\n"
"4. Rename to 'service_account.json'\n"
"5. Place in: ComfyUI/custom_nodes/ComfyUI_DriveSendNode/\n\n"
"⚠️ NOTE: Service accounts only work with Google Workspace (paid).\n"
"For personal Gmail accounts, use OAuth instead.",
)
for key, value in env_vars.items():
print(f"\n{key}={value}")
print("\n" + "="*60)
print("See console output above for full values to copy")
print("="*60)
# Read and encode service account JSON
try:
with open(sa_file, 'r') as f:
sa_data = json.load(f)
sa_email = sa_data.get('client_email', 'unknown')
sa_json_b64 = base64.b64encode(json.dumps(sa_data).encode()).decode()
env_vars['GOOGLE_SERVICE_ACCOUNT_JSON'] = sa_json_b64
results.append("✓ Service account loaded")
results.append(f"✓ Service account email: {sa_email}")
results.append("")
results.append("⚠️ IMPORTANT: Share your Google Drive folder with:")
results.append(f" {sa_email}")
results.append("")
results.append("⚠️ NOTE: Service accounts require Google Workspace.")
results.append(" They do NOT work with personal Gmail accounts!")
except Exception as e:
return (f"❌ Error reading service_account.json: {e}",)
# Print summary
# === Output Results ===
print("\n" + "="*60)
print("[DriveSend Setup] Configuration complete!")
print("[DriveSend Setup] Configuration Complete!")
print("="*60)
print("\nCopy these values to your Environment Variables:\n")
for key, value in env_vars.items():
# Truncate long values for display
display_value = value if len(value) < 50 else value[:47] + "..."
print(f"{key}={display_value}")
print("\n" + "="*60)
print("See console output above for full values to copy")
print("="*60 + "\n")
status = "\n".join(output_lines)
return (status,)
# Save to .env file if requested
if storage_method == 'env_file':
env_file = NODE_DIR / '.env'
try:
with open(env_file, 'w') as f:
for key, value in env_vars.items():
f.write(f"{key}={value}\n")
results.append(f"✓ Saved to {env_file}")
except Exception as e:
results.append(f"⚠️ Could not save .env file: {e}")
# Build status message
status_lines = [
"✅ DriveSend Setup Complete!",
"",
f"Auth Method: {auth_method}",
f"Folder ID: {folder_id}",
f"Encryption: {'Enabled' if encryption_key else 'Disabled'}",
"",
]
status_lines.extend(results)
status_lines.extend([
"",
"Check the console/terminal for credentials to copy.",
])
if storage_method == 'display_only':
status_lines.extend([
"",
"For RunPod: Copy the values from console to RunPod Secrets.",
])
return ("\n".join(status_lines),)
# Node registration
NODE_CLASS_MAPPINGS = {
"DriveSendSetupNode": DriveSendSetupNode
"DriveSendSetup": DriveSendSetupNode
}
NODE_DISPLAY_NAME_MAPPINGS = {
"DriveSendSetupNode": "DriveSend Setup"
"DriveSendSetup": "DriveSend Setup"
}
+145 -116
View File
@@ -1,6 +1,6 @@
"""
DriveSend AutoUploader Node
Monitors ComfyUI output directory and uploads files to Google Drive
Monitors output folder and uploads new files to Google Drive
"""
import os
@@ -9,147 +9,176 @@ from pathlib import Path
from .monitor_output import start_monitor, stop_monitor, get_monitor
# Default output directory
DEFAULT_OUTPUT_DIR = Path(__file__).parent.parent.parent / "output"
class DriveSendAutoUploaderNode:
"""
ComfyUI node for automatic Google Drive uploads with optional encryption.
"""
"""ComfyUI node for automatic Google Drive uploads."""
CATEGORY = "DriveSend"
FUNCTION = "run"
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("status",)
OUTPUT_NODE = True
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"watch_directory": ("STRING", {
"default": str(DEFAULT_OUTPUT_DIR),
"multiline": False,
"placeholder": "Directory to monitor for new files"
}),
"auth_method": (["service_account", "oauth"],),
"auth_method": (["oauth", "service_account"], {"default": "oauth"}),
"run_process": ("BOOLEAN", {"default": True}),
"enable_encryption": ("BOOLEAN", {"default": False}),
"Subfolder_Monitor": ("BOOLEAN", {"default": True}),
"Post_Delete_Enc": ("BOOLEAN", {"default": False}),
},
"optional": {
"watch_folder": ("STRING", {
"default": "",
"multiline": False,
"tooltip": "Folder to monitor (leave blank for ComfyUI output folder)"
}),
"folder_id": ("STRING", {
"default": "",
"multiline": False,
"placeholder": "Google Drive Folder ID (uses env var if blank)"
"tooltip": "Google Drive folder ID (uses env var if blank)"
}),
"owner_email": ("STRING", {
"default": "",
"multiline": False,
"placeholder": "Your Gmail address (required for service account)"
}),
"enable_encryption": ("BOOLEAN", {"default": False}),
"Post_Delete_Enc": ("BOOLEAN", {"default": False}),
"Subfolder_Monitor": ("BOOLEAN", {"default": True}),
"any_input": ("*", {}), # Passthrough for workflow chaining
}
}
RETURN_TYPES = ("STRING", "*")
RETURN_NAMES = ("status", "passthrough")
FUNCTION = "run"
CATEGORY = "DriveSend"
def run(
self,
watch_directory,
auth_method,
run_process,
folder_id="",
owner_email="",
enable_encryption=False,
Post_Delete_Enc=False,
Subfolder_Monitor=True,
any_input=None
):
status_lines = []
def run(self, auth_method, run_process, enable_encryption, Subfolder_Monitor,
Post_Delete_Enc, watch_folder="", folder_id=""):
"""
Start or stop the Google Drive upload monitor.
# Get current monitor state
current_monitor = get_monitor()
Args:
auth_method: 'oauth' or 'service_account'
run_process: True to start, False to stop
enable_encryption: Encrypt files before upload
Subfolder_Monitor: Monitor subfolders recursively
Post_Delete_Enc: Delete .enc files after upload
watch_folder: Folder to monitor (default: ComfyUI output)
folder_id: Google Drive folder ID (default: from env var)
"""
if run_process:
# Start or restart monitor
if current_monitor and current_monitor.is_running():
# Stop if requested
if not run_process:
monitor = get_monitor()
if monitor and monitor.is_running():
stop_monitor()
status_lines.append("Stopped existing monitor")
# Validate watch directory
watch_path = Path(watch_directory)
if not watch_path.exists():
try:
watch_path.mkdir(parents=True, exist_ok=True)
status_lines.append(f"Created watch directory: {watch_path}")
except Exception as e:
status_lines.append(f"Error: Could not create watch directory: {e}")
return ("\n".join(status_lines), any_input)
# Get owner email from field or environment
effective_owner_email = owner_email if owner_email else os.environ.get('GOOGLE_OWNER_EMAIL', '')
# Warn if using service account without owner email
if auth_method == 'service_account' and not effective_owner_email:
status_lines.append("⚠ WARNING: No owner_email set!")
status_lines.append(" Service accounts have 0 GB storage quota.")
status_lines.append(" Set owner_email to your Gmail or set GOOGLE_OWNER_EMAIL env var.")
status_lines.append("")
# Set owner email in environment for upload module
if effective_owner_email:
os.environ['GOOGLE_OWNER_EMAIL'] = effective_owner_email
# Start monitor
try:
monitor = start_monitor(
watch_dir=str(watch_path),
folder_id=folder_id if folder_id else None,
recursive=Subfolder_Monitor,
enable_encryption=enable_encryption,
post_delete_enc=Post_Delete_Enc,
auth_method=auth_method,
owner_email=effective_owner_email
)
status_lines.append(f"✓ Monitor started")
status_lines.append(f" Directory: {watch_path}")
status_lines.append(f" Auth: {auth_method}")
status_lines.append(f" Recursive: {Subfolder_Monitor}")
status_lines.append(f" Encryption: {enable_encryption}")
if effective_owner_email:
status_lines.append(f" Owner Email: {effective_owner_email}")
if folder_id:
status_lines.append(f" Folder ID: {folder_id}")
else:
env_folder = os.environ.get('GOOGLE_DRIVE_FOLDER_ID')
if env_folder:
status_lines.append(f" Folder ID: {env_folder} (from env)")
else:
status_lines.append(" Folder ID: Root (no folder specified)")
except Exception as e:
status_lines.append(f"Error starting monitor: {e}")
else:
# Stop monitor
if current_monitor and current_monitor.is_running():
stop_monitor()
status_lines.append("✓ Monitor stopped")
return ("⏹️ DriveSend monitor stopped.",)
else:
status_lines.append("Monitor not running")
return ("ℹ️ Monitor was not running.",)
status = "\n".join(status_lines)
return (status, any_input)
# Determine watch folder
if watch_folder:
watch_path = Path(watch_folder)
else:
# Default to ComfyUI output folder
# Try common locations
possible_paths = [
Path("/workspace/ComfyUI/output"), # RunPod
Path("./output"), # Relative
Path(__file__).parent.parent.parent / "output", # Relative to node
]
watch_path = None
for p in possible_paths:
if p.exists():
watch_path = p
break
if not watch_path:
return ("❌ Error: Could not find ComfyUI output folder. Please specify watch_folder.",)
if not watch_path.exists():
return (f"❌ Error: Watch folder does not exist: {watch_path}",)
# Get folder ID from parameter or environment
effective_folder_id = folder_id or os.environ.get('GOOGLE_DRIVE_FOLDER_ID')
if not effective_folder_id:
return (
"❌ Error: No folder_id provided.\n\n"
"Either:\n"
"1. Set the folder_id parameter in this node\n"
"2. Set GOOGLE_DRIVE_FOLDER_ID environment variable\n"
"3. Run the DriveSend Setup node first",
)
# Check for encryption key if encryption is enabled
if enable_encryption:
enc_key = os.environ.get('comfyui_encryption_key')
if not enc_key:
return (
"❌ Error: Encryption enabled but no key found.\n\n"
"Set comfyui_encryption_key environment variable\n"
"or run DriveSend Setup with encryption enabled.",
)
# Validate auth method requirements
if auth_method == 'oauth':
client_id = os.environ.get('GOOGLE_CLIENT_ID')
client_secret = os.environ.get('GOOGLE_CLIENT_SECRET')
refresh_token = os.environ.get('GOOGLE_REFRESH_TOKEN')
if not all([client_id, client_secret, refresh_token]):
# Check for token.json file as fallback
token_file = Path(__file__).parent / 'token.json'
if not token_file.exists():
return (
"❌ Error: OAuth credentials not found.\n\n"
"Set these environment variables:\n"
"- GOOGLE_CLIENT_ID\n"
"- GOOGLE_CLIENT_SECRET\n"
"- GOOGLE_REFRESH_TOKEN\n\n"
"Or run DriveSend Setup to authorize.",
)
elif auth_method == 'service_account':
sa_json = os.environ.get('GOOGLE_SERVICE_ACCOUNT_JSON')
sa_file = Path(__file__).parent / 'service_account.json'
if not sa_json and not sa_file.exists():
return (
"❌ Error: Service account credentials not found.\n\n"
"Either:\n"
"1. Set GOOGLE_SERVICE_ACCOUNT_JSON environment variable (base64)\n"
"2. Place service_account.json in the node folder\n\n"
"⚠️ NOTE: Service accounts only work with Google Workspace (paid).\n"
"For personal Gmail, use OAuth instead.",
)
# Start the monitor
try:
monitor = start_monitor(
watch_dir=str(watch_path),
folder_id=effective_folder_id,
recursive=Subfolder_Monitor,
enable_encryption=enable_encryption,
post_delete_enc=Post_Delete_Enc,
auth_method=auth_method
)
status_lines = [
"✅ DriveSend monitor started!",
"",
f"📁 Watching: {watch_path}",
f"☁️ Uploading to: Google Drive folder {effective_folder_id[:20]}...",
f"🔐 Auth method: {auth_method}",
f"🔒 Encryption: {'Enabled' if enable_encryption else 'Disabled'}",
f"📂 Subfolder monitoring: {'Enabled' if Subfolder_Monitor else 'Disabled'}",
"",
"New files will be uploaded automatically.",
"Set run_process to False to stop.",
]
return ("\n".join(status_lines),)
except Exception as e:
return (f"❌ Error starting monitor: {e}",)
# Node registration
NODE_CLASS_MAPPINGS = {
"DriveSendAutoUploaderNode": DriveSendAutoUploaderNode
"DriveSendAutoUploader": DriveSendAutoUploaderNode
}
NODE_DISPLAY_NAME_MAPPINGS = {
"DriveSendAutoUploaderNode": "DriveSend AutoUploader"
"DriveSendAutoUploader": "DriveSend AutoUploader"
}
+265 -164
View File
@@ -1,6 +1,6 @@
"""
Google Drive Authentication Manager
Handles both Service Account and OAuth 2.0 authentication methods
Handles OAuth 2.0 and Service Account authentication with auto-refresh support
"""
import os
@@ -10,176 +10,277 @@ from pathlib import Path
from google.oauth2 import service_account
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
from googleapiclient.discovery import build
# If modifying scopes, delete token.json
# Scopes required for Google Drive uploads
SCOPES = ['https://www.googleapis.com/auth/drive.file']
# Path to credential files
NODE_DIR = Path(__file__).parent
def get_service_from_service_account(service_account_path=None, service_account_json=None):
"""
Authenticate using a Service Account.
Args:
service_account_path: Path to service_account.json file
service_account_json: Base64-encoded service account JSON (for env var storage)
Returns:
Google Drive API service object
"""
credentials = None
# Try environment variable first (base64 encoded)
env_json = os.environ.get('GOOGLE_SERVICE_ACCOUNT_JSON')
if env_json:
try:
# Check if it's base64 encoded
try:
decoded = base64.b64decode(env_json).decode('utf-8')
service_account_info = json.loads(decoded)
except:
# Maybe it's just raw JSON
service_account_info = json.loads(env_json)
credentials = service_account.Credentials.from_service_account_info(
service_account_info, scopes=SCOPES
)
print("[DriveSend] Authenticated using service account from environment variable")
except Exception as e:
print(f"[DriveSend] Failed to parse service account from env: {e}")
# Try provided JSON string
if not credentials and service_account_json:
try:
try:
decoded = base64.b64decode(service_account_json).decode('utf-8')
service_account_info = json.loads(decoded)
except:
service_account_info = json.loads(service_account_json)
credentials = service_account.Credentials.from_service_account_info(
service_account_info, scopes=SCOPES
)
print("[DriveSend] Authenticated using provided service account JSON")
except Exception as e:
print(f"[DriveSend] Failed to parse provided service account JSON: {e}")
# Try file path
if not credentials and service_account_path:
path = Path(service_account_path)
if path.exists():
try:
credentials = service_account.Credentials.from_service_account_file(
str(path), scopes=SCOPES
)
print(f"[DriveSend] Authenticated using service account file: {path}")
except Exception as e:
print(f"[DriveSend] Failed to load service account file: {e}")
# Try default location
if not credentials:
default_path = NODE_DIR / 'service_account.json'
if default_path.exists():
try:
credentials = service_account.Credentials.from_service_account_file(
str(default_path), scopes=SCOPES
)
print(f"[DriveSend] Authenticated using default service account file")
except Exception as e:
print(f"[DriveSend] Failed to load default service account file: {e}")
if not credentials:
raise ValueError(
"No valid service account credentials found. Please provide:\n"
" - GOOGLE_SERVICE_ACCOUNT_JSON environment variable (base64 encoded), or\n"
" - service_account.json file in the node directory, or\n"
" - Path to service account JSON file"
)
return build('drive', 'v3', credentials=credentials)
def get_service_from_oauth(credentials_path=None):
"""
Authenticate using OAuth 2.0 (for local/personal use).
Args:
credentials_path: Path to credentials.json file (OAuth client ID)
Returns:
Google Drive API service object
"""
creds = None
token_path = NODE_DIR / 'token.json'
# Try to load existing token
if token_path.exists():
try:
creds = Credentials.from_authorized_user_file(str(token_path), SCOPES)
print("[DriveSend] Loaded existing OAuth token")
except Exception as e:
print(f"[DriveSend] Failed to load token: {e}")
# If no valid credentials, authenticate
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
try:
creds.refresh(Request())
print("[DriveSend] Refreshed OAuth token")
except Exception as e:
print(f"[DriveSend] Failed to refresh token: {e}")
creds = None
if not creds:
# Need to do full OAuth flow
creds_file = credentials_path or (NODE_DIR / 'credentials.json')
if not Path(creds_file).exists():
raise ValueError(
f"OAuth credentials file not found: {creds_file}\n"
"Please download credentials.json from Google Cloud Console."
)
flow = InstalledAppFlow.from_client_secrets_file(str(creds_file), SCOPES)
creds = flow.run_local_server(port=0)
print("[DriveSend] Completed OAuth authentication")
# Save the credentials for next time
with open(token_path, 'w') as token:
token.write(creds.to_json())
print(f"[DriveSend] Saved OAuth token to {token_path}")
return build('drive', 'v3', credentials=creds)
def get_drive_service(auth_method='service_account', **kwargs):
"""
Get an authenticated Google Drive API service.
Args:
auth_method: 'service_account' or 'oauth'
**kwargs: Additional arguments for the specific auth method
Returns:
Google Drive API service object
"""
if auth_method == 'service_account':
return get_service_from_service_account(
service_account_path=kwargs.get('service_account_path'),
service_account_json=kwargs.get('service_account_json')
)
elif auth_method == 'oauth':
return get_service_from_oauth(
credentials_path=kwargs.get('credentials_path')
)
else:
raise ValueError(f"Unknown auth method: {auth_method}. Use 'service_account' or 'oauth'")
SERVICE_ACCOUNT_FILE = NODE_DIR / 'service_account.json'
CLIENT_SECRET_FILE = NODE_DIR / 'client_secret.json'
TOKEN_FILE = NODE_DIR / 'token.json'
def get_folder_id():
"""Get the target folder ID from environment or return None (root)."""
"""Get the Google Drive folder ID from environment or return None."""
return os.environ.get('GOOGLE_DRIVE_FOLDER_ID')
def get_service_account_credentials():
"""
Get credentials from service account JSON file or environment variable.
Returns:
google.oauth2.service_account.Credentials or None
"""
# Try environment variable first (base64 encoded JSON)
sa_json_b64 = os.environ.get('GOOGLE_SERVICE_ACCOUNT_JSON')
if sa_json_b64:
try:
sa_json = base64.b64decode(sa_json_b64).decode('utf-8')
sa_info = json.loads(sa_json)
credentials = service_account.Credentials.from_service_account_info(
sa_info, scopes=SCOPES
)
print("[DriveSend] Using service account from environment variable")
return credentials
except Exception as e:
print(f"[DriveSend] Failed to load service account from env: {e}")
# Try file
if SERVICE_ACCOUNT_FILE.exists():
try:
credentials = service_account.Credentials.from_service_account_file(
str(SERVICE_ACCOUNT_FILE), scopes=SCOPES
)
print("[DriveSend] Using service account from file")
return credentials
except Exception as e:
print(f"[DriveSend] Failed to load service account file: {e}")
return None
def get_oauth_credentials():
"""
Get OAuth 2.0 credentials with automatic refresh.
Checks for credentials in this order:
1. Environment variables (GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_REFRESH_TOKEN)
2. Saved token.json file
3. Interactive OAuth flow (requires browser)
Returns:
google.oauth2.credentials.Credentials or None
"""
credentials = None
# Option 1: Try environment variables (for RunPod)
client_id = os.environ.get('GOOGLE_CLIENT_ID')
client_secret = os.environ.get('GOOGLE_CLIENT_SECRET')
refresh_token = os.environ.get('GOOGLE_REFRESH_TOKEN')
if client_id and client_secret and refresh_token:
try:
credentials = Credentials(
token=None,
refresh_token=refresh_token,
token_uri='https://oauth2.googleapis.com/token',
client_id=client_id,
client_secret=client_secret,
scopes=SCOPES
)
# Force refresh to get a valid access token
credentials.refresh(Request())
print("[DriveSend] OAuth credentials loaded from environment variables")
print("[DriveSend] ✓ Access token refreshed successfully")
return credentials
except Exception as e:
print(f"[DriveSend] Failed to refresh OAuth token from env: {e}")
# Option 2: Try saved token file
if TOKEN_FILE.exists():
try:
credentials = Credentials.from_authorized_user_file(str(TOKEN_FILE), SCOPES)
if credentials and credentials.expired and credentials.refresh_token:
credentials.refresh(Request())
# Save refreshed token
with open(TOKEN_FILE, 'w') as token:
token.write(credentials.to_json())
print("[DriveSend] OAuth credentials loaded and refreshed from token file")
elif credentials and credentials.valid:
print("[DriveSend] OAuth credentials loaded from token file")
return credentials
except Exception as e:
print(f"[DriveSend] Failed to load token file: {e}")
# Option 3: Interactive OAuth flow (only works with browser)
if CLIENT_SECRET_FILE.exists():
try:
from google_auth_oauthlib.flow import InstalledAppFlow
print("[DriveSend] Starting OAuth authorization flow...")
print("[DriveSend] A browser window will open for authorization.")
flow = InstalledAppFlow.from_client_secrets_file(
str(CLIENT_SECRET_FILE), SCOPES
)
credentials = flow.run_local_server(port=0)
# Save the credentials for future use
with open(TOKEN_FILE, 'w') as token:
token.write(credentials.to_json())
print("[DriveSend] ✓ OAuth authorization complete")
print(f"[DriveSend] Token saved to: {TOKEN_FILE}")
return credentials
except Exception as e:
print(f"[DriveSend] OAuth flow failed: {e}")
print("[DriveSend] Note: OAuth flow requires a browser. For headless servers,")
print("[DriveSend] complete setup locally first and copy credentials to environment variables.")
return None
def get_drive_service(auth_method='oauth', **kwargs):
"""
Get an authenticated Google Drive service.
Args:
auth_method: 'oauth' or 'service_account'
**kwargs: Additional arguments (unused, for compatibility)
Returns:
Google Drive API service object
Raises:
Exception if authentication fails
"""
credentials = None
if auth_method == 'service_account':
credentials = get_service_account_credentials()
if not credentials:
raise Exception(
"Service account credentials not found. "
"Place service_account.json in the node folder or set GOOGLE_SERVICE_ACCOUNT_JSON environment variable.\n"
"NOTE: Service accounts only work with Google Workspace accounts (paid). "
"For personal Gmail, use OAuth instead."
)
elif auth_method == 'oauth':
credentials = get_oauth_credentials()
if not credentials:
raise Exception(
"OAuth credentials not found. Options:\n"
"1. Set GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, and GOOGLE_REFRESH_TOKEN environment variables\n"
"2. Place client_secret.json in the node folder and run setup to authorize\n"
"3. Run setup locally first if using a headless server"
)
else:
raise Exception(f"Unknown auth method: {auth_method}. Use 'oauth' or 'service_account'")
# Build and return the Drive service
service = build('drive', 'v3', credentials=credentials)
return service
def get_oauth_credentials_for_setup(client_id, client_secret, auth_code=None):
"""
Get OAuth credentials during setup process.
If auth_code is provided, exchanges it for tokens.
Otherwise, returns the authorization URL.
Args:
client_id: OAuth client ID
client_secret: OAuth client secret
auth_code: Authorization code from OAuth flow (optional)
Returns:
dict with either 'auth_url' or 'credentials' containing token info
"""
from google_auth_oauthlib.flow import Flow
# Create flow from client config
client_config = {
"installed": {
"client_id": client_id,
"client_secret": client_secret,
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"redirect_uris": ["urn:ietf:wg:oauth:2.0:oob", "http://localhost"]
}
}
flow = Flow.from_client_config(client_config, scopes=SCOPES)
flow.redirect_uri = "urn:ietf:wg:oauth:2.0:oob"
if not auth_code:
# Generate authorization URL
auth_url, _ = flow.authorization_url(
access_type='offline',
include_granted_scopes='true',
prompt='consent' # Force consent to get refresh token
)
return {'auth_url': auth_url}
else:
# Exchange auth code for tokens
try:
flow.fetch_token(code=auth_code)
credentials = flow.credentials
return {
'credentials': {
'client_id': client_id,
'client_secret': client_secret,
'refresh_token': credentials.refresh_token,
'token': credentials.token,
'token_uri': credentials.token_uri,
'scopes': list(credentials.scopes) if credentials.scopes else SCOPES
}
}
except Exception as e:
return {'error': str(e)}
def refresh_access_token(client_id, client_secret, refresh_token):
"""
Refresh an access token using the refresh token.
This is called automatically but can also be used to verify credentials.
Args:
client_id: OAuth client ID
client_secret: OAuth client secret
refresh_token: OAuth refresh token
Returns:
dict with 'access_token' or 'error'
"""
import requests
try:
response = requests.post(
'https://oauth2.googleapis.com/token',
data={
'client_id': client_id,
'client_secret': client_secret,
'refresh_token': refresh_token,
'grant_type': 'refresh_token'
}
)
if response.status_code == 200:
data = response.json()
return {'access_token': data.get('access_token')}
else:
return {'error': f"Token refresh failed: {response.text}"}
except Exception as e:
return {'error': str(e)}
+42 -96
View File
@@ -1,6 +1,6 @@
"""
Google Drive Upload Module
Handles file uploads to Google Drive with integrity verification and ownership transfer
Handles file uploads to Google Drive with integrity verification
"""
import os
@@ -42,52 +42,7 @@ def calculate_sha256(file_path):
return sha256_hash.hexdigest()
def transfer_ownership(service, file_id, owner_email):
"""
Transfer file ownership to a personal Google account.
This is required because service accounts have 0 GB storage quota.
By transferring ownership, the file counts against the personal account's quota.
Args:
service: Google Drive API service object
file_id: ID of the uploaded file
owner_email: Email address to transfer ownership to
Returns:
bool: True if transfer successful, False otherwise
"""
if not owner_email:
return False
try:
# Create permission with ownership transfer
permission = {
'type': 'user',
'role': 'owner',
'emailAddress': owner_email
}
service.permissions().create(
fileId=file_id,
body=permission,
transferOwnership=True,
supportsAllDrives=True
).execute()
print(f"[DriveSend] ✓ Ownership transferred to: {owner_email}")
return True
except HttpError as e:
print(f"[DriveSend] ⚠ Could not transfer ownership: {e}")
return False
except Exception as e:
print(f"[DriveSend] ⚠ Ownership transfer error: {e}")
return False
def upload_file(file_path, folder_id=None, service=None, auth_method='service_account',
owner_email=None, **auth_kwargs):
def upload_file(file_path, folder_id=None, service=None, auth_method='oauth', **kwargs):
"""
Upload a file to Google Drive.
@@ -95,9 +50,8 @@ def upload_file(file_path, folder_id=None, service=None, auth_method='service_ac
file_path: Path to the file to upload
folder_id: Google Drive folder ID (optional, uses env var or root if not provided)
service: Existing Drive service object (optional)
auth_method: Authentication method ('service_account' or 'oauth')
owner_email: Email to transfer ownership to (required for service account uploads)
**auth_kwargs: Additional auth arguments
auth_method: Authentication method ('oauth' or 'service_account')
**kwargs: Additional arguments (for compatibility)
Returns:
dict with upload result including file ID and verification status
@@ -110,7 +64,7 @@ def upload_file(file_path, folder_id=None, service=None, auth_method='service_ac
# Get or create service
if service is None:
try:
service = get_drive_service(auth_method, **auth_kwargs)
service = get_drive_service(auth_method)
except Exception as e:
return {'success': False, 'error': f'Authentication failed: {e}'}
@@ -118,10 +72,6 @@ def upload_file(file_path, folder_id=None, service=None, auth_method='service_ac
if folder_id is None:
folder_id = get_folder_id()
# Get owner email from parameter or environment
if owner_email is None:
owner_email = os.environ.get('GOOGLE_OWNER_EMAIL')
# Calculate local hash for verification
local_hash = calculate_sha256(file_path)
@@ -157,18 +107,6 @@ def upload_file(file_path, folder_id=None, service=None, auth_method='service_ac
remote_size = int(file.get('size', 0))
local_size = file_path.stat().st_size
# Transfer ownership if using service account
ownership_transferred = False
if auth_method == 'service_account' and owner_email:
ownership_transferred = transfer_ownership(service, file_id, owner_email)
if not ownership_transferred:
print(f"[DriveSend] ⚠ Warning: File uploaded but ownership not transferred.")
print(f"[DriveSend] The file may not persist if service account quota is exceeded.")
print(f"[DriveSend] Make sure GOOGLE_OWNER_EMAIL is set to your personal Gmail.")
elif auth_method == 'service_account' and not owner_email:
print(f"[DriveSend] ⚠ Warning: No owner_email set. Service accounts have 0 GB quota.")
print(f"[DriveSend] Set GOOGLE_OWNER_EMAIL environment variable to your Gmail address.")
# Verify upload
size_match = remote_size == local_size
@@ -180,8 +118,7 @@ def upload_file(file_path, folder_id=None, service=None, auth_method='service_ac
'file_name': file.get('name'),
'size': remote_size,
'local_hash': local_hash,
'verified': True,
'ownership_transferred': ownership_transferred
'verified': True
}
else:
print(f"[DriveSend] ⚠ Upload size mismatch: local={local_size}, remote={remote_size}")
@@ -192,14 +129,25 @@ def upload_file(file_path, folder_id=None, service=None, auth_method='service_ac
'size': remote_size,
'local_hash': local_hash,
'verified': False,
'warning': 'Size mismatch - file may be corrupted',
'ownership_transferred': ownership_transferred
'warning': 'Size mismatch - file may be corrupted'
}
except HttpError as e:
error_msg = f'Google Drive API error: {e}'
print(f"[DriveSend] ✗ {error_msg}")
return {'success': False, 'error': error_msg}
error_msg = str(e)
# Provide helpful error messages
if 'storageQuotaExceeded' in error_msg:
print(f"[DriveSend] ✗ Storage quota exceeded!")
print(f"[DriveSend] If using service account with personal Gmail, this won't work.")
print(f"[DriveSend] Service accounts require Google Workspace (paid).")
print(f"[DriveSend] For personal Gmail, use OAuth authentication instead.")
return {
'success': False,
'error': 'Storage quota exceeded. Service accounts do not work with personal Gmail. Use OAuth instead.'
}
print(f"[DriveSend] ✗ Google Drive API error: {error_msg}")
return {'success': False, 'error': f'Google Drive API error: {error_msg}'}
except Exception as e:
error_msg = f'Upload failed: {e}'
@@ -207,7 +155,7 @@ def upload_file(file_path, folder_id=None, service=None, auth_method='service_ac
return {'success': False, 'error': error_msg}
def create_folder(folder_name, parent_folder_id=None, service=None, auth_method='service_account', **auth_kwargs):
def create_folder(folder_name, parent_folder_id=None, service=None, auth_method='oauth'):
"""
Create a folder in Google Drive.
@@ -216,14 +164,13 @@ def create_folder(folder_name, parent_folder_id=None, service=None, auth_method=
parent_folder_id: Parent folder ID (optional)
service: Existing Drive service object (optional)
auth_method: Authentication method
**auth_kwargs: Additional auth arguments
Returns:
dict with folder creation result including folder ID
"""
if service is None:
try:
service = get_drive_service(auth_method, **auth_kwargs)
service = get_drive_service(auth_method)
except Exception as e:
return {'success': False, 'error': f'Authentication failed: {e}'}
@@ -253,46 +200,45 @@ def create_folder(folder_name, parent_folder_id=None, service=None, auth_method=
return {'success': False, 'error': f'Failed to create folder: {e}'}
def find_folder(folder_name, parent_folder_id=None, service=None, auth_method='service_account', **auth_kwargs):
def list_files(folder_id=None, service=None, auth_method='oauth', max_results=100):
"""
Find a folder by name in Google Drive.
List files in a Google Drive folder.
Args:
folder_name: Name of the folder to find
parent_folder_id: Parent folder ID to search in (optional)
folder_id: Folder ID to list (optional, lists root if not provided)
service: Existing Drive service object (optional)
auth_method: Authentication method
**auth_kwargs: Additional auth arguments
max_results: Maximum number of files to return
Returns:
Folder ID if found, None otherwise
dict with list of files or error
"""
if service is None:
try:
service = get_drive_service(auth_method, **auth_kwargs)
service = get_drive_service(auth_method)
except Exception as e:
print(f"[DriveSend] Authentication failed: {e}")
return None
return {'success': False, 'error': f'Authentication failed: {e}'}
query = f"name='{folder_name}' and mimeType='application/vnd.google-apps.folder' and trashed=false"
if folder_id is None:
folder_id = get_folder_id()
if parent_folder_id:
query += f" and '{parent_folder_id}' in parents"
query = f"'{folder_id}' in parents and trashed=false" if folder_id else "trashed=false"
try:
results = service.files().list(
q=query,
fields='files(id, name)',
pageSize=1,
fields='files(id, name, size, mimeType, createdTime)',
pageSize=max_results,
supportsAllDrives=True,
includeItemsFromAllDrives=True
).execute()
files = results.get('files', [])
if files:
return files[0].get('id')
return None
return {
'success': True,
'files': files,
'count': len(files)
}
except HttpError as e:
print(f"[DriveSend] Error finding folder: {e}")
return None
return {'success': False, 'error': f'Failed to list files: {e}'}