Parcourir la source

feat(download): add file preview page and separate download endpoint

- Implement new download page with file preview capability for text files
- Split download functionality into separate endpoints for page view and direct download
- Update UI to include both view and download options with distinct styling
- Add text file preview section with proper error handling for unreadable files
Geovanny Andrés Vega Mite il y a 4 mois
Parent
commit
6c7967819f
4 fichiers modifiés avec 251 ajouts et 3 suppressions
  1. 49 1
      app.py
  2. 5 2
      static/js/main.js
  3. 189 0
      templates/download.html
  4. 8 0
      test.txt

+ 49 - 1
app.py

@@ -213,7 +213,55 @@ def get_user_files(user_session):
     return jsonify(file_list)
 
 @app.route('/download/<file_id>')
-def download_file(file_id):
+def download_page(file_id):
+    conn = sqlite3.connect('files.db')
+    cursor = conn.cursor()
+    cursor.execute('''
+        SELECT original_filename, file_path, expiration_date, file_size, mime_type
+        FROM files 
+        WHERE id = ?
+    ''', (file_id,))
+    
+    file_data = cursor.fetchone()
+    conn.close()
+    
+    if not file_data:
+        return make_response(render_template('file_not_found.html'), 404)
+    
+    original_filename, file_path, expiration_date, file_size, mime_type = file_data
+    
+    # Check if file has expired
+    if datetime.fromisoformat(expiration_date) < datetime.now():
+        return make_response(render_template('file_not_found.html'), 410)
+    
+    # Check if file exists
+    full_path = os.path.join(app.config['UPLOAD_FOLDER'], file_path)
+    if not os.path.exists(full_path):
+        return make_response(render_template('file_not_found.html'), 404)
+    
+    # Check if it's a text file for preview
+    is_text_file = mime_type and (mime_type.startswith('text/') or mime_type == 'application/json')
+    file_content = None
+    
+    if is_text_file:
+        try:
+            with open(full_path, 'r', encoding='utf-8') as f:
+                file_content = f.read()
+        except (UnicodeDecodeError, Exception):
+            # If can't read as text, treat as binary
+            is_text_file = False
+    
+    return render_template('download.html', 
+                         file_id=file_id,
+                         filename=original_filename,
+                         file_size=file_size,
+                         mime_type=mime_type,
+                         expiration_date=expiration_date,
+                         is_text_file=is_text_file,
+                         file_content=file_content)
+
+@app.route('/download/<file_id>/direct')
+def download_file_direct(file_id):
     conn = sqlite3.connect('files.db')
     cursor = conn.cursor()
     cursor.execute('''

+ 5 - 2
static/js/main.js

@@ -199,7 +199,10 @@ async function loadUserFiles() {
                             <div class="text-right">
                                 <p class="text-xs text-gray-500 dark:text-gray-400 file-remaining-time" data-expiration="${file.expiration_date}">${getTimeRemaining(file.expiration_date)}</p>
                             </div>
-                            <a href="${file.download_url}" class="inline-flex items-center px-3 py-1 text-xs font-medium text-blue-600 dark:text-blue-400 bg-blue-100 dark:bg-blue-900/30 hover:bg-blue-200 dark:hover:bg-blue-900/50 rounded-full transition-colors" target="_blank">
+                            <a href="${file.download_url}" class="inline-flex items-center px-3 py-1 text-xs font-medium text-green-600 dark:text-green-400 bg-green-100 dark:bg-green-900/30 hover:bg-green-200 dark:hover:bg-green-900/50 rounded-full transition-colors" target="_blank">
+                                View
+                            </a>
+                            <a href="${file.download_url}/direct" class="inline-flex items-center px-3 py-1 text-xs font-medium text-blue-600 dark:text-blue-400 bg-blue-100 dark:bg-blue-900/30 hover:bg-blue-200 dark:hover:bg-blue-900/50 rounded-full transition-colors" target="_blank">
                                 Download
                             </a>
                             <button onclick="copyDownloadUrl('${file.download_url}')" class="inline-flex items-center px-3 py-1 text-xs font-medium text-gray-700 dark:text-gray-200 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-full transition-colors">
@@ -391,4 +394,4 @@ window.copyDownloadUrl = function(url) {
     }, () => {
         showToast('Failed to copy link', 'error');
     });
-} 
+}

+ 189 - 0
templates/download.html

@@ -0,0 +1,189 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Download - {{ filename }} - HokoriTemp</title>
+    
+    <!-- SVG Favicon -->
+    <link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
+    
+    <link rel="stylesheet" href="/static/css/styles.css">
+    <script src="https://cdn.tailwindcss.com"></script>
+    <script>
+        (function() {
+            const savedTheme = localStorage.getItem('theme') || 'light';
+            if (savedTheme === 'dark') {
+                document.documentElement.classList.add('dark');
+            }
+        })();
+        tailwind.config = {
+            darkMode: 'class',
+            theme: {
+                extend: {
+                    animation: {
+                        'fade-in': 'fadeIn 0.3s ease-in-out',
+                        'slide-up': 'slideUp 0.3s ease-out',
+                        'pulse-slow': 'pulse 2s infinite'
+                    }
+                }
+            }
+        }
+    </script>
+</head>
+<body class="bg-gray-50 dark:bg-gray-900 min-h-screen transition-colors duration-300">
+    <div class="container mx-auto px-4 py-8 max-w-4xl">
+        <!-- Header -->
+        <div class="flex justify-between items-center mb-8">
+            <div class="text-center flex-1">
+                <h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-2">HokoriTemp</h1>
+                <p class="text-gray-600 dark:text-gray-400">File Download</p>
+            </div>
+            
+            <!-- Theme Toggle Button -->
+            <button id="themeToggle" class="p-2 rounded-lg bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors ml-4">
+                <!-- Sun Icon (Light Mode) -->
+                <svg id="sunIcon" class="w-5 h-5 text-gray-800 dark:text-gray-200 hidden dark:block" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"></path>
+                </svg>
+                <!-- Moon Icon (Dark Mode) -->
+                <svg id="moonIcon" class="w-5 h-5 text-gray-800 dark:text-gray-200 block dark:hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"></path>
+                </svg>
+            </button>
+        </div>
+
+        <!-- File Info Section -->
+        <div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-6 transition-colors duration-300">
+            <div class="flex items-center space-x-4 mb-4">
+                <div class="flex-shrink-0">
+                    <svg class="w-12 h-12 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
+                    </svg>
+                </div>
+                <div class="flex-1">
+                    <h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-1">{{ filename }}</h2>
+                    <div class="flex flex-wrap gap-4 text-sm text-gray-600 dark:text-gray-400">
+                        <span>Size: {{ "%.2f"|format(file_size / 1024 / 1024) }} MB</span>
+                        {% if mime_type %}
+                        <span>Type: {{ mime_type }}</span>
+                        {% endif %}
+                        <span>Expires: {{ expiration_date[:19].replace('T', ' ') }}</span>
+                    </div>
+                </div>
+            </div>
+            
+            <!-- Download Button -->
+            <div class="flex gap-3">
+                <a href="/download/{{ file_id }}/direct" 
+                   class="inline-flex items-center px-6 py-3 bg-blue-600 hover:bg-blue-700 dark:bg-blue-700 dark:hover:bg-blue-600 text-white font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
+                    <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
+                    </svg>
+                    Download File
+                </a>
+                
+                <a href="/" 
+                   class="inline-flex items-center px-6 py-3 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-800 dark:text-gray-200 font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
+                    <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z"></path>
+                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5a2 2 0 012-2h2a2 2 0 012 2v0H8v0z"></path>
+                    </svg>
+                    Upload More Files
+                </a>
+            </div>
+        </div>
+
+        <!-- Text File Preview -->
+        {% if is_text_file and file_content %}
+        <div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 transition-colors duration-300">
+            <div class="flex items-center justify-between mb-4">
+                <h3 class="text-lg font-semibold text-gray-900 dark:text-white">File Preview</h3>
+                <span class="text-sm text-gray-500 dark:text-gray-400">Text file preview</span>
+            </div>
+            
+            <div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4 overflow-auto max-h-96">
+                <pre class="text-sm text-gray-800 dark:text-gray-200 whitespace-pre-wrap font-mono">{{ file_content }}</pre>
+            </div>
+            
+            <div class="mt-4 text-xs text-gray-500 dark:text-gray-400">
+                <p>Preview shows the first part of the file content. Download the file to view the complete content.</p>
+            </div>
+        </div>
+        {% elif is_text_file %}
+        <div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 transition-colors duration-300">
+            <div class="text-center py-8">
+                <svg class="w-12 h-12 mx-auto mb-4 text-yellow-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"></path>
+                </svg>
+                <h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">Preview Not Available</h3>
+                <p class="text-gray-600 dark:text-gray-400">This text file couldn't be previewed. Please download it to view the content.</p>
+            </div>
+        </div>
+        {% else %}
+        <div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 transition-colors duration-300">
+            <div class="text-center py-8">
+                <svg class="w-12 h-12 mx-auto mb-4 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
+                </svg>
+                <h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">Binary File</h3>
+                <p class="text-gray-600 dark:text-gray-400">This file cannot be previewed. Click the download button above to get the file.</p>
+            </div>
+        </div>
+        {% endif %}
+    </div>
+
+    <!-- Toast Notification -->
+    <div id="toast" class="fixed top-4 right-4 bg-green-500 text-white px-6 py-3 rounded-lg shadow-lg transform translate-x-full transition-transform duration-300 z-50">
+        <p id="toastMessage"></p>
+    </div>
+
+    <script>
+        // Theme management
+        function initTheme() {
+            const savedTheme = localStorage.getItem('theme') || 'light';
+            if (savedTheme === 'dark') {
+                document.documentElement.classList.add('dark');
+            } else {
+                document.documentElement.classList.remove('dark');
+            }
+        }
+
+        function toggleTheme() {
+            const isDark = document.documentElement.classList.contains('dark');
+            if (isDark) {
+                document.documentElement.classList.remove('dark');
+                localStorage.setItem('theme', 'light');
+            } else {
+                document.documentElement.classList.add('dark');
+                localStorage.setItem('theme', 'dark');
+            }
+        }
+
+        // Toast notification
+        function showToast(message, type = 'success') {
+            const toast = document.getElementById('toast');
+            const toastMessage = document.getElementById('toastMessage');
+            toastMessage.textContent = message;
+            toast.classList.remove('bg-green-500', 'bg-red-500', 'bg-blue-500');
+            if (type === 'success') {
+                toast.classList.add('bg-green-500');
+            } else if (type === 'error') {
+                toast.classList.add('bg-red-500');
+            } else if (type === 'info') {
+                toast.classList.add('bg-blue-500');
+            }
+            toast.classList.remove('translate-x-full');
+            setTimeout(() => {
+                toast.classList.add('translate-x-full');
+            }, 3000);
+        }
+
+        // Initialize
+        document.addEventListener('DOMContentLoaded', function() {
+            initTheme();
+            document.getElementById('themeToggle').addEventListener('click', toggleTheme);
+        });
+    </script>
+</body>
+</html>

+ 8 - 0
test.txt

@@ -0,0 +1,8 @@
+Este es un archivo de prueba para verificar el preview de archivos de texto.
+
+Contenido de ejemplo:
+- Línea 1
+- Línea 2
+- Línea 3
+
+¡El preview debería mostrar este contenido!