import os import shutil import gdown from flask import Blueprint, render_template, request, jsonify, current_app, send_from_directory, send_file, url_for, redirect, session from flask_login import login_required, current_user from database import get_db_connection from datetime import datetime import threading import re import json # Allow OAuth over HTTP for local testing os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1' from gdrive_service import get_drive_service, create_flow, list_drive_files, download_file_to_stream, get_file_metadata drive_bp = Blueprint('drive', __name__) DRIVE_SYNC_FOLDER = 'drive_sync' def extract_drive_id(url): # Extracts Drive ID (File or Folder) - simplified regex for ~25+ chars match = re.search(r'[-\w]{25,}', url) return match.group(0) if match else None def get_sync_folder_path(source_name=None): base = os.path.join(current_app.config['OUTPUT_FOLDER'], DRIVE_SYNC_FOLDER) if not os.path.exists(base): os.makedirs(base) if source_name: path = os.path.join(base, source_name) if not os.path.exists(path): os.makedirs(path) return path return base @drive_bp.route('/drive_manager') @login_required def drive_manager(): conn = get_db_connection() sources = conn.execute('SELECT * FROM drive_sources WHERE user_id = ? ORDER BY created_at DESC', (current_user.id,)).fetchall() # Get last 4 opened PDFs recent_pdfs = conn.execute(''' SELECT file_id, filename, opened_at FROM pdf_access_history WHERE user_id = ? ORDER BY opened_at DESC LIMIT 4 ''', (current_user.id,)).fetchall() conn.close() # Check Drive API Status drive_connected = bool(current_user.google_token) return render_template('drive_manager.html', sources=[dict(s) for s in sources], drive_connected=drive_connected, recent_pdfs=[dict(p) for p in recent_pdfs]) @drive_bp.route('/drive/connect') @login_required def connect_drive(): try: redirect_uri = 'http://localhost' flow = create_flow(redirect_uri) authorization_url, state = flow.authorization_url( access_type='offline', include_granted_scopes='true') session['oauth_state'] = state return render_template('drive_connect_manual.html', auth_url=authorization_url) except FileNotFoundError: return "client_secret.json not found. Please upload it to the app root via Settings.", 404 except Exception as e: return f"Error creating flow: {e}", 500 @drive_bp.route('/drive/manual_callback', methods=['POST']) @login_required def manual_callback(): state = session.get('oauth_state') full_url = request.form.get('full_url') if not full_url: return "URL is required", 400 try: redirect_uri = 'http://localhost' flow = create_flow(redirect_uri) flow.fetch_token(authorization_response=full_url) credentials = flow.credentials token_json = credentials.to_json() conn = get_db_connection() conn.execute('UPDATE users SET google_token = ? WHERE id = ?', (token_json, current_user.id)) conn.commit() conn.close() current_user.google_token = token_json return redirect(url_for('drive.drive_manager')) except Exception as e: return f"Auth failed: {e}

