Spaces:
Running
Running
t
Claude Opus 4.6
commited on
Commit
·
abda79a
1
Parent(s):
579fd4f
feat: add public token links for Drive API files and breadcrumb navigation
Browse files- Add permanent public tokens unique to each file (no auth required)
- Add /drive/api/public/<file_id>?token=<token> endpoint for public download
- Add /drive/api/token/<file_id> to get public URL for a file
- Copy link now fetches public URL with token for API files
- Add breadcrumb navigation support for Google Drive API folders
- Track folder path via query params for back navigation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- drive_routes.py +92 -1
- templates/drive_browser.html +42 -8
drive_routes.py
CHANGED
|
@@ -332,11 +332,18 @@ def browse_drive_api(folder_id):
|
|
| 332 |
if not service: return redirect(url_for('drive.drive_manager'))
|
| 333 |
title = request.args.get('title', 'My Drive')
|
| 334 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 335 |
# For initial page load, just render the template (files loaded via AJAX)
|
| 336 |
return render_template('drive_browser.html',
|
| 337 |
source={'id': 'api', 'name': title},
|
| 338 |
items=[], # Empty - loaded via AJAX
|
| 339 |
-
breadcrumbs=
|
| 340 |
is_api=True,
|
| 341 |
folder_id=folder_id,
|
| 342 |
lazy_load=True)
|
|
@@ -520,6 +527,90 @@ def api_download_file(file_id):
|
|
| 520 |
|
| 521 |
# ==================== PUBLIC ACCESS ROUTES (No Auth Required) ====================
|
| 522 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 523 |
def generate_public_token(source_id, user_id):
|
| 524 |
"""Generate a simple token for public access."""
|
| 525 |
import hashlib
|
|
|
|
| 332 |
if not service: return redirect(url_for('drive.drive_manager'))
|
| 333 |
title = request.args.get('title', 'My Drive')
|
| 334 |
|
| 335 |
+
# Build breadcrumbs from query param (JSON encoded list)
|
| 336 |
+
breadcrumbs_param = request.args.get('breadcrumbs', '[]')
|
| 337 |
+
try:
|
| 338 |
+
breadcrumbs = json.loads(breadcrumbs_param)
|
| 339 |
+
except:
|
| 340 |
+
breadcrumbs = []
|
| 341 |
+
|
| 342 |
# For initial page load, just render the template (files loaded via AJAX)
|
| 343 |
return render_template('drive_browser.html',
|
| 344 |
source={'id': 'api', 'name': title},
|
| 345 |
items=[], # Empty - loaded via AJAX
|
| 346 |
+
breadcrumbs=breadcrumbs,
|
| 347 |
is_api=True,
|
| 348 |
folder_id=folder_id,
|
| 349 |
lazy_load=True)
|
|
|
|
| 527 |
|
| 528 |
# ==================== PUBLIC ACCESS ROUTES (No Auth Required) ====================
|
| 529 |
|
| 530 |
+
def generate_api_file_token(file_id, user_id):
|
| 531 |
+
"""Generate a permanent unique token for a Google Drive API file."""
|
| 532 |
+
import hashlib
|
| 533 |
+
secret = current_app.config.get('SECRET_KEY', 'default-secret')
|
| 534 |
+
data = f"api:{file_id}:{user_id}:{secret}"
|
| 535 |
+
return hashlib.sha256(data.encode()).hexdigest()[:24]
|
| 536 |
+
|
| 537 |
+
|
| 538 |
+
@drive_bp.route('/drive/api/public/<file_id>')
|
| 539 |
+
def public_api_download(file_id):
|
| 540 |
+
"""
|
| 541 |
+
Public endpoint: Download a Google Drive API file without authentication.
|
| 542 |
+
Requires a valid token unique to the file.
|
| 543 |
+
|
| 544 |
+
Usage: /drive/api/public/<file_id>?token=<token>
|
| 545 |
+
"""
|
| 546 |
+
token = request.args.get('token')
|
| 547 |
+
if not token:
|
| 548 |
+
return "Missing token parameter", 401
|
| 549 |
+
|
| 550 |
+
# Find which user owns this token by checking all users with google_token
|
| 551 |
+
conn = get_db_connection()
|
| 552 |
+
users = conn.execute('SELECT id, google_token FROM users WHERE google_token IS NOT NULL').fetchall()
|
| 553 |
+
conn.close()
|
| 554 |
+
|
| 555 |
+
valid_user = None
|
| 556 |
+
for user in users:
|
| 557 |
+
expected_token = generate_api_file_token(file_id, user['id'])
|
| 558 |
+
if token == expected_token:
|
| 559 |
+
valid_user = user
|
| 560 |
+
break
|
| 561 |
+
|
| 562 |
+
if not valid_user:
|
| 563 |
+
return "Invalid or expired token", 403
|
| 564 |
+
|
| 565 |
+
# Build drive service for this user
|
| 566 |
+
try:
|
| 567 |
+
import json
|
| 568 |
+
from google.oauth2.credentials import Credentials
|
| 569 |
+
from googleapiclient.discovery import build
|
| 570 |
+
|
| 571 |
+
token_info = json.loads(valid_user['google_token'])
|
| 572 |
+
SCOPES = ['https://www.googleapis.com/auth/drive.readonly']
|
| 573 |
+
creds = Credentials.from_authorized_user_info(token_info, SCOPES)
|
| 574 |
+
service = build('drive', 'v3', credentials=creds)
|
| 575 |
+
|
| 576 |
+
meta = get_file_metadata(service, file_id)
|
| 577 |
+
if not meta:
|
| 578 |
+
return "File not found", 404
|
| 579 |
+
|
| 580 |
+
filename = meta['name']
|
| 581 |
+
cache_dir = os.path.join(current_app.config['UPLOAD_FOLDER'], 'drive_cache')
|
| 582 |
+
if not os.path.exists(cache_dir):
|
| 583 |
+
os.makedirs(cache_dir)
|
| 584 |
+
|
| 585 |
+
from werkzeug.utils import secure_filename
|
| 586 |
+
safe_name = secure_filename(filename)
|
| 587 |
+
file_path = os.path.join(cache_dir, safe_name)
|
| 588 |
+
|
| 589 |
+
if not os.path.exists(file_path):
|
| 590 |
+
with open(file_path, 'wb') as f:
|
| 591 |
+
download_file_to_stream(service, file_id, f)
|
| 592 |
+
|
| 593 |
+
return send_from_directory(cache_dir, safe_name, as_attachment=False)
|
| 594 |
+
except Exception as e:
|
| 595 |
+
return f"Error: {e}", 500
|
| 596 |
+
|
| 597 |
+
|
| 598 |
+
@drive_bp.route('/drive/api/token/<file_id>')
|
| 599 |
+
@login_required
|
| 600 |
+
def get_api_file_token(file_id):
|
| 601 |
+
"""Get the public access token for a specific Google Drive API file."""
|
| 602 |
+
if not current_user.google_token:
|
| 603 |
+
return jsonify({'error': 'Not connected to Google Drive'}), 401
|
| 604 |
+
|
| 605 |
+
token = generate_api_file_token(file_id, current_user.id)
|
| 606 |
+
public_url = url_for('drive.public_api_download', file_id=file_id, token=token, _external=True)
|
| 607 |
+
|
| 608 |
+
return jsonify({
|
| 609 |
+
'token': token,
|
| 610 |
+
'public_url': public_url
|
| 611 |
+
})
|
| 612 |
+
|
| 613 |
+
|
| 614 |
def generate_public_token(source_id, user_id):
|
| 615 |
"""Generate a simple token for public access."""
|
| 616 |
import hashlib
|
templates/drive_browser.html
CHANGED
|
@@ -356,14 +356,20 @@
|
|
| 356 |
<nav aria-label="breadcrumb">
|
| 357 |
<ol class="breadcrumb">
|
| 358 |
<li class="breadcrumb-item"><a href="/drive_manager"><i class="bi bi-cloud me-1"></i>Drives</a></li>
|
| 359 |
-
{% if
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 360 |
<li class="breadcrumb-item active">{{ source.name }}</li>
|
|
|
|
| 361 |
{% else %}
|
| 362 |
<li class="breadcrumb-item"><a href="/drive/browse/{{ source.id }}">{{ source.name }}</a></li>
|
| 363 |
-
{% endif %}
|
| 364 |
{% for crumb in breadcrumbs %}
|
| 365 |
<li class="breadcrumb-item active">{{ crumb.name }}</li>
|
| 366 |
{% endfor %}
|
|
|
|
| 367 |
</ol>
|
| 368 |
</nav>
|
| 369 |
|
|
@@ -596,7 +602,9 @@
|
|
| 596 |
lazyLoad: {{ 'true' if lazy_load is defined and lazy_load else 'false' }},
|
| 597 |
sourceId: '{{ source.id }}',
|
| 598 |
folderId: '{{ folder_id if folder_id is defined else "" }}',
|
| 599 |
-
|
|
|
|
|
|
|
| 600 |
};
|
| 601 |
|
| 602 |
// Lazy loading state
|
|
@@ -608,6 +616,17 @@
|
|
| 608 |
itemCount: 0
|
| 609 |
};
|
| 610 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 611 |
// Navigation with loader
|
| 612 |
function navigate(url) {
|
| 613 |
document.getElementById('loader').classList.add('active');
|
|
@@ -641,10 +660,25 @@
|
|
| 641 |
localStorage.setItem('driveViewMode', view);
|
| 642 |
}
|
| 643 |
|
| 644 |
-
// Copy file link
|
| 645 |
-
function copyFileLink(path, btn) {
|
| 646 |
-
|
| 647 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 648 |
const originalHtml = btn.innerHTML;
|
| 649 |
btn.innerHTML = '<i class="bi bi-check"></i>';
|
| 650 |
btn.style.background = '#198754';
|
|
@@ -679,7 +713,7 @@
|
|
| 679 |
|
| 680 |
if (CONFIG.isApi) {
|
| 681 |
link = item.type === 'folder'
|
| 682 |
-
?
|
| 683 |
: `/drive/api/open/${item.path}`;
|
| 684 |
|
| 685 |
// For API files, use download endpoint for copy link
|
|
|
|
| 356 |
<nav aria-label="breadcrumb">
|
| 357 |
<ol class="breadcrumb">
|
| 358 |
<li class="breadcrumb-item"><a href="/drive_manager"><i class="bi bi-cloud me-1"></i>Drives</a></li>
|
| 359 |
+
{% if is_api %}
|
| 360 |
+
<li class="breadcrumb-item"><a href="/drive/api/browse/root?title=My Drive">My Drive</a></li>
|
| 361 |
+
{% for crumb in breadcrumbs %}
|
| 362 |
+
<li class="breadcrumb-item"><a href="/drive/api/browse/{{ crumb.id }}?title={{ crumb.name }}&breadcrumbs={{ crumb.trail | tojson | urlencode }}">{{ crumb.name }}</a></li>
|
| 363 |
+
{% endfor %}
|
| 364 |
+
{% if folder_id != 'root' and not breadcrumbs %}
|
| 365 |
<li class="breadcrumb-item active">{{ source.name }}</li>
|
| 366 |
+
{% endif %}
|
| 367 |
{% else %}
|
| 368 |
<li class="breadcrumb-item"><a href="/drive/browse/{{ source.id }}">{{ source.name }}</a></li>
|
|
|
|
| 369 |
{% for crumb in breadcrumbs %}
|
| 370 |
<li class="breadcrumb-item active">{{ crumb.name }}</li>
|
| 371 |
{% endfor %}
|
| 372 |
+
{% endif %}
|
| 373 |
</ol>
|
| 374 |
</nav>
|
| 375 |
|
|
|
|
| 602 |
lazyLoad: {{ 'true' if lazy_load is defined and lazy_load else 'false' }},
|
| 603 |
sourceId: '{{ source.id }}',
|
| 604 |
folderId: '{{ folder_id if folder_id is defined else "" }}',
|
| 605 |
+
sourceName: '{{ source.name }}',
|
| 606 |
+
subpath: '{{ current_subpath if current_subpath is defined else "" }}',
|
| 607 |
+
breadcrumbs: {{ breadcrumbs | tojson }}
|
| 608 |
};
|
| 609 |
|
| 610 |
// Lazy loading state
|
|
|
|
| 616 |
itemCount: 0
|
| 617 |
};
|
| 618 |
|
| 619 |
+
// Build folder URL with breadcrumb tracking for API
|
| 620 |
+
function buildFolderUrl(folderId, folderName) {
|
| 621 |
+
if (CONFIG.isApi) {
|
| 622 |
+
// Build new breadcrumb trail
|
| 623 |
+
const newTrail = [...CONFIG.breadcrumbs, { id: CONFIG.folderId, name: CONFIG.sourceName }];
|
| 624 |
+
const breadcrumbsParam = encodeURIComponent(JSON.stringify(newTrail));
|
| 625 |
+
return `/drive/api/browse/${folderId}?title=${encodeURIComponent(folderName)}&breadcrumbs=${breadcrumbsParam}`;
|
| 626 |
+
}
|
| 627 |
+
return `/drive/browse/${CONFIG.sourceId}/${folderId}`;
|
| 628 |
+
}
|
| 629 |
+
|
| 630 |
// Navigation with loader
|
| 631 |
function navigate(url) {
|
| 632 |
document.getElementById('loader').classList.add('active');
|
|
|
|
| 660 |
localStorage.setItem('driveViewMode', view);
|
| 661 |
}
|
| 662 |
|
| 663 |
+
// Copy file link - for API files, get public token first
|
| 664 |
+
async function copyFileLink(path, btn) {
|
| 665 |
+
let urlToCopy = window.location.origin + path;
|
| 666 |
+
|
| 667 |
+
// For API files, get the public token URL
|
| 668 |
+
if (CONFIG.isApi && path.includes('/drive/api/download/')) {
|
| 669 |
+
const fileId = path.split('/drive/api/download/')[1];
|
| 670 |
+
try {
|
| 671 |
+
const response = await fetch(`/drive/api/token/${fileId}`);
|
| 672 |
+
if (response.ok) {
|
| 673 |
+
const data = await response.json();
|
| 674 |
+
urlToCopy = data.public_url;
|
| 675 |
+
}
|
| 676 |
+
} catch (e) {
|
| 677 |
+
console.error('Failed to get public token:', e);
|
| 678 |
+
}
|
| 679 |
+
}
|
| 680 |
+
|
| 681 |
+
navigator.clipboard.writeText(urlToCopy).then(() => {
|
| 682 |
const originalHtml = btn.innerHTML;
|
| 683 |
btn.innerHTML = '<i class="bi bi-check"></i>';
|
| 684 |
btn.style.background = '#198754';
|
|
|
|
| 713 |
|
| 714 |
if (CONFIG.isApi) {
|
| 715 |
link = item.type === 'folder'
|
| 716 |
+
? buildFolderUrl(item.path, item.name)
|
| 717 |
: `/drive/api/open/${item.path}`;
|
| 718 |
|
| 719 |
// For API files, use download endpoint for copy link
|