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>

Files changed (2) hide show
  1. drive_routes.py +92 -1
  2. 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 source.id == 'api' %}
 
 
 
 
 
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
- subpath: '{{ current_subpath if current_subpath is defined else "" }}'
 
 
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
- const fullUrl = window.location.origin + path;
647
- navigator.clipboard.writeText(fullUrl).then(() => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- ? `/drive/api/browse/${item.path}`
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