Make sure you copied the full URL correctly.", 500 @drive_bp.route('/oauth2callback') def oauth2callback(): state = session.get('oauth_state') if not state: return "Invalid state", 400 try: redirect_uri = url_for('drive.oauth2callback', _external=True) flow = create_flow(redirect_uri) flow.fetch_token(authorization_response=request.url) credentials = flow.credentials token_json = credentials.to_json() conn = get_db_connection() conn.execute('UPDATE users SET google_token = ? WHERE id = ?', (token_json, current_user.id)) conn.commit() conn.close() current_user.google_token = token_json return redirect(url_for('drive.drive_manager')) except Exception as e: return f"Auth failed: {e}", 500 @drive_bp.route('/drive/add', methods=['POST']) @login_required def add_source(): name = request.form.get('name') url = request.form.get('url') if not name or not url: return jsonify({'error': 'Name and URL required'}), 400 conn = get_db_connection() try: source_type = 'file' if '/folders/' in url or 'drive/folders' in url: source_type = 'folder' local_path = name.strip().replace(' ', '_') conn.execute('INSERT INTO drive_sources (name, url, local_path, user_id, source_type) VALUES (?, ?, ?, ?, ?)', (name, url, local_path, current_user.id, source_type)) conn.commit() return jsonify({'success': True}) except Exception as e: return jsonify({'error': str(e)}), 500 finally: conn.close() @drive_bp.route('/drive/delete/', methods=['POST']) @login_required def delete_source(id): conn = get_db_connection() source = conn.execute('SELECT * FROM drive_sources WHERE id = ?', (id,)).fetchone() if not source or source['user_id'] != current_user.id: conn.close() return jsonify({'error': 'Unauthorized'}), 403 conn.execute('DELETE FROM drive_sources WHERE id = ?', (id,)) conn.commit() conn.close() try: path = get_sync_folder_path(source['local_path']) if os.path.exists(path): shutil.rmtree(path) except Exception as e: print(f"Error deleting folder: {e}") return jsonify({'success': True}) def sync_task(source_id, user_id, app_config): import sqlite3 conn = sqlite3.connect('database.db') conn.row_factory = sqlite3.Row try: source = conn.execute('SELECT * FROM drive_sources WHERE id = ?', (source_id,)).fetchone() if not source: return output_base = os.path.join(app_config['OUTPUT_FOLDER'], DRIVE_SYNC_FOLDER, source['local_path']) if not os.path.exists(output_base): os.makedirs(output_base) print(f"Syncing Drive: {source['name']} to {output_base}") try: gdown.download_folder(url=source['url'], output=output_base, quiet=False, use_cookies=False) conn.execute('UPDATE drive_sources SET last_synced = CURRENT_TIMESTAMP WHERE id = ?', (source_id,)) conn.commit() print("Sync complete.") except Exception as e: print(f"GDown Error: {e}") except Exception as e: print(f"Sync Task Error: {e}") finally: conn.close() @drive_bp.route('/drive/sync/', methods=['POST']) @login_required def sync_source(id): conn = get_db_connection() source = conn.execute('SELECT * FROM drive_sources WHERE id = ?', (id,)).fetchone() conn.close() if not source or source['user_id'] != current_user.id: return jsonify({'error': 'Unauthorized'}), 403 thread = threading.Thread(target=sync_task, args=(id, current_user.id, current_app.config.copy())) thread.start() return jsonify({'success': True, 'message': 'Sync started in background'}) @drive_bp.route('/drive/browse/') @drive_bp.route('/drive/browse//') @login_required def browse_drive(source_id, subpath=''): conn = get_db_connection() source = conn.execute('SELECT * FROM drive_sources WHERE id = ?', (source_id,)).fetchone() conn.close() if not source or source['user_id'] != current_user.id: return "Unauthorized", 403 # === API Upgrade Logic === if current_user.google_token and not subpath: drive_id = extract_drive_id(source['url']) if drive_id: return redirect(url_for('drive.browse_drive_api', folder_id=drive_id, title=source['name'])) # ========================= base_path = get_sync_folder_path(source['local_path']) current_path = os.path.join(base_path, subpath) if not os.path.exists(current_path): if source['source_type'] == 'file': pass else: return "Path not found (Not synced yet). Click Sync Now in Manager.", 404 # Count total files to determine if lazy loading is needed file_count = 0 if os.path.exists(current_path): try: file_count = sum(1 for _ in os.scandir(current_path)) except: pass breadcrumbs = [] if subpath: parts = subpath.split('/') built = '' for part in parts: built = os.path.join(built, part).strip('/') breadcrumbs.append({'name': part, 'path': built}) # Use lazy loading for folders with more than 50 items if file_count > 50: return render_template('drive_browser.html', source=source, items=[], breadcrumbs=breadcrumbs, current_subpath=subpath, lazy_load=True, is_local=True) # Small folder - load all items inline items = [] if os.path.exists(current_path): try: for entry in os.scandir(current_path): is_dir = entry.is_dir() file_type = 'file' name_lower = entry.name.lower() if is_dir: file_type = 'folder' elif name_lower.endswith('.pdf'): file_type = 'pdf' elif name_lower.endswith(('.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp')): file_type = 'image' elif name_lower.endswith(('.ppt', '.pptx', '.odp')): file_type = 'slides' elif name_lower.endswith(('.xls', '.xlsx', '.ods', '.csv')): file_type = 'sheet' elif name_lower.endswith(('.doc', '.docx', '.odt', '.txt', '.rtf')): file_type = 'doc' elif name_lower.endswith(('.mp4', '.mov', '.avi', '.mkv', '.webm')): file_type = 'video' items.append({ 'name': entry.name, 'type': file_type, 'path': os.path.join(subpath, entry.name).strip('/') }) except Exception as e: return f"Error listing files: {e}", 500 items.sort(key=lambda x: (x['type'] != 'folder', x['name'].lower())) if not items and source['source_type'] == 'file': items.append({'name': 'Tap to Download & View', 'type': 'pdf', 'path': 'document.pdf'}) return render_template('drive_browser.html', source=source, items=items, breadcrumbs=breadcrumbs, current_subpath=subpath) @drive_bp.route('/drive/file//') @login_required def view_drive_file(source_id, filepath): conn = get_db_connection() source = conn.execute('SELECT * FROM drive_sources WHERE id = ?', (source_id,)).fetchone() conn.close() if not source or source['user_id'] != current_user.id: return "Unauthorized", 403 base_path = get_sync_folder_path(source['local_path']) full_path = os.path.join(base_path, filepath) if not os.path.exists(full_path) and source['source_type'] == 'file': try: if not os.path.exists(base_path): os.makedirs(base_path) gdown.download(url=source['url'], output=full_path, quiet=False, fuzzy=True) except Exception as e: return f"Error downloading file: {e}", 500 if not os.path.exists(full_path): return "File not found.", 404 name_lower = full_path.lower() if name_lower.endswith('.pdf'): file_url = url_for('drive.serve_drive_file', source_id=source_id, filepath=os.path.basename(full_path)) return render_template('pdfjs_viewer.html', pdf_url=file_url, pdf_title=os.path.basename(full_path)) if name_lower.endswith(('.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp')): file_url = url_for('drive.serve_drive_file', source_id=source_id, filepath=os.path.basename(full_path)) return render_template('image_viewer.html', image_url=file_url, image_title=os.path.basename(full_path)) return send_from_directory(os.path.dirname(full_path), os.path.basename(full_path)) @drive_bp.route('/drive/raw//') @login_required def serve_drive_file(source_id, filepath): conn = get_db_connection() source = conn.execute('SELECT * FROM drive_sources WHERE id = ?', (source_id,)).fetchone() conn.close() if not source or source['user_id'] != current_user.id: return "Unauthorized", 403 base_path = get_sync_folder_path(source['local_path']) return send_from_directory(base_path, filepath) @drive_bp.route('/drive/api/list') @drive_bp.route('/drive/api/list/') @login_required def api_list_files(folder_id='root'): service = get_drive_service(current_user) if not service: return jsonify({'error': 'Not connected'}), 401 files, next_token = list_drive_files(service, folder_id) file_list = [] for f in files: is_folder = f['mimeType'] == 'application/vnd.google-apps.folder' icon = 'folder-fill text-warning' if is_folder else 'file-earmark-text text-secondary' if f['mimeType'] == 'application/pdf': icon = 'file-earmark-pdf-fill text-danger' elif 'image' in f['mimeType']: icon = 'file-earmark-image-fill text-info' file_list.append({ 'id': f['id'], 'name': f['name'], 'type': 'folder' if is_folder else 'file', 'mimeType': f['mimeType'], 'icon': icon, 'size': f.get('size') }) return jsonify({'files': file_list, 'next_token': next_token}) @drive_bp.route('/drive/api/browse/') @login_required def browse_drive_api(folder_id): service = get_drive_service(current_user) if not service: return redirect(url_for('drive.drive_manager')) title = request.args.get('title', 'My Drive') # Build breadcrumbs from query param (JSON encoded list) breadcrumbs_param = request.args.get('breadcrumbs', '[]') try: breadcrumbs = json.loads(breadcrumbs_param) except: breadcrumbs = [] # For initial page load, just render the template (files loaded via AJAX) return render_template('drive_browser.html', source={'id': 'api', 'name': title}, items=[], # Empty - loaded via AJAX breadcrumbs=breadcrumbs, is_api=True, folder_id=folder_id, lazy_load=True) @drive_bp.route('/drive/api/files/') @login_required def api_get_files(folder_id): """Paginated JSON API for lazy loading files.""" service = get_drive_service(current_user) if not service: return jsonify({'error': 'Not connected'}), 401 page_token = request.args.get('page_token') files, next_token = list_drive_files(service, folder_id, page_token) items = [] for f in files: is_folder = f['mimeType'] == 'application/vnd.google-apps.folder' mime = f['mimeType'] f_type = 'folder' if is_folder else 'file' if mime == 'application/pdf': f_type = 'pdf' elif 'image' in mime: f_type = 'image' elif mime in ['application/vnd.google-apps.presentation', 'application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation']: f_type = 'slides' elif mime in ['application/vnd.google-apps.spreadsheet', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet']: f_type = 'sheet' elif mime in ['application/vnd.google-apps.document', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'text/plain']: f_type = 'doc' elif 'video' in mime: f_type = 'video' items.append({ 'name': f['name'], 'type': f_type, 'path': f['id'], 'size': f.get('size'), 'modified': f.get('modifiedTime') }) return jsonify({ 'items': items, 'next_token': next_token, 'has_more': next_token is not None }) @drive_bp.route('/drive/local/files/') @login_required def local_get_files(source_id): """Paginated JSON API for lazy loading local synced files.""" conn = get_db_connection() source = conn.execute('SELECT * FROM drive_sources WHERE id = ?', (source_id,)).fetchone() conn.close() if not source or source['user_id'] != current_user.id: return jsonify({'error': 'Unauthorized'}), 403 subpath = request.args.get('path', '') offset = int(request.args.get('offset', 0)) limit = int(request.args.get('limit', 50)) base_path = get_sync_folder_path(source['local_path']) current_path = os.path.join(base_path, subpath) if subpath else base_path if not os.path.exists(current_path): return jsonify({'items': [], 'has_more': False}) all_items = [] try: for entry in os.scandir(current_path): is_dir = entry.is_dir() file_type = 'file' name_lower = entry.name.lower() if is_dir: file_type = 'folder' elif name_lower.endswith('.pdf'): file_type = 'pdf' elif name_lower.endswith(('.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp')): file_type = 'image' elif name_lower.endswith(('.ppt', '.pptx', '.odp')): file_type = 'slides' elif name_lower.endswith(('.xls', '.xlsx', '.ods', '.csv')): file_type = 'sheet' elif name_lower.endswith(('.doc', '.docx', '.odt', '.txt', '.rtf')): file_type = 'doc' elif name_lower.endswith(('.mp4', '.mov', '.avi', '.mkv', '.webm')): file_type = 'video' stat = entry.stat() all_items.append({ 'name': entry.name, 'type': file_type, 'path': os.path.join(subpath, entry.name).strip('/'), 'size': stat.st_size if not is_dir else None, 'sort_key': (0 if is_dir else 1, entry.name.lower()) }) except Exception as e: return jsonify({'error': str(e)}), 500 # Sort: folders first, then alphabetically all_items.sort(key=lambda x: x['sort_key']) for item in all_items: del item['sort_key'] # Paginate paginated = all_items[offset:offset + limit] has_more = offset + limit < len(all_items) return jsonify({ 'items': paginated, 'has_more': has_more, 'total': len(all_items) }) @drive_bp.route('/drive/api/open/') @login_required def api_open_file(file_id): service = get_drive_service(current_user) if not service: return "Not connected", 401 try: meta = get_file_metadata(service, file_id) if not meta: return "File not found", 404 filename = meta['name'] cache_dir = os.path.join(current_app.config['UPLOAD_FOLDER'], 'drive_cache') if not os.path.exists(cache_dir): os.makedirs(cache_dir) from werkzeug.utils import secure_filename safe_name = secure_filename(filename) file_path = os.path.join(cache_dir, safe_name) if not os.path.exists(file_path): with open(file_path, 'wb') as f: download_file_to_stream(service, file_id, f) if safe_name.lower().endswith('.pdf'): # Log PDF access to history conn = get_db_connection() conn.execute(''' INSERT INTO pdf_access_history (user_id, file_id, filename, source_type, opened_at) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP) ''', (current_user.id, file_id, filename, 'drive_api')) conn.commit() conn.close() file_url = url_for('drive.serve_cache_file', filename=safe_name) return render_template('pdfjs_viewer.html', pdf_url=file_url, pdf_title=filename) if safe_name.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp')): return render_template('image_viewer.html', image_url=url_for('drive.serve_cache_file', filename=safe_name), image_title=filename) if safe_name.lower().endswith(('.ppt', '.pptx', '.odp')): # Redirect to Google Slides for presentation files return redirect(f'https://docs.google.com/presentation/d/{file_id}/edit') if safe_name.lower().endswith(('.xls', '.xlsx', '.ods', '.csv')): return redirect(f'https://docs.google.com/spreadsheets/d/{file_id}/edit') if safe_name.lower().endswith(('.doc', '.docx', '.odt')): return redirect(f'https://docs.google.com/document/d/{file_id}/edit') # For other files, serve directly return send_from_directory(cache_dir, safe_name) except Exception as e: return f"Error opening file: {e}", 500 @drive_bp.route('/drive/cache/') @login_required def serve_cache_file(filename): cache_dir = os.path.join(current_app.config['UPLOAD_FOLDER'], 'drive_cache') return send_from_directory(cache_dir, filename) @drive_bp.route('/drive/api/download/') @login_required def api_download_file(file_id): """Download a file from Google Drive (caches if not already cached).""" service = get_drive_service(current_user) if not service: return "Not connected", 401 try: meta = get_file_metadata(service, file_id) if not meta: return "File not found", 404 filename = meta['name'] cache_dir = os.path.join(current_app.config['UPLOAD_FOLDER'], 'drive_cache') if not os.path.exists(cache_dir): os.makedirs(cache_dir) from werkzeug.utils import secure_filename safe_name = secure_filename(filename) file_path = os.path.join(cache_dir, safe_name) if not os.path.exists(file_path): with open(file_path, 'wb') as f: download_file_to_stream(service, file_id, f) return send_from_directory(cache_dir, safe_name, as_attachment=False) except Exception as e: return f"Error downloading file: {e}", 500 # ==================== PUBLIC ACCESS ROUTES (No Auth Required) ==================== def generate_api_file_token(file_id, user_id): """Generate a permanent unique token for a Google Drive API file.""" import hashlib secret = current_app.config.get('SECRET_KEY', 'default-secret') data = f"api:{file_id}:{user_id}:{secret}" return hashlib.sha256(data.encode()).hexdigest()[:24] @drive_bp.route('/drive/api/public/') def public_api_download(file_id): """ Public endpoint: Download a Google Drive API file without authentication. Requires a valid token unique to the file. Usage: /drive/api/public/?token= """ token = request.args.get('token') if not token: return "Missing token parameter", 401 # Find which user owns this token by checking all users with google_token conn = get_db_connection() users = conn.execute('SELECT id, google_token FROM users WHERE google_token IS NOT NULL').fetchall() conn.close() valid_user = None for user in users: expected_token = generate_api_file_token(file_id, user['id']) if token == expected_token: valid_user = user break if not valid_user: return "Invalid or expired token", 403 # Build drive service for this user try: import json from google.oauth2.credentials import Credentials from googleapiclient.discovery import build token_info = json.loads(valid_user['google_token']) SCOPES = ['https://www.googleapis.com/auth/drive.readonly'] creds = Credentials.from_authorized_user_info(token_info, SCOPES) service = build('drive', 'v3', credentials=creds) meta = get_file_metadata(service, file_id) if not meta: return "File not found", 404 filename = meta['name'] cache_dir = os.path.join(current_app.config['UPLOAD_FOLDER'], 'drive_cache') if not os.path.exists(cache_dir): os.makedirs(cache_dir) from werkzeug.utils import secure_filename safe_name = secure_filename(filename) file_path = os.path.join(cache_dir, safe_name) if not os.path.exists(file_path): with open(file_path, 'wb') as f: download_file_to_stream(service, file_id, f) # Determine MIME type import mimetypes mime_type = mimetypes.guess_type(safe_name)[0] or 'application/octet-stream' # Check if download mode requested download_mode = request.args.get('download', '').lower() in ('1', 'true', 'yes') # Return file with proper headers for both inline viewing and download response = send_file( file_path, mimetype=mime_type, as_attachment=download_mode, download_name=filename ) # Add headers for better compatibility with PDF viewer apps if not download_mode: response.headers['Content-Disposition'] = f'inline; filename="{filename}"' # Allow cross-origin access for apps response.headers['Access-Control-Allow-Origin'] = '*' response.headers['Accept-Ranges'] = 'bytes' return response except Exception as e: return f"Error: {e}", 500 @drive_bp.route('/drive/api/token/') @login_required def get_api_file_token(file_id): """Get the public access token for a specific Google Drive API file.""" if not current_user.google_token: return jsonify({'error': 'Not connected to Google Drive'}), 401 token = generate_api_file_token(file_id, current_user.id) public_url = url_for('drive.public_api_download', file_id=file_id, token=token, _external=True) return jsonify({ 'token': token, 'public_url': public_url }) def generate_public_token(source_id, user_id): """Generate a simple token for public access.""" import hashlib secret = current_app.config.get('SECRET_KEY', 'default-secret') data = f"{source_id}:{user_id}:{secret}" return hashlib.sha256(data.encode()).hexdigest()[:32] def verify_public_token(source_id, token): """Verify public access token and return the source if valid.""" conn = get_db_connection() source = conn.execute('SELECT * FROM drive_sources WHERE id = ?', (source_id,)).fetchone() conn.close() if not source: return None expected_token = generate_public_token(source_id, source['user_id']) if token == expected_token: return dict(source) return None @drive_bp.route('/drive/public//token') @login_required def get_public_token(source_id): """Generate a public access token for a source (owner only).""" conn = get_db_connection() source = conn.execute('SELECT * FROM drive_sources WHERE id = ?', (source_id,)).fetchone() conn.close() if not source or source['user_id'] != current_user.id: return jsonify({'error': 'Unauthorized'}), 403 token = generate_public_token(source_id, current_user.id) return jsonify({'token': token}) @drive_bp.route('/drive/public//list.txt') def public_list_files(source_id): """ Public endpoint: Returns a plain text list of all files in the synced folder. Perfect for curl/wget scripts. Usage: curl https://host/drive/public/123/list.txt?token=abc123 """ token = request.args.get('token') if not token: return "Missing token parameter. Add ?token=YOUR_TOKEN to the URL.", 401 source = verify_public_token(source_id, token) if not source: return "Invalid or expired token.", 403 base_path = get_sync_folder_path(source['local_path']) if not os.path.exists(base_path): return "Source not synced yet.", 404 subpath = request.args.get('path', '') current_path = os.path.join(base_path, subpath) if subpath else base_path if not os.path.exists(current_path): return "Path not found.", 404 # Collect all files recursively files_list = [] base_url = request.host_url.rstrip('/') def scan_directory(dir_path, prefix=''): try: for entry in sorted(os.scandir(dir_path), key=lambda e: e.name.lower()): rel_path = os.path.join(prefix, entry.name) if prefix else entry.name if entry.is_dir(): files_list.append(f"[DIR] {rel_path}/") scan_directory(entry.path, rel_path) else: # Add download URL for each file download_url = f"{base_url}/drive/public/{source_id}/download/{rel_path}?token={token}" size = entry.stat().st_size size_str = format_file_size(size) files_list.append(f"{rel_path}\t{size_str}\t{download_url}") except Exception as e: files_list.append(f"[ERROR] {str(e)}") scan_directory(current_path) # Build response header = f"# Directory listing for: {source['name']}\n" header += f"# Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n" header += f"# Format: filename\\tsize\\tdownload_url\n" header += "#" + "=" * 60 + "\n\n" content = header + "\n".join(files_list) from flask import Response return Response(content, mimetype='text/plain', headers={ 'Content-Disposition': f'inline; filename="{source["name"]}_files.txt"' }) def format_file_size(size_bytes): """Format bytes to human readable string.""" for unit in ['B', 'KB', 'MB', 'GB']: if size_bytes < 1024: return f"{size_bytes:.1f}{unit}" size_bytes /= 1024 return f"{size_bytes:.1f}TB" @drive_bp.route('/drive/public//download') @drive_bp.route('/drive/public//download/') def public_download_file(source_id, filepath=None): """ Public endpoint: Download a file without authentication. Usage: - Single file source: /drive/public/123/download?token=abc123 - Specific file: /drive/public/123/download/subfolder/file.pdf?token=abc123 """ token = request.args.get('token') if not token: return "Missing token parameter. Add ?token=YOUR_TOKEN to the URL.", 401 source = verify_public_token(source_id, token) if not source: return "Invalid or expired token.", 403 base_path = get_sync_folder_path(source['local_path']) # If no filepath specified, try to find the first/only file if not filepath: if source['source_type'] == 'file': # For file sources, download using gdown if not cached if not os.path.exists(base_path): os.makedirs(base_path) # Find existing file or download files = [f for f in os.listdir(base_path) if os.path.isfile(os.path.join(base_path, f))] if os.path.exists(base_path) else [] if files: filepath = files[0] else: # Try to download try: output_path = os.path.join(base_path, 'document.pdf') gdown.download(url=source['url'], output=output_path, quiet=True, fuzzy=True) filepath = 'document.pdf' except Exception as e: return f"Error downloading file: {e}", 500 else: return "Filepath required for folder sources. Use /download/path/to/file.pdf", 400 full_path = os.path.join(base_path, filepath) # Security: Ensure path doesn't escape base if not os.path.abspath(full_path).startswith(os.path.abspath(base_path)): return "Invalid path.", 403 if not os.path.exists(full_path): return "File not found.", 404 if not os.path.isfile(full_path): return "Not a file.", 400 # Serve the file directory = os.path.dirname(full_path) filename = os.path.basename(full_path) # Check if inline viewing is requested inline = request.args.get('inline', '0') == '1' return send_from_directory( directory, filename, as_attachment=not inline ) @drive_bp.route('/drive/public//browse') def public_browse(source_id): """ Public endpoint: Browse files in a synced folder (JSON response). """ token = request.args.get('token') if not token: return jsonify({'error': 'Missing token'}), 401 source = verify_public_token(source_id, token) if not source: return jsonify({'error': 'Invalid token'}), 403 base_path = get_sync_folder_path(source['local_path']) subpath = request.args.get('path', '') current_path = os.path.join(base_path, subpath) if subpath else base_path if not os.path.exists(current_path): return jsonify({'error': 'Path not found'}), 404 items = [] base_url = request.host_url.rstrip('/') try: for entry in sorted(os.scandir(current_path), key=lambda e: (not e.is_dir(), e.name.lower())): rel_path = os.path.join(subpath, entry.name) if subpath else entry.name item = { 'name': entry.name, 'type': 'folder' if entry.is_dir() else 'file', 'path': rel_path } if entry.is_file(): item['size'] = entry.stat().st_size item['download_url'] = f"{base_url}/drive/public/{source_id}/download/{rel_path}?token={token}" items.append(item) except Exception as e: return jsonify({'error': str(e)}), 500 return jsonify({ 'source': source['name'], 'path': subpath or '/', 'items': items })