Files
MachinePainting 2c49196561 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
2026-02-01 02:34:43 -08:00

245 lines
7.5 KiB
Python

"""
Google Drive Upload Module
Handles file uploads to Google Drive with integrity verification
"""
import os
import hashlib
from pathlib import Path
from googleapiclient.http import MediaFileUpload
from googleapiclient.errors import HttpError
from .gdrive_auth_manager import get_drive_service, get_folder_id
# MIME type mapping
MIME_TYPES = {
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.webp': 'image/webp',
'.gif': 'image/gif',
'.mp4': 'video/mp4',
'.avi': 'video/x-msvideo',
'.mov': 'video/quicktime',
'.enc': 'application/octet-stream',
}
def get_mime_type(file_path):
"""Get MIME type based on file extension."""
ext = Path(file_path).suffix.lower()
return MIME_TYPES.get(ext, 'application/octet-stream')
def calculate_sha256(file_path):
"""Calculate SHA256 hash of a file."""
sha256_hash = hashlib.sha256()
with open(file_path, 'rb') as f:
for chunk in iter(lambda: f.read(8192), b''):
sha256_hash.update(chunk)
return sha256_hash.hexdigest()
def upload_file(file_path, folder_id=None, service=None, auth_method='oauth', **kwargs):
"""
Upload a file to Google Drive.
Args:
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 ('oauth' or 'service_account')
**kwargs: Additional arguments (for compatibility)
Returns:
dict with upload result including file ID and verification status
"""
file_path = Path(file_path)
if not file_path.exists():
return {'success': False, 'error': f'File not found: {file_path}'}
# Get or create service
if service is None:
try:
service = get_drive_service(auth_method)
except Exception as e:
return {'success': False, 'error': f'Authentication failed: {e}'}
# Get folder ID
if folder_id is None:
folder_id = get_folder_id()
# Calculate local hash for verification
local_hash = calculate_sha256(file_path)
# Prepare file metadata
file_metadata = {
'name': file_path.name,
'description': f'Uploaded by ComfyUI DriveSend. SHA256: {local_hash}'
}
if folder_id:
file_metadata['parents'] = [folder_id]
# Prepare media
mime_type = get_mime_type(file_path)
media = MediaFileUpload(
str(file_path),
mimetype=mime_type,
resumable=True
)
try:
# Upload file
print(f"[DriveSend] Uploading: {file_path.name}")
file = service.files().create(
body=file_metadata,
media_body=media,
fields='id, name, size, md5Checksum',
supportsAllDrives=True
).execute()
file_id = file.get('id')
remote_size = int(file.get('size', 0))
local_size = file_path.stat().st_size
# Verify upload
size_match = remote_size == local_size
if size_match:
print(f"[DriveSend] ✓ Upload verified: {file_path.name} (ID: {file_id})")
return {
'success': True,
'file_id': file_id,
'file_name': file.get('name'),
'size': remote_size,
'local_hash': local_hash,
'verified': True
}
else:
print(f"[DriveSend] ⚠ Upload size mismatch: local={local_size}, remote={remote_size}")
return {
'success': True,
'file_id': file_id,
'file_name': file.get('name'),
'size': remote_size,
'local_hash': local_hash,
'verified': False,
'warning': 'Size mismatch - file may be corrupted'
}
except HttpError as e:
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}'
print(f"[DriveSend] ✗ {error_msg}")
return {'success': False, 'error': error_msg}
def create_folder(folder_name, parent_folder_id=None, service=None, auth_method='oauth'):
"""
Create a folder in Google Drive.
Args:
folder_name: Name of the folder to create
parent_folder_id: Parent folder ID (optional)
service: Existing Drive service object (optional)
auth_method: Authentication method
Returns:
dict with folder creation result including folder ID
"""
if service is None:
try:
service = get_drive_service(auth_method)
except Exception as e:
return {'success': False, 'error': f'Authentication failed: {e}'}
file_metadata = {
'name': folder_name,
'mimeType': 'application/vnd.google-apps.folder'
}
if parent_folder_id:
file_metadata['parents'] = [parent_folder_id]
try:
folder = service.files().create(
body=file_metadata,
fields='id, name',
supportsAllDrives=True
).execute()
print(f"[DriveSend] Created folder: {folder_name} (ID: {folder.get('id')})")
return {
'success': True,
'folder_id': folder.get('id'),
'folder_name': folder.get('name')
}
except HttpError as e:
return {'success': False, 'error': f'Failed to create folder: {e}'}
def list_files(folder_id=None, service=None, auth_method='oauth', max_results=100):
"""
List files in a Google Drive folder.
Args:
folder_id: Folder ID to list (optional, lists root if not provided)
service: Existing Drive service object (optional)
auth_method: Authentication method
max_results: Maximum number of files to return
Returns:
dict with list of files or error
"""
if service is None:
try:
service = get_drive_service(auth_method)
except Exception as e:
return {'success': False, 'error': f'Authentication failed: {e}'}
if folder_id is None:
folder_id = get_folder_id()
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, size, mimeType, createdTime)',
pageSize=max_results,
supportsAllDrives=True,
includeItemsFromAllDrives=True
).execute()
files = results.get('files', [])
return {
'success': True,
'files': files,
'count': len(files)
}
except HttpError as e:
return {'success': False, 'error': f'Failed to list files: {e}'}