Browse Source

feat(file-preview): add archive file preview support for ZIP, TAR and RAR files

- Implement archive content extraction and preview for ZIP, TAR and RAR files
- Add new template section for displaying archive contents with file details
- Include optional RAR support with proper documentation
- Show compression ratios and file statistics in the preview
Geovanny Andrés Vega Mite 3 months ago
parent
commit
154cd09ead
4 changed files with 221 additions and 10 deletions
  1. 47 1
      README.md
  2. 112 1
      app.py
  3. 62 0
      templates/download.html
  4. 0 8
      test.txt

+ 47 - 1
README.md

@@ -57,6 +57,32 @@ python app.py --network --port 8080 --max-file-size 50 --base-url https://upload
 - ✅ **Responsive design** with Tailwind CSS
 - ✅ **Download links** with custom domains
 - ✅ **File management** (upload/download/delete)
+- ✅ **File preview** for text files and JSON
+- ✅ **Archive preview** for ZIP, TAR, and RAR files
+- ✅ **Download page** with file information and preview
+- ✅ **Dual download options** (direct download vs. info page)
+
+## 👁️ File Preview Features
+
+### Text File Preview
+- **Supported formats**: .txt, .json, .md, .csv, .log, and other text-based files
+- **Real-time preview** of file content before downloading
+- **Syntax highlighting** for JSON files
+- **Error handling** for unreadable text files
+
+### Archive File Preview
+- **ZIP files**: Complete file listing with compression ratios
+- **TAR files**: Support for .tar, .tar.gz, .tar.bz2, .tar.xz formats
+- **RAR files**: Optional support (requires rarfile library)
+- **File information**: Names, sizes, dates, and folder structure
+- **Statistics**: Total files and folders count
+- **Visual indicators**: Different icons for files and directories
+
+### Download Options
+- **View button**: Opens information page with preview
+- **Download button**: Direct file download from file list
+- **Copy link**: Share the information page URL
+- **Modern UI**: Responsive design with dark/light theme support
 
 ## 🗂️ Project Structure
 
@@ -66,7 +92,13 @@ project/
 ├── app.py               # Main Flask application
 ├── requirements.txt     # Dependencies
 ├── templates/
-│   └── index.html      # Web interface
+│   ├── index.html      # Main web interface
+│   └── download.html   # File information and preview page
+├── static/
+│   ├── css/
+│   │   └── styles.css  # Custom styles
+│   └── js/
+│       └── main.js     # Frontend JavaScript
 ├── uploads/            # Files storage (auto-created)
 └── files.db           # SQLite database (auto-created)
 ```
@@ -93,6 +125,20 @@ export BASE_URL=https://files.mysite.com
 python app.py --network
 ```
 
+## 📦 Optional Dependencies
+
+For **RAR file preview support**, install the rarfile library:
+```bash
+pip install rarfile
+```
+
+**Note**: RAR support also requires the UnRAR library to be installed on your system:
+- **Windows**: Download UnRAR from WinRAR website
+- **Linux**: `sudo apt-get install unrar` or `sudo yum install unrar`
+- **macOS**: `brew install unrar`
+
+Without rarfile, the application will work normally but won't preview RAR archives.
+
 ## 📝 File Expiration
 
 Users can set file duration when uploading:

+ 112 - 1
app.py

@@ -12,12 +12,114 @@ from werkzeug.utils import secure_filename
 from flask import Flask, request, jsonify, render_template, send_from_directory, abort, make_response
 from werkzeug.security import generate_password_hash
 import re
+import zipfile
+import tarfile
+try:
+    import rarfile
+    RARFILE_AVAILABLE = True
+except ImportError:
+    RARFILE_AVAILABLE = False
 
 def generate_secret_key(length=32):
     """Generate a random secret key with specified length"""
     alphabet = string.ascii_letters + string.digits + string.punctuation
     return ''.join(secrets.choice(alphabet) for _ in range(length))
 
