Report-Generator / drive_routes.py
t
fix: improve public download headers for PDF viewer app compatibility
f07037a
Raw
History Blame Contribute Delete
34 kB
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}<br><br>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/<int:id>', 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/<int:id>', 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/<int:source_id>')
@drive_bp.route('/drive/browse/<int:source_id>/<path:subpath>')
@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/<int:source_id>/<path:filepath>')
@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/<int:source_id>/<path:filepath>')
@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/<folder_id>')
@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/<folder_id>')
@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/<folder_id>')
@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/<int:source_id>')
@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/<file_id>')
@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/<filename>')
@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/<file_id>')
@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/<file_id>')
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/<file_id>?token=<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/<file_id>')
@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/<int:source_id>/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/<int:source_id>/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/<int:source_id>/download')
@drive_bp.route('/drive/public/<int:source_id>/download/<path:filepath>')
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/<int:source_id>/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
})