+def get_archive_contents(file_path, mime_type):
+    """Get contents of archive files (ZIP, TAR, RAR)"""
+    try:
+        # Check if it's a ZIP file
+        if zipfile.is_zipfile(file_path):
+            with zipfile.ZipFile(file_path, 'r') as zip_ref:
+                file_list = []
+                for info in zip_ref.infolist():
+                    if not info.is_dir():
+                        file_list.append({
+                            'name': info.filename,
+                            'size': info.file_size,
+                            'compressed_size': info.compress_size,
+                            'date_time': info.date_time,
+                            'type': 'file'
+                        })
+                    else:
+                        file_list.append({
+                            'name': info.filename,
+                            'size': 0,
+                            'type': 'directory'
+                        })
+                return {'type': 'zip', 'files': file_list}
+        
+        # Check if it's a TAR file
+        elif tarfile.is_tarfile(file_path):
+            with tarfile.open(file_path, 'r:*') as tar_ref:
+                file_list = []
+                for member in tar_ref.getmembers():
+                    file_list.append({
+                        'name': member.name,
+                        'size': member.size,
+                        'type': 'file' if member.isfile() else ('directory' if member.isdir() else 'other'),
+                        'mode': oct(member.mode) if member.mode else None,
+                        'uid': member.uid,
+                        'gid': member.gid,
+                        'mtime': member.mtime
+                    })
+                return {'type': 'tar', 'files': file_list}
+        
+        # Check if it's a RAR file (if rarfile is available)
+        elif RARFILE_AVAILABLE and mime_type and 'rar' in mime_type.lower():
+            try:
+                with rarfile.RarFile(file_path) as rar_ref:
+                    file_list = []
+                    for info in rar_ref.infolist():
+                        file_list.append({
+                            'name': info.filename,
+                            'size': info.file_size,
+                            'compressed_size': info.compress_size,
+                            'date_time': info.date_time,
+                            'type': 'file'
+                        })
+                    return {'type': 'rar', 'files': file_list}
+            except:
+                pass
+        
+        return None
+    except Exception as e:
+        print(f"Error reading archive {file_path}: {e}")
+        return None
+
+def is_archive_file(file_path, mime_type, filename):
+    """Check if file is a supported archive format"""
+    # Check by file extension
+    lower_filename = filename.lower()
+    if any(lower_filename.endswith(ext) for ext in ['.zip', '.tar', '.tar.gz', '.tar.bz2', '.tar.xz', '.tgz']):
+        return True
+    
+    if RARFILE_AVAILABLE and lower_filename.endswith('.rar'):
+        return True
+    
+    # Check by MIME type
+    if mime_type:
+        archive_mimes = [
+            'application/zip',
+            'application/x-zip-compressed',
+            'application/x-tar',
+            'application/x-gtar',
+            'application/x-compressed-tar',
+            'application/vnd.rar',
+            'application/x-rar-compressed'
+        ]
+        if any(mime in mime_type.lower() for mime in archive_mimes):
+            return True
+    
+    # Check by actual file content
+    try:
+        if zipfile.is_zipfile(file_path) or tarfile.is_tarfile(file_path):
+            return True
+    except:
+        pass
+    
+    return False
+
 def parse_arguments():
     """Parse command line arguments"""
     parser = argparse.ArgumentParser(description='Flask Temporary File Upload Server')
@@ -251,6 +353,13 @@ def download_page(file_id):
             # If can't read as text, treat as binary
             is_text_file = False
     
+    # Check if it's an archive file for preview
+    is_archive_file_flag = is_archive_file(full_path, mime_type, original_filename)
+    archive_contents = None
+    
+    if is_archive_file_flag:
+        archive_contents = get_archive_contents(full_path, mime_type)
+    
     return render_template('download.html', 
                          file_id=file_id,
                          filename=original_filename,
@@ -258,7 +367,9 @@ def download_page(file_id):
                          mime_type=mime_type,
                          expiration_date=expiration_date,
                          is_text_file=is_text_file,
-                         file_content=file_content)
+                         file_content=file_content,
+                         is_archive_file=is_archive_file_flag,
+                         archive_contents=archive_contents)
 
 @app.route('/download/<file_id>/direct')
 def download_file_direct(file_id):

+ 62 - 0
templates/download.html

@@ -120,6 +120,68 @@
                 <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>
+        <!-- Archive File Preview -->
+        {% elif is_archive_file and archive_contents %}
+        <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">Archive Contents</h3>
+                <span class="text-sm text-gray-500 dark:text-gray-400">{{ archive_contents.type|upper }} archive preview</span>
+            </div>
+            
+            <div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4 max-h-96 overflow-auto">
+                <div class="space-y-2">
+                    {% for file in archive_contents.files %}
+                    <div class="flex items-center justify-between py-2 px-3 bg-white dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700">
+                        <div class="flex items-center space-x-3">
+                            {% if file.type == 'directory' %}
+                            <svg class="w-4 h-4 text-blue-500" fill="currentColor" viewBox="0 0 20 20">
+                                <path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"></path>
+                            </svg>
+                            {% else %}
+                            <svg class="w-4 h-4 text-gray-500" fill="currentColor" viewBox="0 0 20 20">
+                                <path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z" clip-rule="evenodd"></path>
+                            </svg>
+                            {% endif %}
+                            <span class="text-sm font-medium text-gray-900 dark:text-white truncate">{{ file.name }}</span>
+                        </div>
+                        <div class="flex items-center space-x-4 text-xs text-gray-500 dark:text-gray-400">
+                            {% if file.type != 'directory' %}
+                            <span>{{ "%.1f"|format(file.size / 1024) }} KB</span>
+                            {% if file.compressed_size %}
+                            <span class="text-green-600 dark:text-green-400">{{ "%.1f%%"|format((1 - file.compressed_size / file.size) * 100) }} compressed</span>
+                            {% endif %}
+                            {% endif %}
+                            {% if file.date_time %}
+                            <span>{{ "%04d-%02d-%02d"|format(file.date_time[0], file.date_time[1], file.date_time[2]) }}</span>
+                            {% endif %}
+                        </div>
+                    </div>
+                    {% endfor %}
+                </div>
+            </div>
+            
+            <div class="mt-4 flex items-center justify-between text-xs text-gray-500 dark:text-gray-400">
+                <p>Archive contains {{ archive_contents.files|length }} items. Download to extract files.</p>
+                <div class="flex space-x-4">
+                    {% set total_files = archive_contents.files|selectattr('type', 'equalto', 'file')|list|length %}
+                    {% set total_dirs = archive_contents.files|selectattr('type', 'equalto', 'directory')|list|length %}
+                    <span>{{ total_files }} files</span>
+                    {% if total_dirs > 0 %}
+                    <span>{{ total_dirs }} folders</span>
+                    {% endif %}
+                </div>
+            </div>
+        </div>
+        {% elif is_archive_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">Archive Preview Not Available</h3>
+                <p class="text-gray-600 dark:text-gray-400">This archive file couldn't be previewed. Please download it to view the contents.</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">

+ 0 - 8
test.txt

@@ -1,8 +0,0 @@
-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!