Browse Source

HokoriTemp V2

GeoShaPoh 5 months ago
parent
commit
7d4b8d2233
17 changed files with 889 additions and 2268 deletions
  1. 0 221
      DATABASE.md
  2. 36 124
      README.md
  3. 0 166
      SETUP.md
  4. 277 224
      app.py
  5. 0 152
      cleanup.py
  6. 0 150
      database.py
  7. 0 45
      env.example
  8. BIN
      requirements.txt
  9. 0 60
      setup-proxy.sh
  10. 0 421
      static/app.js
  11. 79 0
      static/css/styles.css
  12. 339 0
      static/js/main.js
  13. 0 137
      static/styles.css
  14. 0 268
      static/view.js
  15. 0 48
      temp.mysite.conf
  16. 158 152
      templates/index.html
  17. 0 100
      templates/myfiles.html

+ 0 - 221
DATABASE.md

@@ -1,221 +0,0 @@
-# Base de Datos SQLite - HokoriTemp
-
-## Descripción
-
-Se ha implementado una base de datos SQLite para almacenar información de los archivos subidos, reemplazando el sistema anterior basado en localStorage.
-
-## Características
-
-### Base de Datos
-- **Archivo**: `files.db` (se crea automáticamente)
-- **Tabla**: `files`
-- **Campos**:
-  - `id`: Identificador único del archivo (UUID)
-  - `original_filename`: Nombre original del archivo
-  - `stored_filename`: Nombre del archivo en el servidor
-  - `file_size`: Tamaño del archivo en bytes
-  - `created_at`: Fecha de creación
-  - `expires_at`: Fecha de expiración
-  - `download_count`: Contador de descargas
-
-### Funcionalidades
-
-#### Subida de Archivos
-- Los archivos se guardan físicamente en la carpeta `uploads/`
-- La información se almacena en la base de datos
-- Se genera un enlace único para descarga
-- **Tiempo de vida configurable**: 1 hora, 6 horas, 12 horas, 24 horas, 2 días, 3 días, 7 días
-
-#### Gestión de Enlaces
-- Lista de todos los archivos subidos
-- Información detallada: nombre, tamaño, descargas, fecha de expiración
-- **Eliminación permanente**: archivo físico + registro en BD se eliminan completamente
-
-#### Descargas
-- Verificación de existencia en BD
-- Verificación de expiración
-- Contador de descargas automático
-- Descarga con nombre original del archivo
-
-## Tiempo de Vida Configurable
-
-### Opciones Disponibles
-- **1 hora**: Para archivos muy temporales
-- **6 horas**: Para uso durante el día
-- **12 horas**: Para uso durante medio día
-- **24 horas**: Por defecto, un día completo
-- **2 días**: Para archivos que necesitan más tiempo
-- **3 días**: Para archivos de uso extendido
-- **7 días**: Máximo tiempo permitido
-
-### Configuración
-El tiempo de vida se puede configurar desde la interfaz web antes de subir el archivo. El valor se envía al servidor y se almacena en la base de datos.
-
-## APIs Implementadas
-
-### GET `/api/links`
-Obtiene todos los enlaces guardados en la base de datos.
-
-**Respuesta**:
-```json
-[
-  {
-    "id": "uuid-del-archivo",
-    "filename": "documento.pdf",
-    "url": "http://localhost:5000/download/uuid-del-archivo",
-    "created_at": "2024-01-01T12:00:00",
-    "expires_at": "2024-01-02T12:00:00",
-    "file_size": 1024000,
-    "download_count": 5
-  }
-]
-```
-
-### DELETE `/api/delete/<file_id>`
-Elimina un archivo específico.
-
-**Respuesta**:
-```json
-{
-  "success": true,
-  "message": "Archivo eliminado correctamente"
-}
-```
-
-### GET `/api/stats`
-Obtiene estadísticas de la base de datos.
-
-**Respuesta**:
-```json
-{
-  "total_files": 150,
-  "active_files": 120,
-  "total_downloads": 2500
-}
-```
-
-## Scripts de Mantenimiento
-
-### cleanup.py
-Script para limpiar archivos expirados y huérfanos.
-
-**Uso**:
-```bash
-# Limpieza completa
-python cleanup.py
-
-# Solo estadísticas
-python cleanup.py --stats
-
-# Solo limpiar base de datos
-python cleanup.py --db-only
-
-# Solo limpiar archivos físicos
-python cleanup.py --files-only
-
-# Ayuda
-python cleanup.py --help
-```
-
-## Migración desde localStorage
-
-### Cambios en el Frontend
-- **app.js**: Eliminada función `saveLinkToStorage()`
-- **view.js**: Actualizado para usar APIs en lugar de localStorage
-- **myfiles.html**: Ahora muestra información desde la base de datos
-
-### Beneficios
-1. **Persistencia**: Los enlaces se mantienen aunque se reinicie el servidor
-2. **Seguridad**: Verificación de expiración en el servidor
-3. **Estadísticas**: Contador de descargas y métricas
-4. **Escalabilidad**: Preparado para múltiples usuarios
-5. **Mantenimiento**: Scripts automáticos de limpieza
-
-## Configuración
-
-### Variables de Entorno
-```bash
-# Tamaño máximo de archivo (en bytes)
-MAX_FILE_SIZE=16777216
-
-# Carpeta de uploads
-UPLOAD_FOLDER=uploads
-
-# Clave secreta para sesiones
-SECRET_KEY=tu_clave_secreta_aqui
-```
-
-### Base de Datos
-La base de datos se crea automáticamente al iniciar la aplicación:
-```python
-# Se ejecuta automáticamente en app.py
-init_database()
-```
-
-## Estructura de Archivos
-
-```
-hokoritemp/
-├── app.py              # Aplicación principal con APIs
-├── database.py         # Funciones de base de datos
-├── cleanup.py          # Script de limpieza
-├── files.db           # Base de datos SQLite (se crea automáticamente)
-├── uploads/           # Carpeta de archivos físicos
-├── static/
-│   ├── app.js         # JavaScript principal (actualizado)
-│   └── view.js        # JavaScript de vista (actualizado)
-└── templates/
-    ├── index.html      # Página principal
-    └── myfiles.html    # Página de enlaces (actualizada)
-```
-
-## Seguridad
-
-### Eliminación Permanente
-Los archivos se eliminan completamente tanto del servidor como de la base de datos, ya que son archivos temporales.
-
-### Verificación de Expiración
-- Se verifica en cada descarga
-- Se limpia automáticamente con el script de mantenimiento
-
-### Validación de Archivos
-- Verificación de tamaño antes y después de la subida
-- Nombres de archivo seguros con `secure_filename()`
-- UUIDs únicos para evitar colisiones
-
-## Mantenimiento
-
-### Limpieza Automática
-Se recomienda ejecutar el script de limpieza periódicamente:
-
-```bash
-# Agregar al crontab para ejecutar diariamente
-0 2 * * * cd /path/to/hokoritemp && python cleanup.py
-```
-
-### Backup
-```bash
-# Backup de la base de datos
-cp files.db files.db.backup
-
-# Backup de archivos
-tar -czf uploads_backup.tar.gz uploads/
-```
-
-## Troubleshooting
-
-### Error: "Archivo no encontrado"
-1. Verificar que el archivo existe en `uploads/`
-2. Verificar que el registro existe en la base de datos
-3. Ejecutar `python cleanup.py --stats` para ver estadísticas
-
-### Error: "Archivo expirado"
-- Los archivos expiran automáticamente después de 24 horas
-- Ejecutar `python cleanup.py` para limpiar archivos expirados
-
-### Base de datos corrupta
-```bash
-# Eliminar y recrear la base de datos
-rm files.db
-python app.py  # Se creará automáticamente
-``` 

+ 36 - 124
README.md

@@ -1,136 +1,48 @@
-# HokoriTemp - Servidor de Archivos Temporales
+# HokoriTemp v2
 
-Una aplicación web en Flask para subir y compartir archivos de forma temporal, similar a filebin pero con funcionalidades adicionales.
+Aplicación Flask para subida temporal de archivos con expiración automática.
 
-## Características
-
-- **Subida de archivos:** Interfaz drag & drop para subir archivos
-- **Enlaces temporales:** Los archivos expiran automáticamente después de 24 horas
-- **Gestión de enlaces:** Página para ver y gestionar todos los enlaces activos del usuario
-- **Almacenamiento local:** Los enlaces se guardan en localStorage del navegador
-- **Interfaz moderna:** Diseño responsive con Tailwind CSS
+## Novedades en la versión 2
+- **Nombre actualizado:** HokoriTemp
+- **Estructura mejorada:**
+  - Archivos JavaScript y CSS movidos a `/static/js/main.js` y `/static/css/styles.css`.
+  - Favicon SVG moderno en `/static/favicon.svg`.
+- **Soporte visual avanzado:**
+  - Mejor contraste y experiencia en modo claro/oscuro.
+  - Toast de notificación mejorado, sin errores visuales.
+  - Tooltips personalizados para nombres de archivos truncados.
+  - Truncado automático de nombres largos de archivos.
 
 ## Instalación
 
-1. **Clonar el repositorio:**
-```bash
-git clone https://git.checkthis.space/geoshapoh/hokoritemp.git
-cd hokoritemp
-```
-
-2. **Activar el entorno virtual:**
-```bash
-# Windows
-.\venv\Scripts\Activate.ps1
-
-# Linux/Mac
-source venv/bin/activate
-```
-
-3. **Instalar dependencias:**
-```bash
-pip install -r requirements.txt
-```
+1. Clona el repositorio y entra en la carpeta del proyecto.
+2. Instala las dependencias:
+   ```bash
+   pip install -r requirements.txt
+   ```
+3. Ejecuta la aplicación Flask:
+   ```bash
+   flask run
+   ```
 
 ## Uso
+- Accede a la interfaz web para subir archivos temporales.
+- Elige la duración de expiración.
+- Descarga o elimina tus archivos desde la lista.
+- Cambia entre modo claro y oscuro con el botón de la esquina.
 
-### Ejecutar el servidor
-
-```bash
-# Ejecutar con configuración por defecto (localhost:5000)
-python app.py
-
-# Ejecutar en puerto específico
-python app.py --port 8080
-
-# Ejecutar en host específico
-python app.py --host 0.0.0.0 --port 8080
-
-# Ejecutar en modo debug
-python app.py --debug
-
-# Ejecutar con HTTPS (requiere certificados)
-python app.py --https
+## Estructura relevante
 ```
-
-### Argumentos disponibles
-
-- `--host`: Host del servidor (default: 127.0.0.1)
-- `--port`: Puerto del servidor (default: 5000)
-- `--https`: Usar HTTPS
-- `--debug`: Modo debug
-
-## Estructura del proyecto
-
+static/
+  css/styles.css         # Estilos personalizados y mejoras dark/light
+  js/main.js             # Toda la lógica JS de la app
+  favicon.svg            # Favicon SVG moderno
+uploads/                 # Archivos subidos por los usuarios
+templates/
+  index.html             # Plantilla principal
+app.py                   # Backend Flask
 ```
-hokoritemp/
-├── app.py                 # Aplicación principal Flask
-├── requirements.txt       # Dependencias del proyecto
-├── uploads/              # Carpeta donde se guardan los archivos
-├── templates/            # Plantillas HTML
-│   ├── index.html       # Página principal de subida
-│   └── view.html        # Página de gestión de enlaces
-├── static/               # Archivos estáticos
-│   ├── styles.css       # Estilos CSS
-│   ├── app.js          # JavaScript principal
-│   └── view.js         # JavaScript para vista de enlaces
-└── README.md            # Este archivo
-```
-
-## Funcionalidades
-
-### Página Principal (/)
-- Interfaz drag & drop para subir archivos
-- Límite de 16MB por archivo
-- Generación automática de enlaces temporales
-- Guardado automático en localStorage
-
-### Página de Enlaces (/view)
-- Lista todos los enlaces guardados en localStorage
-- Muestra tiempo restante hasta expiración
-- Permite copiar y abrir enlaces
-- Filtra automáticamente enlaces expirados
-
-### API Endpoints
-- `POST /upload`: Subir archivo
-- `GET /download/<file_id>`: Descargar archivo
-- `GET /api/links`: Obtener enlaces (para futuras implementaciones)
-
-## Configuración
-
-### Variables de entorno
-Puedes crear un archivo `.env` con las siguientes variables:
-
-```env
-SECRET_KEY=tu_clave_secreta_aqui
-MAX_CONTENT_LENGTH=16777216
-UPLOAD_FOLDER=uploads
-```
-
-### Límites
-- Tamaño máximo de archivo: 16MB
-- Tiempo de expiración: 24 horas
-- Tipos de archivo: Todos los tipos permitidos
-
-## Desarrollo
-
-### Agregar nuevas funcionalidades
-1. Modifica `app.py` para agregar nuevas rutas
-2. Actualiza las plantillas HTML según sea necesario
-3. Agrega nuevas dependencias a `requirements.txt`
-
-### Personalización
-- Cambia el tiempo de expiración en `app.py` (línea 58)
-- Modifica el límite de tamaño en `app.py` (línea 12)
-- Personaliza el diseño editando las plantillas HTML
-
-## Seguridad
-
-- Los archivos se guardan con nombres únicos (UUID)
-- Se usa `secure_filename` para sanitizar nombres de archivo
-- Los archivos expiran automáticamente
-- No se almacenan metadatos sensibles
 
-## Licencia
+---
 
-Este proyecto está bajo la licencia MIT. Ver el archivo LICENSE para más detalles. 
+¡Disfruta usando HokoriTemp v2! Si tienes sugerencias, abre un issue o contacta al autor.

+ 0 - 166
SETUP.md

@@ -1,166 +0,0 @@
-# Configuración del Proyecto HokoriTemp
-
-## Configuración Inicial
-
-### 1. Variables de Entorno
-
-Copia el archivo `env.example` como `.env` y configura las variables según tu entorno:
-
-```bash
-cp env.example .env
-```
-
-### 2. Configurar .env
-
-Edita el archivo `.env` con tus valores:
-
-```env
-# Clave secreta (¡OBLIGATORIO cambiar!)
-SECRET_KEY=tu_clave_secreta_muy_segura_aqui
-
-# Configuración del servidor
-HOST=127.0.0.1
-PORT=5000
-DEBUG=False
-
-# Configuración del dominio (para reverse proxy)
-DOMAIN=temp.mysite.net
-
-# Configuración de archivos
-MAX_FILE_SIZE=16777216  # 16MB en bytes
-UPLOAD_FOLDER=uploads
-FILE_EXPIRATION_HOURS=24
-```
-
-### 3. Generar Clave Secreta
-
-Para generar una clave secreta segura, puedes usar Python:
-
-```python
-import secrets
-print(secrets.token_hex(32))
-```
-
-## Estructura de Archivos
-
-```
-hokoritemp/
-├── app.py                 # Aplicación principal
-├── requirements.txt       # Dependencias
-├── .env                  # Variables de entorno (crear desde env.example)
-├── .gitignore           # Archivos a ignorar en Git
-├── env.example          # Ejemplo de variables de entorno
-├── README.md            # Documentación principal
-├── SETUP.md             # Este archivo
-├── uploads/             # Carpeta de archivos (se crea automáticamente)
-└── templates/           # Plantillas HTML
-    ├── index.html
-    └── view.html
-```
-
-## Comandos Útiles
-
-### Desarrollo
-```bash
-# Activar entorno virtual
-.\venv\Scripts\Activate.ps1
-
-# Instalar dependencias
-pip install -r requirements.txt
-
-# Ejecutar en modo desarrollo
-python app.py --debug
-
-# Ejecutar en puerto específico
-python app.py --port 8080
-```
-
-### Producción
-```bash
-# Ejecutar en modo producción
-python app.py --host 0.0.0.0 --port 80
-
-# Con HTTPS (requiere certificados)
-python app.py --https
-```
-
-## Variables de Entorno Disponibles
-
-| Variable | Descripción | Valor por Defecto |
-|----------|-------------|-------------------|
-| `SECRET_KEY` | Clave secreta de Flask | `tu_clave_secreta_aqui` |
-| `HOST` | Host del servidor | `127.0.0.1` |
-| `PORT` | Puerto del servidor | `5000` |
-| `DEBUG` | Modo debug | `False` |
-| `DOMAIN` | Dominio para enlaces (reverse proxy) | `temp.mysite.net` |
-| `MAX_FILE_SIZE` | Tamaño máximo de archivo (bytes) | `16777216` (16MB) |
-| `UPLOAD_FOLDER` | Carpeta de archivos | `uploads` |
-| `FILE_EXPIRATION_HOURS` | Horas hasta expiración | `24` |
-
-## Seguridad
-
-### Archivos Sensibles
-- `.env` - Contiene variables sensibles (NO subir a Git)
-- `uploads/` - Contiene archivos subidos (NO subir a Git)
-- `venv/` - Entorno virtual (NO subir a Git)
-
-### Recomendaciones
-1. **Nunca** subas el archivo `.env` a Git
-2. Cambia la `SECRET_KEY` por defecto
-3. Configura límites de archivo apropiados
-4. Usa HTTPS en producción
-5. Configura un firewall apropiado
-
-## Troubleshooting
-
-### Error: "No module named 'dotenv'"
-```bash
-pip install python-dotenv
-```
-
-### Error: "Permission denied" al crear carpeta uploads
-```bash
-# En Windows, ejecutar como administrador
-# En Linux/Mac
-chmod 755 uploads/
-```
-
-### Error: "Address already in use"
-```bash
-# Cambiar puerto
-python app.py --port 8080
-```
-
-## Despliegue
-
-### Con Apache Reverse Proxy
-1. Copiar `temp.mysite.conf` a `/etc/httpd/conf.d/`
-2. Configurar el dominio en `.env`
-3. Ejecutar la app: `python app.py --host 127.0.0.1 --port 5000`
-4. Acceder a `http://temp.mysite.net`
-
-### Heroku
-1. Crear `Procfile`:
-```
-web: python app.py --host 0.0.0.0 --port $PORT
-```
-
-2. Configurar variables de entorno en Heroku Dashboard
-
-### Docker
-1. Crear `Dockerfile`:
-```dockerfile
-FROM python:3.9-slim
-WORKDIR /app
-COPY requirements.txt .
-RUN pip install -r requirements.txt
-COPY . .
-EXPOSE 5000
-CMD ["python", "app.py", "--host", "0.0.0.0"]
-```
-
-2. Construir y ejecutar:
-```bash
-docker build -t hokoritemp .
-docker run -p 5000:5000 hokoritemp
-``` 

+ 277 - 224
app.py

@@ -1,254 +1,307 @@
 import os
-import argparse
+import sqlite3
 import uuid
-import shutil
+import hashlib
+import threading
+import time
+import secrets
+import string
+import argparse
 from datetime import datetime, timedelta
-from flask import Flask, render_template, request, jsonify, send_file, redirect, url_for
 from werkzeug.utils import secure_filename
-from dotenv import load_dotenv
-import json
-from database import init_database, save_file, get_file_by_id, get_all_files, delete_file, hard_delete_file, increment_download_count, cleanup_expired_files, get_file_stats
+from flask import Flask, request, jsonify, render_template, send_from_directory, abort
+from werkzeug.security import generate_password_hash
+import re
 
-# Cargar variables de entorno desde .env
-load_dotenv()
+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))
 
-app = Flask(__name__)
-app.config['MAX_CONTENT_LENGTH'] = int(os.getenv('MAX_FILE_SIZE', 16 * 1024 * 1024))  # 16MB max file size
-app.config['UPLOAD_FOLDER'] = os.getenv('UPLOAD_FOLDER', 'uploads')
-app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'tu_clave_secreta_aqui')
-
-# Manejador de errores para archivos demasiado grandes
-@app.errorhandler(413)
-def too_large(e):
-    return jsonify({
-        'error': f'El archivo es demasiado grande. Tamaño máximo: {app.config["MAX_CONTENT_LENGTH"] // (1024*1024)}MB'
-    }), 413
-
-# Manejador de errores general
-@app.errorhandler(500)
-def internal_error(e):
-    return jsonify({'error': 'Error interno del servidor'}), 500
-
-@app.errorhandler(404)
-def not_found(e):
-    return jsonify({'error': 'Página no encontrada'}), 404
-
-# Verificar y crear carpeta de uploads si no existe
-def ensure_upload_folder():
-    if not os.path.exists(app.config['UPLOAD_FOLDER']):
-        os.makedirs(app.config['UPLOAD_FOLDER'])
-        print(f"Carpeta '{app.config['UPLOAD_FOLDER']}' creada automáticamente.")
-
-# Inicializar base de datos
-def init_app():
-    ensure_upload_folder()
-    init_database()
-    print("Base de datos inicializada correctamente.")
-
-# Configurar argumentos de línea de comandos
 def parse_arguments():
-    parser = argparse.ArgumentParser(description='Servidor de archivos temporales')
-    parser.add_argument('--host', default=os.getenv('HOST', '127.0.0.1'), help='Host del servidor (default: 127.0.0.1)')
-    parser.add_argument('--port', type=int, default=int(os.getenv('PORT', 5000)), help='Puerto del servidor (default: 5000)')
-    parser.add_argument('--https', action='store_true', help='Usar HTTPS')
-    parser.add_argument('--debug', action='store_true', default=os.getenv('DEBUG', 'False').lower() == 'true', help='Modo debug')
+    """Parse command line arguments"""
+    parser = argparse.ArgumentParser(description='Flask Temporary File Upload Server')
+    parser.add_argument('--host', default='127.0.0.1', help='Host to bind to (default: 127.0.0.1)')
+    parser.add_argument('--port', type=int, default=5000, help='Port to bind to (default: 5000)')
+    parser.add_argument('--debug', action='store_true', help='Enable debug mode')
+    parser.add_argument('--base-url', default=None, help='Base URL for download links (e.g., https://mysite.com)')
+    parser.add_argument('--network', action='store_true', help='Bind to all network interfaces (0.0.0.0)')
+    parser.add_argument('--max-file-size', type=int, default=16, help='Maximum file size in MB (default: 16)')
     return parser.parse_args()
 
-@app.route('/')
-def index():
-    """Página principal para subir archivos"""
-    # Pasar configuración al template
-    max_file_size = app.config['MAX_CONTENT_LENGTH']
-    max_file_size_mb = max_file_size // (1024 * 1024)
+# Parse command line arguments
+args = parse_arguments()
+
+# Set host based on arguments
+if args.network:
+    HOST = '0.0.0.0'
+else:
+    HOST = args.host
+
+# Set base URL for download links
+BASE_URL = args.base_url or os.environ.get('BASE_URL', f'http://{HOST}:{args.port}')
+
+# Validate max file size
+if args.max_file_size <= 0:
+    print("❌ Error: Maximum file size must be a positive integer")
+    exit(1)
+
+app = Flask(__name__)
+app.config['SECRET_KEY'] = generate_secret_key(32)
+app.config['UPLOAD_FOLDER'] = 'uploads'
+app.config['MAX_CONTENT_LENGTH'] = args.max_file_size * 1024 * 1024  # Convert MB to bytes
+app.config['BASE_URL'] = BASE_URL
+
+# Ensure upload directory exists
+os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
+
+# Database initialization
+def init_db():
+    conn = sqlite3.connect('files.db')
+    cursor = conn.cursor()
+    cursor.execute('''
+        CREATE TABLE IF NOT EXISTS files (
+            id TEXT PRIMARY KEY,
+            original_filename TEXT NOT NULL,
+            sanitized_filename TEXT NOT NULL,
+            file_path TEXT NOT NULL,
+            user_session TEXT NOT NULL,
+            upload_date TEXT NOT NULL,
+            expiration_date TEXT NOT NULL,
+            duration_hours INTEGER NOT NULL,
+            file_size INTEGER NOT NULL,
+            mime_type TEXT
+        )
+    ''')
+    conn.commit()
+    conn.close()
+
+def sanitize_filename(filename):
+    """Sanitize filename for safe storage"""
+    # Remove special characters and spaces
+    name, ext = os.path.splitext(filename)
+    sanitized = re.sub(r'[^a-zA-Z0-9_-]', '_', name)
+    # Generate unique identifier
+    unique_id = str(uuid.uuid4())[:8]
+    return f"{sanitized}_{unique_id}{ext}"
+
+def cleanup_expired_files():
+    """Remove expired files from database and filesystem"""
+    conn = sqlite3.connect('files.db')
+    cursor = conn.cursor()
+    
+    # Get expired files
+    cursor.execute('''
+        SELECT file_path FROM files 
+        WHERE expiration_date < ?
+    ''', (datetime.now().isoformat(),))
     
-    return render_template('index.html', 
-                         max_file_size=max_file_size,
-                         max_file_size_mb=max_file_size_mb)
+    expired_files = cursor.fetchall()
+    
+    # Delete files from filesystem
+    for (file_path,) in expired_files:
+        full_path = os.path.join(app.config['UPLOAD_FOLDER'], file_path)
+        try:
+            if os.path.exists(full_path):
+                os.remove(full_path)
+                print(f"Deleted expired file: {file_path}")
+        except Exception as e:
+            print(f"Error deleting file {file_path}: {e}")
+    
+    # Remove from database
+    cursor.execute('DELETE FROM files WHERE expiration_date < ?', 
+                  (datetime.now().isoformat(),))
+    
+    conn.commit()
+    conn.close()
+    print(f"Cleaned up {len(expired_files)} expired files")
+
+def cleanup_worker():
+    """Background worker to cleanup expired files every 10 minutes"""
+    while True:
+        time.sleep(600)  # 10 minutes
+        cleanup_expired_files()
 
-@app.route('/myfiles')
-def myfiles():
-    """Página para ver enlaces guardados en localStorage"""
-    return render_template('myfiles.html')
+@app.route('/')
+def index():
+    max_file_size = app.config['MAX_CONTENT_LENGTH'] // (1024 * 1024)  # Convert bytes to MB
+    return render_template('index.html', max_file_size=max_file_size)
 
 @app.route('/upload', methods=['POST'])
 def upload_file():
-    """Manejar la subida de archivos"""
-    try:
-        # Verificar si hay archivo en la request
-        if 'file' not in request.files:
-            return jsonify({'error': 'No se seleccionó ningún archivo'}), 400
-        
-        file = request.files['file']
-        if file.filename == '':
-            return jsonify({'error': 'No se seleccionó ningún archivo'}), 400
-        
-        # Obtener tiempo de vida del archivo (por defecto 24 horas)
-        expires_hours = request.form.get('expires_hours', 24, type=int)
+    if 'file' not in request.files:
+        return jsonify({'error': 'No file provided'}), 400
+    
+    file = request.files['file']
+    user_session = request.form.get('user_session')
+    duration_hours = int(request.form.get('duration_hours', 24))
+    
+    if file.filename == '':
+        return jsonify({'error': 'No file selected'}), 400
+    
+    if not user_session:
+        return jsonify({'error': 'No user session provided'}), 400
+    
+    if file:
+        # Generate file info
+        file_id = str(uuid.uuid4())
+        original_filename = file.filename
+        sanitized_filename = sanitize_filename(original_filename)
+        upload_date = datetime.now()
+        expiration_date = upload_date + timedelta(hours=duration_hours)
         
-        # Validar tiempo de vida (entre 1 hora y 7 días)
-        if expires_hours < 1 or expires_hours > 168:  # 168 horas = 7 días
-            expires_hours = 24
+        # Save file
+        file_path = sanitized_filename
+        full_path = os.path.join(app.config['UPLOAD_FOLDER'], file_path)
+        file.save(full_path)
         
-        # Validar tamaño del archivo antes de procesar
-        max_size = app.config['MAX_CONTENT_LENGTH']
-        max_size_mb = max_size // (1024 * 1024)
+        # Get file info
+        file_size = os.path.getsize(full_path)
+        mime_type = file.content_type
         
-        # Verificar content_length si está disponible
-        if request.content_length and request.content_length > max_size:
-            return jsonify({
-                'error': f'El archivo es demasiado grande. Tamaño máximo: {max_size_mb}MB'
-            }), 413
+        # Save to database
+        conn = sqlite3.connect('files.db')
+        cursor = conn.cursor()
+        cursor.execute('''
+            INSERT INTO files (id, original_filename, sanitized_filename, file_path, 
+                             user_session, upload_date, expiration_date, duration_hours, 
+                             file_size, mime_type)
+            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+        ''', (file_id, original_filename, sanitized_filename, file_path, user_session,
+              upload_date.isoformat(), expiration_date.isoformat(), duration_hours,
+              file_size, mime_type))
+        conn.commit()
+        conn.close()
         
-        # Verificar el tamaño del archivo después de recibirlo
-        if file:
-            # Leer el archivo en chunks para verificar el tamaño
-            file.seek(0, 2)  # Ir al final del archivo
-            file_size = file.tell()
-            file.seek(0)  # Volver al inicio
-            
-            if file_size > max_size:
-                return jsonify({
-                    'error': f'El archivo es demasiado grande. Tamaño máximo: {max_size_mb}MB'
-                }), 413
-            
-            # Generar nombre único para el archivo
-            original_filename = file.filename
-            if not original_filename:
-                return jsonify({'error': 'Nombre de archivo no válido'}), 400
-                
-            filename = secure_filename(original_filename)
-            if not filename:
-                return jsonify({'error': 'Nombre de archivo no válido'}), 400
-                
-            file_id = str(uuid.uuid4())
-            file_extension = os.path.splitext(filename)[1]
-            unique_filename = f"{file_id}{file_extension}"
-            
-            # Guardar archivo
-            file_path = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename)
-            file.save(file_path)
-            
-            # Guardar en base de datos con el tiempo de vida especificado
-            save_file(file_id, original_filename, unique_filename, file_size, expires_hours)
-            
-            # Crear enlace temporal con el tiempo de vida especificado
-            # Usar el dominio configurado o el host actual
-            domain = os.getenv('DOMAIN', request.host)
-            protocol = 'https' if args.https else 'http'
-            download_url = f"{protocol}://{domain}/download/{file_id}"
-            
-            return jsonify({
-                'success': True,
-                'download_url': download_url,
-                'file_id': file_id,
-                'original_filename': original_filename,
-                'expires_at': (datetime.now() + timedelta(hours=expires_hours)).isoformat()
-            })
-        else:
-            return jsonify({'error': 'No se pudo procesar el archivo'}), 400
-            
-    except Exception as e:
-        print(f"Error en upload: {str(e)}")
-        return jsonify({'error': 'Error interno del servidor'}), 500
+        return jsonify({
+            'success': True,
+            'file_id': file_id,
+            'original_filename': original_filename,
+            'download_url': f'{app.config["BASE_URL"]}/download/{file_id}',
+            'expiration_date': expiration_date.isoformat(),
+            'file_size': file_size
+        })
+
+@app.route('/files/<user_session>')
+def get_user_files(user_session):
+    conn = sqlite3.connect('files.db')
+    cursor = conn.cursor()
+    cursor.execute('''
+        SELECT id, original_filename, upload_date, expiration_date, 
+               duration_hours, file_size, mime_type
+        FROM files 
+        WHERE user_session = ? AND expiration_date > ?
+        ORDER BY upload_date DESC
+    ''', (user_session, datetime.now().isoformat()))
+    
+    files = cursor.fetchall()
+    conn.close()
+    
+    file_list = []
+    for file_data in files:
+        file_list.append({
+            'id': file_data[0],
+            'original_filename': file_data[1],
+            'upload_date': file_data[2],
+            'expiration_date': file_data[3],
+            'duration_hours': file_data[4],
+            'file_size': file_data[5],
+            'mime_type': file_data[6],
+            'download_url': f'{app.config["BASE_URL"]}/download/{file_data[0]}'
+        })
+    
+    return jsonify(file_list)
 
 @app.route('/download/<file_id>')
 def download_file(file_id):
-    """Descargar archivo por ID"""
-    # Buscar archivo en la base de datos
-    file_info = get_file_by_id(file_id)
-    if not file_info:
-        return "Archivo no encontrado", 404
-    
-    # Verificar si el archivo ha expirado
-    expires_at = datetime.fromisoformat(file_info['expires_at'])
-    if expires_at <= datetime.now():
-        return "Archivo expirado", 410
-    
-    # Verificar si el archivo existe físicamente
-    file_path = os.path.join(app.config['UPLOAD_FOLDER'], file_info['stored_filename'])
-    if not os.path.exists(file_path):
-        return "Archivo no encontrado", 404
-    
-    # Incrementar contador de descargas
-    increment_download_count(file_id)
-    
-    # Enviar archivo
-    return send_file(file_path, as_attachment=True, download_name=file_info['original_filename'])
-
-@app.route('/api/links')
-def get_links():
-    """API para obtener enlaces guardados desde la base de datos"""
-    try:
-        files = get_all_files()
-        links = []
-        
-        for file_info in files:
-            # Crear URL de descarga
-            domain = os.getenv('DOMAIN', request.host)
-            protocol = 'https' if args.https else 'http'
-            download_url = f"{protocol}://{domain}/download/{file_info['id']}"
-            
-            links.append({
-                'id': file_info['id'],
-                'filename': file_info['original_filename'],
-                'url': download_url,
-                'created_at': file_info['created_at'],
-                'expires_at': file_info['expires_at'],
-                'file_size': file_info['file_size'],
-                'download_count': file_info['download_count']
-            })
-        
-        return jsonify(links)
-    except Exception as e:
-        print(f"Error obteniendo enlaces: {str(e)}")
-        return jsonify({'error': 'Error interno del servidor'}), 500
-
-@app.route('/api/delete/<file_id>', methods=['DELETE'])
-def delete_link(file_id):
-    """API para eliminar un enlace permanentemente"""
-    try:
-        # Obtener información del archivo
-        file_info = get_file_by_id(file_id)
-        if not file_info:
-            return jsonify({'error': 'Archivo no encontrado'}), 404
-        
-        # Eliminar archivo físico
-        file_path = os.path.join(app.config['UPLOAD_FOLDER'], file_info['stored_filename'])
-        if os.path.exists(file_path):
-            os.remove(file_path)
-        
-        # Eliminar permanentemente de la base de datos
-        hard_delete_file(file_id)
-        
-        return jsonify({'success': True, 'message': 'Archivo eliminado permanentemente'})
-    except Exception as e:
-        print(f"Error eliminando archivo: {str(e)}")
-        return jsonify({'error': 'Error interno del servidor'}), 500
+    conn = sqlite3.connect('files.db')
+    cursor = conn.cursor()
+    cursor.execute('''
+        SELECT original_filename, file_path, expiration_date
+        FROM files 
+        WHERE id = ?
+    ''', (file_id,))
+    
+    file_data = cursor.fetchone()
+    conn.close()
+    
+    if not file_data:
+        abort(404)
+    
+    original_filename, file_path, expiration_date = file_data
+    
+    # Check if file has expired
+    if datetime.fromisoformat(expiration_date) < datetime.now():
+        abort(410)  # Gone
+    
+    # Check if file exists
+    full_path = os.path.join(app.config['UPLOAD_FOLDER'], file_path)
+    if not os.path.exists(full_path):
+        abort(404)
+    
+    return send_from_directory(app.config['UPLOAD_FOLDER'], file_path, 
+                              as_attachment=True, download_name=original_filename)
 
-@app.route('/api/stats')
-def get_stats():
-    """API para obtener estadísticas"""
+@app.route('/delete/<file_id>', methods=['DELETE'])
+def delete_file(file_id):
+    user_session = request.json.get('user_session')
+    
+    if not user_session:
+        return jsonify({'error': 'No user session provided'}), 400
+    
+    conn = sqlite3.connect('files.db')
+    cursor = conn.cursor()
+    
+    # Get file info and verify ownership
+    cursor.execute('''
+        SELECT file_path FROM files 
+        WHERE id = ? AND user_session = ?
+    ''', (file_id, user_session))
+    
+    file_data = cursor.fetchone()
+    
+    if not file_data:
+        conn.close()
+        return jsonify({'error': 'File not found or unauthorized'}), 404
+    
+    file_path = file_data[0]
+    
+    # Delete from filesystem
+    full_path = os.path.join(app.config['UPLOAD_FOLDER'], file_path)
     try:
-        stats = get_file_stats()
-        return jsonify(stats)
+        if os.path.exists(full_path):
+            os.remove(full_path)
     except Exception as e:
-        print(f"Error obteniendo estadísticas: {str(e)}")
-        return jsonify({'error': 'Error interno del servidor'}), 500
+        print(f"Error deleting file {file_path}: {e}")
+    
+    # Delete from database
+    cursor.execute('DELETE FROM files WHERE id = ? AND user_session = ?', 
+                  (file_id, user_session))
+    
+    conn.commit()
+    conn.close()
+    
+    return jsonify({'success': True})
 
 if __name__ == '__main__':
-    args = parse_arguments()
+    # Print configuration info
+    print(f"🚀 Starting Flask Temporary File Upload Server")
+    print(f"📁 Upload folder: {app.config['UPLOAD_FOLDER']}")
+    print(f"🔑 Secret key: {app.config['SECRET_KEY'][:8]}... (32 chars)")
+    print(f"🌐 Base URL: {app.config['BASE_URL']}")
+    print(f"🖥️  Server: http://{HOST}:{args.port}")
+    print(f"🔒 Max file size: {args.max_file_size}MB")
+    print(f"⏰ Cleanup interval: 10 minutes")
+    print(f"🐛 Debug mode: {'ON' if args.debug else 'OFF'}")
+    print("-" * 50)
     
-    # Inicializar aplicación (carpeta de uploads y base de datos)
-    init_app()
+    init_db()
     
-    # Configurar protocolo
-    protocol = 'https' if args.https else 'http'
+    # Start cleanup worker in background
+    cleanup_thread = threading.Thread(target=cleanup_worker, daemon=True)
+    cleanup_thread.start()
     
-    print(f"Servidor iniciando en {protocol}://{args.host}:{args.port}")
-    print(f"Modo debug: {'Activado' if args.debug else 'Desactivado'}")
+    # Initial cleanup
+    cleanup_expired_files()
     
-    app.run(
-        host=args.host,
-        port=args.port,
-        debug=args.debug
-    ) 
+    app.run(debug=args.debug, host=HOST, port=args.port)

+ 0 - 152
cleanup.py

@@ -1,152 +0,0 @@
-#!/usr/bin/env python3
-"""
-Script de limpieza para HokoriTemp
-Elimina archivos expirados de la base de datos y del sistema de archivos
-"""
-
-import os
-import sys
-from datetime import datetime
-from database import cleanup_expired_files, get_all_files, hard_delete_file
-
-def cleanup_physical_files(upload_folder='uploads'):
-    """Eliminar archivos físicos que ya no están en la base de datos"""
-    if not os.path.exists(upload_folder):
-        print(f"La carpeta {upload_folder} no existe.")
-        return
-    
-    # Obtener todos los archivos en la base de datos
-    db_files = get_all_files()
-    db_filenames = {file['stored_filename'] for file in db_files}
-    
-    # Obtener todos los archivos físicos
-    physical_files = os.listdir(upload_folder)
-    
-    # Encontrar archivos huérfanos (físicos pero no en BD)
-    orphaned_files = []
-    for filename in physical_files:
-        if filename not in db_filenames:
-            orphaned_files.append(filename)
-    
-    # Eliminar archivos huérfanos
-    deleted_count = 0
-    for filename in orphaned_files:
-        file_path = os.path.join(upload_folder, filename)
-        try:
-            os.remove(file_path)
-            deleted_count += 1
-            print(f"Eliminado archivo huérfano: {filename}")
-        except Exception as e:
-            print(f"Error eliminando {filename}: {e}")
-    
-    print(f"Se eliminaron {deleted_count} archivos huérfanos.")
-
-def cleanup_expired_from_db():
-    """Limpiar archivos expirados de la base de datos permanentemente"""
-    try:
-        from database import get_active_files, hard_delete_file
-        import os
-        
-        # Obtener archivos activos (no expirados)
-        active_files = get_active_files()
-        active_file_ids = {file['id'] for file in active_files}
-        
-        # Obtener todos los archivos
-        all_files = get_all_files()
-        
-        # Encontrar archivos expirados
-        expired_files = []
-        for file in all_files:
-            if file['id'] not in active_file_ids:
-                expired_files.append(file)
-        
-        # Eliminar archivos expirados permanentemente
-        deleted_count = 0
-        for file in expired_files:
-            try:
-                # Eliminar archivo físico
-                file_path = os.path.join('uploads', file['stored_filename'])
-                if os.path.exists(file_path):
-                    os.remove(file_path)
-                
-                # Eliminar de la base de datos
-                hard_delete_file(file['id'])
-                deleted_count += 1
-                print(f"Eliminado archivo expirado: {file['original_filename']}")
-            except Exception as e:
-                print(f"Error eliminando archivo expirado {file['original_filename']}: {e}")
-        
-        print(f"Se eliminaron permanentemente {deleted_count} archivos expirados.")
-        return deleted_count
-    except Exception as e:
-        print(f"Error limpiando base de datos: {e}")
-        return 0
-
-def get_stats():
-    """Mostrar estadísticas de la base de datos"""
-    try:
-        from database import get_file_stats
-        stats = get_file_stats()
-        print(f"\nEstadísticas:")
-        print(f"- Total de archivos: {stats['total_files']}")
-        print(f"- Archivos activos: {stats['active_files']}")
-        print(f"- Total de descargas: {stats['total_downloads']}")
-    except Exception as e:
-        print(f"Error obteniendo estadísticas: {e}")
-
-def main():
-    """Función principal del script de limpieza"""
-    print("=== HokoriTemp - Script de Limpieza ===")
-    
-    # Verificar argumentos
-    if len(sys.argv) > 1:
-        command = sys.argv[1]
-        
-        if command == '--help' or command == '-h':
-            print("""
-Uso: python cleanup.py [comando]
-
-Comandos disponibles:
-  --help, -h     Mostrar esta ayuda
-  --stats        Mostrar estadísticas de la base de datos
-  --db-only      Solo limpiar base de datos (no archivos físicos)
-  --files-only   Solo limpiar archivos físicos huérfanos
-  --all          Limpiar todo (por defecto)
-
-Ejemplos:
-  python cleanup.py --stats
-  python cleanup.py --db-only
-  python cleanup.py --all
-            """)
-            return
-        
-        elif command == '--stats':
-            get_stats()
-            return
-        
-        elif command == '--db-only':
-            print("Limpiando solo base de datos...")
-            cleanup_expired_from_db()
-            return
-        
-        elif command == '--files-only':
-            print("Limpiando solo archivos físicos...")
-            cleanup_physical_files()
-            return
-    
-    # Limpieza completa por defecto
-    print("Iniciando limpieza completa...")
-    
-    # Limpiar base de datos
-    db_deleted = cleanup_expired_from_db()
-    
-    # Limpiar archivos físicos
-    cleanup_physical_files()
-    
-    # Mostrar estadísticas finales
-    get_stats()
-    
-    print("\nLimpieza completada.")
-
-if __name__ == '__main__':
-    main() 

+ 0 - 150
database.py

@@ -1,150 +0,0 @@
-import sqlite3
-import os
-from datetime import datetime, timedelta
-from contextlib import contextmanager
-
-DATABASE_PATH = 'files.db'
-
-def init_database():
-    """Inicializar la base de datos y crear las tablas necesarias"""
-    with get_db_connection() as conn:
-        cursor = conn.cursor()
-        
-        # Crear tabla de archivos
-        cursor.execute('''
-            CREATE TABLE IF NOT EXISTS files (
-                id TEXT PRIMARY KEY,
-                original_filename TEXT NOT NULL,
-                stored_filename TEXT NOT NULL,
-                file_size INTEGER NOT NULL,
-                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-                expires_at TIMESTAMP NOT NULL,
-                download_count INTEGER DEFAULT 0,
-                is_deleted BOOLEAN DEFAULT FALSE
-            )
-        ''')
-        
-        conn.commit()
-
-@contextmanager
-def get_db_connection():
-    """Context manager para conexiones a la base de datos"""
-    conn = sqlite3.connect(DATABASE_PATH)
-    conn.row_factory = sqlite3.Row  # Permite acceder a las columnas por nombre
-    try:
-        yield conn
-    finally:
-        conn.close()
-
-def save_file(file_id, original_filename, stored_filename, file_size, expires_hours=24):
-    """Guardar información de un archivo en la base de datos"""
-    expires_at = datetime.now() + timedelta(hours=expires_hours)
-    
-    with get_db_connection() as conn:
-        cursor = conn.cursor()
-        cursor.execute('''
-            INSERT INTO files (id, original_filename, stored_filename, file_size, expires_at)
-            VALUES (?, ?, ?, ?, ?)
-        ''', (file_id, original_filename, stored_filename, file_size, expires_at.isoformat()))
-        conn.commit()
-
-def get_file_by_id(file_id):
-    """Obtener información de un archivo por su ID"""
-    with get_db_connection() as conn:
-        cursor = conn.cursor()
-        cursor.execute('''
-            SELECT * FROM files 
-            WHERE id = ? AND is_deleted = FALSE
-        ''', (file_id,))
-        return cursor.fetchone()
-
-def get_all_files():
-    """Obtener todos los archivos no eliminados"""
-    with get_db_connection() as conn:
-        cursor = conn.cursor()
-        cursor.execute('''
-            SELECT * FROM files 
-            WHERE is_deleted = FALSE 
-            ORDER BY created_at DESC
-        ''')
-        return cursor.fetchall()
-
-def get_active_files():
-    """Obtener solo archivos activos (no expirados y no eliminados)"""
-    with get_db_connection() as conn:
-        cursor = conn.cursor()
-        cursor.execute('''
-            SELECT * FROM files 
-            WHERE is_deleted = FALSE AND expires_at > ?
-            ORDER BY created_at DESC
-        ''', (datetime.now().isoformat(),))
-        return cursor.fetchall()
-
-def delete_file(file_id):
-    """Marcar un archivo como eliminado (soft delete)"""
-    with get_db_connection() as conn:
-        cursor = conn.cursor()
-        cursor.execute('''
-            UPDATE files 
-            SET is_deleted = TRUE 
-            WHERE id = ?
-        ''', (file_id,))
-        conn.commit()
-        return cursor.rowcount > 0
-
-def hard_delete_file(file_id):
-    """Eliminar completamente un archivo de la base de datos"""
-    with get_db_connection() as conn:
-        cursor = conn.cursor()
-        cursor.execute('DELETE FROM files WHERE id = ?', (file_id,))
-        conn.commit()
-        return cursor.rowcount > 0
-
-def increment_download_count(file_id):
-    """Incrementar el contador de descargas de un archivo"""
-    with get_db_connection() as conn:
-        cursor = conn.cursor()
-        cursor.execute('''
-            UPDATE files 
-            SET download_count = download_count + 1 
-            WHERE id = ?
-        ''', (file_id,))
-        conn.commit()
-
-def cleanup_expired_files():
-    """Eliminar archivos expirados de la base de datos"""
-    with get_db_connection() as conn:
-        cursor = conn.cursor()
-        cursor.execute('''
-            UPDATE files 
-            SET is_deleted = TRUE 
-            WHERE expires_at <= ? AND is_deleted = FALSE
-        ''', (datetime.now().isoformat(),))
-        conn.commit()
-        return cursor.rowcount
-
-def get_file_stats():
-    """Obtener estadísticas de archivos"""
-    with get_db_connection() as conn:
-        cursor = conn.cursor()
-        
-        # Total de archivos
-        cursor.execute('SELECT COUNT(*) FROM files WHERE is_deleted = FALSE')
-        total_files = cursor.fetchone()[0]
-        
-        # Archivos activos
-        cursor.execute('''
-            SELECT COUNT(*) FROM files 
-            WHERE is_deleted = FALSE AND expires_at > ?
-        ''', (datetime.now().isoformat(),))
-        active_files = cursor.fetchone()[0]
-        
-        # Total de descargas
-        cursor.execute('SELECT SUM(download_count) FROM files WHERE is_deleted = FALSE')
-        total_downloads = cursor.fetchone()[0] or 0
-        
-        return {
-            'total_files': total_files,
-            'active_files': active_files,
-            'total_downloads': total_downloads
-        } 

+ 0 - 45
env.example

@@ -1,45 +0,0 @@
-# Configuración de la aplicación Flask
-# Copia este archivo como .env y modifica los valores según tu entorno
-
-# Clave secreta para Flask (cambia esto por una clave segura)
-SECRET_KEY=tu_clave_secreta_muy_segura_aqui
-
-# Configuración de archivos
-MAX_CONTENT_LENGTH=16777216
-UPLOAD_FOLDER=uploads
-
-# Configuración del servidor
-HOST=127.0.0.1
-PORT=5000
-DEBUG=False
-
-# Configuración del dominio (para reverse proxy)
-DOMAIN=temp.mysite.net
-
-# Configuración de seguridad
-# Tiempo de expiración en horas (por defecto 24 horas)
-FILE_EXPIRATION_HOURS=24
-
-# Configuración de HTTPS (opcional)
-# SSL_CERT_FILE=cert.pem
-# SSL_KEY_FILE=key.pem
-
-# Configuración de base de datos (para futuras implementaciones)
-# DATABASE_URL=sqlite:///hokoritemp.db
-
-# Configuración de logging
-LOG_LEVEL=INFO
-LOG_FILE=logs/app.log
-
-# Configuración de límites
-# Tamaño máximo de archivo en bytes (16MB por defecto)
-MAX_FILE_SIZE=16777216
-
-# Tipos de archivo permitidos (dejar vacío para permitir todos)
-# ALLOWED_EXTENSIONS=.txt,.pdf,.doc,.docx,.jpg,.png,.gif,.zip,.rar
-
-# Configuración de limpieza automática
-# Habilitar limpieza automática de archivos expirados
-AUTO_CLEANUP=True
-# Frecuencia de limpieza en horas
-CLEANUP_INTERVAL_HOURS=1 

BIN
requirements.txt


+ 0 - 60
setup-proxy.sh

@@ -1,60 +0,0 @@
-#!/bin/bash
-
-# Script para configurar HokoriTemp con Apache reverse proxy
-# Ejecutar como root o con sudo
-
-echo "Configurando HokoriTemp con Apache reverse proxy..."
-
-# 1. Verificar que Apache esté instalado
-if ! command -v httpd &> /dev/null; then
-    echo "Error: Apache no está instalado"
-    echo "Instala Apache con: sudo yum install httpd (CentOS/RHEL)"
-    echo "o: sudo apt install apache2 (Ubuntu/Debian)"
-    exit 1
-fi
-
-# 2. Habilitar módulos necesarios
-echo "Habilitando módulos de Apache..."
-a2enmod proxy
-a2enmod proxy_http
-a2enmod headers
-
-# 3. Copiar configuración
-echo "Copiando configuración de Apache..."
-cp temp.mysite.conf /etc/httpd/conf.d/
-# Para Ubuntu/Debian usar: cp temp.mysite.conf /etc/apache2/sites-available/
-
-# 4. Habilitar sitio (Ubuntu/Debian)
-# a2ensite temp.mysite.conf
-
-# 5. Crear archivo .env
-echo "Creando archivo .env..."
-cp env.example .env
-
-# 6. Generar clave secreta
-echo "Generando clave secreta..."
-SECRET_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))")
-sed -i "s/tu_clave_secreta_muy_segura_aqui/$SECRET_KEY/" .env
-
-# 7. Configurar dominio
-read -p "Ingresa tu dominio (ej: temp.mysite.net): " DOMAIN
-sed -i "s/temp.mysite.net/$DOMAIN/" .env
-
-# 8. Reiniciar Apache
-echo "Reiniciando Apache..."
-systemctl restart httpd
-# Para Ubuntu/Debian: systemctl restart apache2
-
-# 9. Verificar configuración
-echo "Verificando configuración de Apache..."
-apache2ctl configtest
-
-echo ""
-echo "✅ Configuración completada!"
-echo ""
-echo "Para ejecutar la aplicación:"
-echo "1. Activa el entorno virtual: source venv/bin/activate"
-echo "2. Ejecuta la app: python app.py --host 127.0.0.1 --port 5000"
-echo "3. Accede a: http://$DOMAIN"
-echo ""
-echo "Para ejecutar como servicio, crea un archivo systemd o usa supervisor" 

+ 0 - 421
static/app.js

@@ -1,421 +0,0 @@
-// HokoriTemp - JavaScript principal
-
-let selectedFile = null;
-// Usar configuración del servidor si está disponible, sino usar valor por defecto
-const MAX_FILE_SIZE = (typeof SERVER_CONFIG !== 'undefined' && SERVER_CONFIG.maxFileSize) 
-    ? SERVER_CONFIG.maxFileSize 
-    : 16 * 1024 * 1024; // 16MB por defecto
-
-// DOM Elements
-const dropZone = document.getElementById('dropZone');
-const fileInput = document.getElementById('fileInput');
-const fileInfo = document.getElementById('fileInfo');
-const fileName = document.getElementById('fileName');
-const fileSize = document.getElementById('fileSize');
-const removeFile = document.getElementById('removeFile');
-const uploadForm = document.getElementById('uploadForm');
-const uploadBtn = document.getElementById('uploadBtn');
-const uploadBtnText = document.getElementById('uploadBtnText');
-const uploadBtnLoading = document.getElementById('uploadBtnLoading');
-const resultSection = document.getElementById('resultSection');
-const emptyResultSection = document.getElementById('emptyResultSection');
-const errorSection = document.getElementById('errorSection');
-const errorMessage = document.getElementById('errorMessage');
-const downloadLink = document.getElementById('downloadLink');
-
-// Progress bar elements
-const progressContainer = document.getElementById('progressContainer');
-const progressBar = document.getElementById('progressBar');
-const progressText = document.getElementById('progressText');
-const uploadedSize = document.getElementById('uploadedSize');
-const totalSize = document.getElementById('totalSize');
-
-// Event Listeners
-dropZone.addEventListener('click', () => fileInput.click());
-dropZone.addEventListener('dragover', handleDragOver);
-dropZone.addEventListener('dragleave', handleDragLeave);
-dropZone.addEventListener('drop', handleDrop);
-fileInput.addEventListener('change', handleFileSelect);
-removeFile.addEventListener('click', removeSelectedFile);
-uploadForm.addEventListener('submit', handleUpload);
-
-// Inicializar estado
-document.addEventListener('DOMContentLoaded', function() {
-    // Mostrar el estado vacío del resultado al cargar
-    emptyResultSection.classList.remove('hidden');
-    emptyResultSection.classList.add('fade-in');
-});
-
-function handleDragOver(e) {
-    e.preventDefault();
-    dropZone.classList.add('dragover');
-}
-
-function handleDragLeave(e) {
-    e.preventDefault();
-    dropZone.classList.remove('dragover');
-}
-
-function handleDrop(e) {
-    e.preventDefault();
-    dropZone.classList.remove('dragover');
-    const files = e.dataTransfer.files;
-    if (files.length > 0) {
-        handleFile(files[0]);
-    }
-}
-
-function handleFileSelect(e) {
-    const file = e.target.files[0];
-    if (file) {
-        handleFile(file);
-    }
-}
-
-function handleFile(file) {
-    // Validar tamaño del archivo
-    if (file.size > MAX_FILE_SIZE) {
-        showError(`El archivo es demasiado grande. Tamaño máximo: ${getMaxFileSizeMB()}MB`);
-        return;
-    }
-
-    selectedFile = file;
-    fileName.textContent = file.name;
-    fileName.title = file.name; // Agregar título completo para tooltip
-    fileSize.textContent = formatFileSize(file.size);
-    
-    // Aplicar clase de advertencia si el archivo es grande (>10MB)
-    if (file.size > 10 * 1024 * 1024) {
-        fileInfo.classList.add('file-size-warning');
-    } else {
-        fileInfo.classList.remove('file-size-warning');
-    }
-    
-    fileInfo.classList.remove('hidden');
-    uploadBtn.disabled = false;
-    hideError();
-}
-
-function removeSelectedFile() {
-    selectedFile = null;
-    fileInput.value = '';
-    fileInfo.classList.add('hidden');
-    fileInfo.classList.remove('file-size-warning', 'file-size-error');
-    uploadBtn.disabled = true;
-    // Ocultar resultado si hay uno mostrado
-    hideResult();
-    // Ocultar barra de progreso
-    hideProgress();
-}
-
-function formatFileSize(bytes) {
-    if (bytes === 0) return '0 Bytes';
-    const k = 1024;
-    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
-    const i = Math.floor(Math.log(bytes) / Math.log(k));
-    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
-}
-
-async function handleUpload(e) {
-    e.preventDefault();
-    
-    if (!selectedFile) {
-        showError('Por favor selecciona un archivo');
-        return;
-    }
-
-    // Validar tamaño del archivo antes de subir
-    if (selectedFile.size > MAX_FILE_SIZE) {
-        showError(`El archivo es demasiado grande. Tamaño máximo: ${getMaxFileSizeMB()}MB`);
-        return;
-    }
-
-    // Show loading state
-    uploadBtn.disabled = true;
-    uploadBtnText.classList.add('hidden');
-    uploadBtnLoading.classList.remove('hidden');
-    hideError();
-    hideResult();
-    showProgress();
-    totalSize.textContent = formatFileSize(selectedFile.size);
-
-    const formData = new FormData();
-    formData.append('file', selectedFile);
-    
-    // Agregar tiempo de vida del archivo
-    const expiresHours = document.getElementById('expiresHours').value;
-    formData.append('expires_hours', expiresHours);
-
-    return new Promise((resolve, reject) => {
-        const xhr = new XMLHttpRequest();
-        
-        // Configurar eventos de progreso
-        xhr.upload.addEventListener('progress', function(e) {
-            if (e.lengthComputable) {
-                updateProgress(e.loaded, e.total);
-            }
-        });
-        
-        // Configurar eventos de respuesta
-        xhr.addEventListener('load', function() {
-            hideProgress();
-            
-            try {
-                // Verificar si la respuesta es JSON válido
-                const contentType = xhr.getResponseHeader('content-type');
-                const isJson = contentType && contentType.includes('application/json');
-                
-                // Manejar diferentes códigos de respuesta
-                if (xhr.status === 413) {
-                    showError(`El archivo es demasiado grande. Tamaño máximo: ${getMaxFileSizeMB()}MB`);
-                    resolve();
-                    return;
-                }
-                
-                if (xhr.status !== 200) {
-                    let errorMessage = `Error del servidor (${xhr.status})`;
-                    
-                    if (isJson) {
-                        try {
-                            const errorData = JSON.parse(xhr.responseText);
-                            errorMessage = errorData.error || errorMessage;
-                        } catch (e) {
-                            console.error('Error parsing JSON response:', e);
-                        }
-                    } else {
-                        // Si no es JSON, intentar leer como texto
-                        const errorText = xhr.responseText;
-                        console.error('Error response:', errorText);
-                        
-                        // Detectar errores específicos basados en el contenido
-                        if (errorText.includes('Request Entity Too Large') || errorText.includes('413')) {
-                            errorMessage = `El archivo es demasiado grande. Tamaño máximo: ${getMaxFileSizeMB()}MB`;
-                        } else if (errorText.includes('413')) {
-                            errorMessage = `El archivo es demasiado grande. Tamaño máximo: ${getMaxFileSizeMB()}MB`;
-                        } else {
-                            errorMessage = `Error del servidor: ${xhr.statusText}`;
-                        }
-                    }
-                    
-                    showError(errorMessage);
-                    resolve();
-                    return;
-                }
-                
-                if (isJson) {
-                    const result = JSON.parse(xhr.responseText);
-                    
-                    if (result.success) {
-                        showResult(result.download_url);
-                        showNotification('Archivo subido exitosamente', 'success');
-                    } else {
-                        showError(result.error || 'Error al subir el archivo');
-                    }
-                } else {
-                    showError('Respuesta inesperada del servidor');
-                }
-            } catch (error) {
-                console.error('Upload error:', error);
-                showError('Error al procesar la respuesta del servidor.');
-            }
-            
-            resolve();
-        });
-        
-        // Configurar eventos de error
-        xhr.addEventListener('error', function() {
-            hideProgress();
-            console.error('Upload error: Network error');
-            showError('Error de conexión. Verifica tu conexión a internet e intenta de nuevo.');
-            resolve();
-        });
-        
-        xhr.addEventListener('abort', function() {
-            hideProgress();
-            console.error('Upload error: Request aborted');
-            showError('Subida cancelada.');
-            resolve();
-        });
-        
-        // Configurar timeout
-        xhr.timeout = 300000; // 5 minutos
-        xhr.addEventListener('timeout', function() {
-            hideProgress();
-            console.error('Upload error: Request timeout');
-            showError('Tiempo de espera agotado. Intenta de nuevo.');
-            resolve();
-        });
-        
-        // Enviar la petición
-        xhr.open('POST', '/upload');
-        xhr.send(formData);
-    }).finally(() => {
-        // Reset loading state
-        uploadBtn.disabled = false;
-        uploadBtnText.classList.remove('hidden');
-        uploadBtnLoading.classList.add('hidden');
-    });
-}
-
-// Función eliminada - ya no se usa localStorage, los enlaces se guardan en la base de datos
-
-function showResult(url) {
-    downloadLink.value = url;
-    
-    // Actualizar el tiempo de vida mostrado
-    const expiresHours = document.getElementById('expiresHours').value;
-    const expiresTimeText = getExpiresTimeText(expiresHours);
-    document.getElementById('selectedExpiresTime').textContent = expiresTimeText;
-    
-    resultSection.classList.remove('hidden');
-    resultSection.classList.add('fade-in');
-    emptyResultSection.classList.add('hidden');
-    
-    // Ocultar el formulario de subida
-    const uploadFormContainer = document.querySelector('.bg-white.rounded-lg.shadow-lg.p-6');
-    if (uploadFormContainer) {
-        uploadFormContainer.classList.add('hidden');
-    }
-}
-
-function getExpiresTimeText(hours) {
-    const hoursInt = parseInt(hours);
-    if (hoursInt === 1) return '1 hora';
-    if (hoursInt === 6) return '6 horas';
-    if (hoursInt === 12) return '12 horas';
-    if (hoursInt === 24) return '24 horas';
-    if (hoursInt === 48) return '2 días';
-    if (hoursInt === 72) return '3 días';
-    if (hoursInt === 168) return '7 días';
-    return `${hoursInt} horas`;
-}
-
-function showError(message) {
-    errorMessage.textContent = message;
-    errorSection.classList.remove('hidden');
-    showNotification(message, 'error');
-}
-
-function hideError() {
-    errorSection.classList.add('hidden');
-}
-
-function hideResult() {
-    resultSection.classList.add('hidden');
-    resultSection.classList.remove('fade-in');
-    emptyResultSection.classList.remove('hidden');
-    emptyResultSection.classList.add('fade-in');
-    
-    // Mostrar el formulario de subida nuevamente
-    const uploadFormContainer = document.querySelector('.bg-white.rounded-lg.shadow-lg.p-6');
-    if (uploadFormContainer) {
-        uploadFormContainer.classList.remove('hidden');
-    }
-}
-
-function copyLink() {
-    downloadLink.select();
-    document.execCommand('copy');
-    // Show feedback
-    const copyBtn = event.target;
-    const originalText = copyBtn.textContent;
-    copyBtn.textContent = '¡Copiado!';
-    setTimeout(() => {
-        copyBtn.textContent = originalText;
-    }, 2000);
-    showNotification('Enlace copiado al portapapeles', 'success');
-}
-
-function openLink() {
-    window.open(downloadLink.value, '_blank');
-}
-
-function resetForm() {
-    removeSelectedFile();
-    hideResult();
-    hideError();
-    hideProgress();
-    
-    // Mostrar el formulario de subida nuevamente
-    const uploadFormContainer = document.querySelector('.bg-white.rounded-lg.shadow-lg.p-6');
-    if (uploadFormContainer) {
-        uploadFormContainer.classList.remove('hidden');
-    }
-    
-    // Mostrar el estado vacío del resultado
-    emptyResultSection.classList.remove('hidden');
-}
-
-// Función para mostrar notificaciones
-function showNotification(message, type = 'info') {
-    // Crear elemento de notificación
-    const notification = document.createElement('div');
-    notification.className = `notification ${type}`;
-    notification.textContent = message;
-    
-    // Agregar al DOM
-    document.body.appendChild(notification);
-    
-    // Mostrar con animación
-    setTimeout(() => {
-        notification.classList.add('show');
-    }, 100);
-    
-    // Ocultar después de 3 segundos
-    setTimeout(() => {
-        notification.classList.remove('show');
-        setTimeout(() => {
-            document.body.removeChild(notification);
-        }, 300);
-    }, 3000);
-}
-
-// Función helper para obtener el tamaño máximo en MB
-function getMaxFileSizeMB() {
-    return (typeof SERVER_CONFIG !== 'undefined' && SERVER_CONFIG.maxFileSizeMB) 
-        ? SERVER_CONFIG.maxFileSizeMB 
-        : MAX_FILE_SIZE / (1024 * 1024);
-}
-
-// Función para validar archivo antes de subir
-function validateFile(file) {
-    const maxSize = MAX_FILE_SIZE;
-    const maxSizeMB = getMaxFileSizeMB();
-    
-    if (file.size > maxSize) {
-        return {
-            valid: false,
-            message: `El archivo es demasiado grande. Tamaño máximo: ${maxSizeMB}MB`
-        };
-    }
-    
-    return { valid: true };
-}
-
-// Progress bar functions
-function showProgress() {
-    progressContainer.classList.remove('hidden');
-    progressBar.style.width = '0%';
-    progressText.textContent = '0%';
-    uploadedSize.textContent = '0 KB';
-    totalSize.textContent = '0 KB';
-}
-
-function hideProgress() {
-    progressContainer.classList.add('hidden');
-}
-
-function updateProgress(uploaded, total) {
-    const percentage = Math.round((uploaded / total) * 100);
-    progressBar.style.width = percentage + '%';
-    progressText.textContent = percentage + '%';
-    uploadedSize.textContent = formatFileSize(uploaded);
-    totalSize.textContent = formatFileSize(total);
-}
-
-function resetProgress() {
-    progressBar.style.width = '0%';
-    progressText.textContent = '0%';
-    uploadedSize.textContent = '0 KB';
-    totalSize.textContent = '0 KB';
-} 

+ 79 - 0
static/css/styles.css

@@ -0,0 +1,79 @@
+@keyframes fadeIn {
+    from { opacity: 0; }
+    to { opacity: 1; }
+}
+
+@keyframes slideUp {
+    from { transform: translateY(10px); opacity: 0; }
+    to { transform: translateY(0); opacity: 1; }
+}
+
+/* Toast personalizado */
+#toast {
+    top: 2rem;
+    right: 2rem;
+    min-width: 220px;
+    max-width: 320px;
+    background: #22c55e; /* verde por defecto */
+    color: #fff;
+    box-shadow: 0 4px 24px 0 rgba(0,0,0,0.18);
+    border-radius: 0.75rem;
+    z-index: 9999;
+    transition: transform 0.3s, background 0.2s;
+    animation: fadeIn 0.3s;
+    position: fixed;
+    display: flex;
+    align-items: center;
+    gap: 0.75rem;
+    padding: 0.75rem 1.5rem;
+}
+#toast.bg-green-500 { background: #22c55e; }
+#toast.bg-red-500 { background: #ef4444; }
+#toast.bg-blue-500 { background: #3b82f6; }
+#toast.translate-x-full { transform: translateX(120%); }
+
+/* Tooltip personalizado */
+.custom-tooltip {
+    position: absolute;
+    z-index: 50;
+    background: #222;
+    color: #fff;
+    padding: 0.25rem 0.75rem;
+    border-radius: 0.375rem;
+    font-size: 0.875rem;
+    white-space: pre-line;
+    box-shadow: 0 2px 8px rgba(0,0,0,0.18);
+    opacity: 0;
+    pointer-events: none;
+    transition: opacity 0.15s;
+}
+.custom-tooltip.visible {
+    opacity: 1;
+}
+
+/* Mejoras para dark mode */
+.dark .text-gray-900 { color: #f3f4f6 !important; }
+.dark .text-gray-700 { color: #d1d5db !important; }
+.dark .text-gray-600 { color: #9ca3af !important; }
+.dark .text-gray-500 { color: #6b7280 !important; }
+.dark .bg-white { background-color: #1f2937 !important; }
+.dark .border-gray-200 { border-color: #374151 !important; }
+.dark .border-gray-300 { border-color: #4b5563 !important; }
+.dark .bg-gray-50 { background-color: #111827 !important; }
+.dark .bg-blue-100 { background-color: #1e40af !important; }
+.dark .bg-red-100 { background-color: #7f1d1d !important; }
+.dark .bg-blue-900\/20 { background-color: rgba(30,64,175,0.2) !important; }
+.dark .bg-blue-900\/30 { background-color: rgba(30,64,175,0.3) !important; }
+.dark .bg-blue-900\/50 { background-color: rgba(30,64,175,0.5) !important; }
+.dark .bg-red-900\/30 { background-color: rgba(127,29,29,0.3) !important; }
+.dark .bg-red-900\/50 { background-color: rgba(127,29,29,0.5) !important; }
+
+/* Truncado de texto */
+.truncate {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    max-width: 180px;
+    display: inline-block;
+    vertical-align: bottom;
+} 

+ 339 - 0
static/js/main.js

@@ -0,0 +1,339 @@
+// Gestión de tema (light/dark)
+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');
+    }
+}
+
+// Gestión de sesión de usuario
+function getUserSession() {
+    let session = localStorage.getItem('userSession');
+    if (!session) {
+        session = 'user_' + Math.random().toString(36).substr(2, 9) + '_' + Date.now();
+        localStorage.setItem('userSession', session);
+    }
+    return session;
+}
+
+// Notificación tipo toast
+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);
+}
+
+// Tooltip personalizado
+let tooltipTimeout;
+function showCustomTooltip(target, text) {
+    let tooltip = document.getElementById('custom-tooltip');
+    if (!tooltip) {
+        tooltip = document.createElement('div');
+        tooltip.id = 'custom-tooltip';
+        tooltip.className = 'custom-tooltip';
+        document.body.appendChild(tooltip);
+    }
+    tooltip.textContent = text;
+    const rect = target.getBoundingClientRect();
+    tooltip.style.top = (window.scrollY + rect.bottom + 4) + 'px';
+    tooltip.style.left = (window.scrollX + rect.left) + 'px';
+    tooltip.classList.add('visible');
+    clearTimeout(tooltipTimeout);
+}
+function hideCustomTooltip() {
+    const tooltip = document.getElementById('custom-tooltip');
+    if (tooltip) {
+        tooltip.classList.remove('visible');
+        tooltipTimeout = setTimeout(() => {
+            tooltip.remove();
+        }, 200);
+    }
+}
+
+// Formateo de tamaño de archivo
+function formatFileSize(bytes) {
+    if (bytes === 0) return '0 Bytes';
+    const k = 1024;
+    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
+    const i = Math.floor(Math.log(bytes) / Math.log(k));
+    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
+}
+
+// Formateo de fecha
+function formatDate(dateString) {
+    const date = new Date(dateString);
+    return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
+}
+
+// Tiempo restante
+function getTimeRemaining(expirationDate) {
+    const now = new Date();
+    const expiry = new Date(expirationDate);
+    const diff = expiry - now;
+    if (diff <= 0) return 'Expirado';
+    const hours = Math.floor(diff / (1000 * 60 * 60));
+    const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
+    if (hours > 0) {
+        return `${hours}h ${minutes}m restantes`;
+    } else {
+        return `${minutes}m restantes`;
+    }
+}
+
+// Progreso de subida
+let uploadStartTime = 0;
+let uploadedBytes = 0;
+function updateProgress(loaded, total) {
+    const percent = Math.round((loaded / total) * 100);
+    const progressBar = document.getElementById('progressBar');
+    const progressPercent = document.getElementById('progressPercent');
+    const uploadSpeed = document.getElementById('uploadSpeed');
+    const timeRemaining = document.getElementById('timeRemaining');
+    progressBar.style.width = percent + '%';
+    progressPercent.textContent = percent + '%';
+    const elapsed = (Date.now() - uploadStartTime) / 1000;
+    const speed = loaded / elapsed;
+    const speedMB = (speed / (1024 * 1024)).toFixed(2);
+    uploadSpeed.textContent = `${speedMB} MB/s`;
+    if (speed > 0) {
+        const remaining = (total - loaded) / speed;
+        const minutes = Math.floor(remaining / 60);
+        const seconds = Math.floor(remaining % 60);
+        timeRemaining.textContent = `${minutes}:${seconds.toString().padStart(2, '0')} restantes`;
+    }
+}
+
+// Manejo de selección de archivo
+function handleFileSelection(file) {
+    const fileInfo = document.getElementById('file-info');
+    const fileName = document.getElementById('file-name');
+    const fileSize = document.getElementById('file-size');
+    if (file) {
+        fileName.textContent = file.name;
+        fileName.classList.add('truncate');
+        fileName.setAttribute('data-tooltip', file.name);
+        fileSize.textContent = formatFileSize(file.size);
+        fileInfo.classList.remove('hidden');
+    } else {
+        fileInfo.classList.add('hidden');
+    }
+}
+
+function clearFileSelection() {
+    const fileInput = document.getElementById('file-upload');
+    const fileInfo = document.getElementById('file-info');
+    fileInput.value = '';
+    fileInfo.classList.add('hidden');
+}
+
+// Cargar archivos del usuario
+async function loadUserFiles() {
+    const userSession = getUserSession();
+    try {
+        const response = await fetch(`/files/${userSession}`);
+        const files = await response.json();
+        const filesList = document.getElementById('filesList');
+        const noFiles = document.getElementById('noFiles');
+        if (files.length === 0) {
+            filesList.innerHTML = '';
+            noFiles.classList.remove('hidden');
+        } else {
+            noFiles.classList.add('hidden');
+            filesList.innerHTML = files.map(file => `
+                <div class="border border-gray-200 dark:border-gray-600 rounded-lg p-4 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors animate-slide-up">
+                    <div class="flex items-center justify-between">
+                        <div class="flex-1">
+                            <div class="flex items-center space-x-3">
+                                <div class="flex-shrink-0">
+                                    <svg class="w-8 h-8 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>
+                                </div>
+                                <div class="flex-1 min-w-0">
+                                    <p class="text-sm font-medium text-gray-900 dark:text-white truncate file-tooltip" data-tooltip="${file.original_filename}">${file.original_filename}</p>
+                                    <p class="text-xs text-gray-500 dark:text-gray-400">${formatFileSize(file.file_size)} • Subido ${formatDate(file.upload_date)}</p>
+                                </div>
+                            </div>
+                        </div>
+                        <div class="flex items-center space-x-2">
+                            <div class="text-right">
+                                <p class="text-xs text-gray-500 dark:text-gray-400">${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">
+                                Descargar
+                            </a>
+                            <button onclick="deleteFile('${file.id}')" class="inline-flex items-center px-3 py-1 text-xs font-medium text-red-600 dark:text-red-400 bg-red-100 dark:bg-red-900/30 hover:bg-red-200 dark:hover:bg-red-900/50 rounded-full transition-colors">
+                                Eliminar
+                            </button>
+                        </div>
+                    </div>
+                </div>
+            `).join('');
+        }
+        // Tooltips personalizados para nombres truncados
+        document.querySelectorAll('.file-tooltip').forEach(el => {
+            el.addEventListener('mouseenter', function(e) {
+                showCustomTooltip(e.target, e.target.getAttribute('data-tooltip'));
+            });
+            el.addEventListener('mouseleave', hideCustomTooltip);
+        });
+    } catch (error) {
+        showToast('Error al cargar archivos', 'error');
+    }
+}
+
+// Variables para el modal de eliminación
+let fileIdToDelete = null;
+
+function openDeleteModal(fileId) {
+    fileIdToDelete = fileId;
+    document.getElementById('deleteModal').classList.remove('hidden');
+}
+
+function closeDeleteModal() {
+    fileIdToDelete = null;
+    document.getElementById('deleteModal').classList.add('hidden');
+}
+
+// Eliminar archivo (ahora solo ejecuta si se confirma en el modal)
+async function confirmDeleteFile() {
+    if (!fileIdToDelete) return;
+    const userSession = getUserSession();
+    try {
+        const response = await fetch(`/delete/${fileIdToDelete}`, {
+            method: 'DELETE',
+            headers: { 'Content-Type': 'application/json' },
+            body: JSON.stringify({ user_session: userSession })
+        });
+        const result = await response.json();
+        if (result.success) {
+            showToast('Archivo eliminado correctamente');
+            loadUserFiles();
+        } else {
+            showToast(result.error || 'Error al eliminar', 'error');
+        }
+    } catch (error) {
+        showToast('Error al eliminar: ' + error.message, 'error');
+    } finally {
+        closeDeleteModal();
+    }
+}
+
+// Reemplazar deleteFile para abrir el modal
+window.deleteFile = openDeleteModal;
+
+// Eventos del modal
+function setupDeleteModalEvents() {
+    document.getElementById('cancelDeleteBtn').addEventListener('click', closeDeleteModal);
+    document.getElementById('confirmDeleteBtn').addEventListener('click', confirmDeleteFile);
+    // Cerrar modal con Escape
+    document.addEventListener('keydown', function(e) {
+        if (e.key === 'Escape') closeDeleteModal();
+    });
+}
+
+// Inicialización de eventos
+function setupEventListeners() {
+    // Tema
+    document.getElementById('themeToggle').addEventListener('click', toggleTheme);
+    // Selección de archivo
+    document.getElementById('file-upload').addEventListener('change', function(e) {
+        const file = e.target.files[0];
+        handleFileSelection(file);
+    });
+    // Limpiar selección
+    document.getElementById('clear-file').addEventListener('click', clearFileSelection);
+    // Subida de archivo
+    document.getElementById('uploadForm').addEventListener('submit', async function(e) {
+        e.preventDefault();
+        const fileInput = document.getElementById('file-upload');
+        const file = fileInput.files[0];
+        const duration = document.getElementById('duration').value;
+        const uploadBtn = document.getElementById('uploadBtn');
+        const uploadBtnText = document.getElementById('uploadBtnText');
+        const uploadProgress = document.getElementById('uploadProgress');
+        if (!file) {
+            showToast('Por favor selecciona un archivo', 'error');
+            return;
+        }
+        uploadBtn.disabled = true;
+        uploadBtnText.textContent = 'Subiendo...';
+        uploadProgress.classList.remove('hidden');
+        uploadStartTime = Date.now();
+        uploadedBytes = 0;
+        const formData = new FormData();
+        formData.append('file', file);
+        formData.append('user_session', getUserSession());
+        formData.append('duration_hours', duration);
+        try {
+            const xhr = new XMLHttpRequest();
+            xhr.upload.addEventListener('progress', function(e) {
+                if (e.lengthComputable) {
+                    updateProgress(e.loaded, e.total);
+                }
+            });
+            xhr.addEventListener('load', function() {
+                if (xhr.status === 200) {
+                    const result = JSON.parse(xhr.responseText);
+                    if (result.success) {
+                        showToast('Archivo subido correctamente!');
+                        clearFileSelection();
+                        loadUserFiles();
+                    } else {
+                        showToast(result.error || 'Error al subir', 'error');
+                    }
+                } else {
+                    showToast('Error al subir', 'error');
+                }
+            });
+            xhr.addEventListener('error', function() {
+                showToast('Error de red al subir', 'error');
+            });
+            xhr.open('POST', '/upload');
+            xhr.send(formData);
+        } catch (error) {
+            showToast('Error al subir: ' + error.message, 'error');
+        } finally {
+            setTimeout(() => {
+                uploadBtn.disabled = false;
+                uploadBtnText.textContent = 'Subir archivo';
+                uploadProgress.classList.add('hidden');
+            }, 1000);
+        }
+    });
+    // Refrescar lista
+    document.getElementById('refreshBtn').addEventListener('click', loadUserFiles);
+}
+
+document.addEventListener('DOMContentLoaded', function() {
+    initTheme();
+    setupEventListeners();
+    setupDeleteModalEvents();
+    loadUserFiles();
+    setInterval(loadUserFiles, 30000);
+}); 

+ 0 - 137
static/styles.css

@@ -1,137 +0,0 @@
-/* Estilos para HokoriTemp */
-
-.drop-zone {
-    transition: all 0.3s ease;
-}
-
-.drop-zone.dragover {
-    border-color: #3b82f6;
-    background-color: #eff6ff;
-}
-
-.result-section {
-    transition: all 0.3s ease;
-}
-
-.fade-in {
-    animation: fadeIn 0.3s ease-in;
-}
-
-@keyframes fadeIn {
-    from { 
-        opacity: 0; 
-        transform: translateY(10px); 
-    }
-    to { 
-        opacity: 1; 
-        transform: translateY(0); 
-    }
-}
-
-/* Estilos para archivos grandes */
-.file-size-warning {
-    background-color: #fef3c7;
-    border-color: #f59e0b;
-    color: #92400e;
-}
-
-.file-size-error {
-    background-color: #fee2e2;
-    border-color: #ef4444;
-    color: #991b1b;
-}
-
-/* Estilos para loading */
-.loading-spinner {
-    animation: spin 1s linear infinite;
-}
-
-@keyframes spin {
-    from { transform: rotate(0deg); }
-    to { transform: rotate(360deg); }
-}
-
-/* Estilos para notificaciones */
-.notification {
-    position: fixed;
-    top: 20px;
-    right: 20px;
-    z-index: 1000;
-    padding: 1rem;
-    border-radius: 0.5rem;
-    box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
-    transform: translateX(100%);
-    transition: transform 0.3s ease;
-}
-
-/* Estilos para truncate mejorado */
-.truncate {
-    overflow: hidden;
-    text-overflow: ellipsis;
-    white-space: nowrap;
-}
-
-/* Mejorar el tooltip para nombres largos */
-[title] {
-    cursor: help;
-}
-
-.notification.show {
-    transform: translateX(0);
-}
-
-.notification.success {
-    background-color: #d1fae5;
-    border: 1px solid #10b981;
-    color: #065f46;
-}
-
-.notification.error {
-    background-color: #fee2e2;
-    border: 1px solid #ef4444;
-    color: #991b1b;
-}
-
-.notification.warning {
-    background-color: #fef3c7;
-    border: 1px solid #f59e0b;
-    color: #92400e;
-}
-
-/* Estilos para la barra de progreso */
-#progressContainer {
-    transition: all 0.3s ease;
-}
-
-#progressBar {
-    transition: width 0.3s ease-out;
-    background: linear-gradient(90deg, #3b82f6 0%, #1d4ed8 100%);
-    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
-}
-
-#progressBar::after {
-    content: '';
-    position: absolute;
-    top: 0;
-    left: 0;
-    right: 0;
-    bottom: 0;
-    background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.3) 50%, transparent 100%);
-    animation: shimmer 2s infinite;
-}
-
-@keyframes shimmer {
-    0% { transform: translateX(-100%); }
-    100% { transform: translateX(100%); }
-}
-
-/* Animación para mostrar/ocultar la barra de progreso */
-#progressContainer.hidden {
-    opacity: 0;
-    transform: translateY(-10px);
-}
-
-#progressContainer:not(.hidden) {
-    opacity: 1;
-    transform: translateY(0);
-} 

+ 0 - 268
static/view.js

@@ -1,268 +0,0 @@
-// HokoriTemp - JavaScript para la página de vista
-
-// DOM Elements
-const linksContainer = document.getElementById('linksContainer');
-const emptyState = document.getElementById('emptyState');
-const loadingState = document.getElementById('loadingState');
-const confirmModal = document.getElementById('confirmModal');
-const confirmDeleteBtn = document.getElementById('confirmDeleteBtn');
-const cancelDeleteBtn = document.getElementById('cancelDeleteBtn');
-
-// Variable para almacenar el ID del archivo a eliminar
-let fileToDelete = null;
-
-// Load links on page load
-document.addEventListener('DOMContentLoaded', loadLinks);
-
-// Event listeners para el modal
-confirmDeleteBtn.addEventListener('click', confirmDelete);
-cancelDeleteBtn.addEventListener('click', hideModal);
-
-// Cerrar modal al hacer clic fuera de él
-confirmModal.addEventListener('click', (e) => {
-    if (e.target === confirmModal) {
-        hideModal();
-    }
-});
-
-// Cerrar modal con Escape
-document.addEventListener('keydown', (e) => {
-    if (e.key === 'Escape' && !confirmModal.classList.contains('hidden')) {
-        hideModal();
-    }
-});
-
-async function loadLinks() {
-    try {
-        const response = await fetch('/api/links');
-        if (!response.ok) {
-            throw new Error(`HTTP error! status: ${response.status}`);
-        }
-        
-        const links = await response.json();
-        
-        // Hide loading
-        loadingState.classList.add('hidden');
-
-        if (links.length === 0) {
-            showEmptyState();
-        } else {
-            showLinks(links);
-        }
-    } catch (error) {
-        console.error('Error cargando enlaces:', error);
-        loadingState.classList.add('hidden');
-        showError('Error al cargar los enlaces');
-    }
-}
-
-function showEmptyState() {
-    emptyState.classList.remove('hidden');
-}
-
-function showLinks(links) {
-    const linksHTML = links.map(link => createLinkCard(link)).join('');
-    linksContainer.innerHTML = linksHTML;
-}
-
-function createLinkCard(link) {
-    const createdAt = new Date(link.created_at);
-    const expiresAt = new Date(link.expires_at);
-    const timeLeft = getTimeLeft(expiresAt);
-    const isExpired = expiresAt <= new Date();
-    const fileSize = formatFileSize(link.file_size);
-
-    return `
-        <div class="bg-white rounded-lg shadow-lg p-6 mb-4 ${isExpired ? 'opacity-50' : ''}">
-            <div class="flex items-start justify-between">
-                <div class="flex-1">
-                    <div class="flex items-center space-x-3">
-                        <svg class="h-8 w-8 text-blue-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>
-                        <div>
-                            <h3 class="text-lg font-medium text-gray-900">${link.filename}</h3>
-                            <p class="text-sm text-gray-500">Creado: ${formatDate(createdAt)} • ${fileSize} • ${link.download_count} descarga${link.download_count !== 1 ? 's' : ''}</p>
-                        </div>
-                    </div>
-                    
-                    <div class="mt-4">
-                        <div class="flex items-center space-x-2">
-                            <span class="text-sm text-gray-500">Enlace:</span>
-                            <input type="text" value="${link.url}" class="flex-1 px-3 py-1 text-sm border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500" readonly>
-                            <button onclick="copyLink('${link.url}')" class="px-3 py-1 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500">
-                                Copiar
-                            </button>
-                        </div>
-                    </div>
-
-                    <div class="mt-3 flex items-center justify-between">
-                        <div class="flex items-center space-x-4">
-                            <span class="text-sm text-gray-500">
-                                ${isExpired ? 'Expirado' : `Expira en: ${timeLeft}`}
-                            </span>
-                            ${isExpired ? '' : `
-                                <button onclick="openLink('${link.url}')" class="text-sm text-blue-600 hover:text-blue-800 underline">
-                                    Abrir enlace
-                                </button>
-                            `}
-                        </div>
-                        <button onclick="showDeleteModal('${link.id}')" class="text-sm text-red-600 hover:text-red-800 underline">
-                            Eliminar
-                        </button>
-                    </div>
-                </div>
-            </div>
-        </div>
-    `;
-}
-
-function getTimeLeft(expiresAt) {
-    const now = new Date();
-    const diff = expiresAt - now;
-    
-    if (diff <= 0) return 'Expirado';
-    
-    const hours = Math.floor(diff / (1000 * 60 * 60));
-    const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
-    
-    if (hours > 0) {
-        return `${hours}h ${minutes}m`;
-    } else {
-        return `${minutes}m`;
-    }
-}
-
-function formatDate(date) {
-    return date.toLocaleString('es-ES', {
-        year: 'numeric',
-        month: 'short',
-        day: 'numeric',
-        hour: '2-digit',
-        minute: '2-digit'
-    });
-}
-
-function formatFileSize(bytes) {
-    if (bytes === 0) return '0 Bytes';
-    const k = 1024;
-    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
-    const i = Math.floor(Math.log(bytes) / Math.log(k));
-    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
-}
-
-function copyLink(url) {
-    navigator.clipboard.writeText(url).then(() => {
-        // Show feedback
-        const button = event.target;
-        const originalText = button.textContent;
-        button.textContent = '¡Copiado!';
-        setTimeout(() => {
-            button.textContent = originalText;
-        }, 2000);
-        
-        // Show notification
-        showNotification('Enlace copiado al portapapeles', 'success');
-    }).catch(() => {
-        // Fallback for older browsers
-        const textArea = document.createElement('textarea');
-        textArea.value = url;
-        document.body.appendChild(textArea);
-        textArea.select();
-        document.execCommand('copy');
-        document.body.removeChild(textArea);
-        
-        const button = event.target;
-        const originalText = button.textContent;
-        button.textContent = '¡Copiado!';
-        setTimeout(() => {
-            button.textContent = originalText;
-        }, 2000);
-        
-        showNotification('Enlace copiado al portapapeles', 'success');
-    });
-}
-
-function openLink(url) {
-    window.open(url, '_blank');
-}
-
-function showDeleteModal(linkId) {
-    fileToDelete = linkId;
-    confirmModal.classList.remove('hidden');
-}
-
-function hideModal() {
-    confirmModal.classList.add('hidden');
-    fileToDelete = null;
-}
-
-async function confirmDelete() {
-    if (!fileToDelete) return;
-    
-    try {
-        const response = await fetch(`/api/delete/${fileToDelete}`, {
-            method: 'DELETE'
-        });
-        
-        if (!response.ok) {
-            throw new Error(`HTTP error! status: ${response.status}`);
-        }
-        
-        const result = await response.json();
-        
-        if (result.success) {
-            showNotification('Archivo eliminado permanentemente', 'success');
-            hideModal();
-            // Recargar la página para actualizar la lista
-            location.reload();
-        } else {
-            throw new Error(result.error || 'Error al eliminar el archivo');
-        }
-    } catch (error) {
-        console.error('Error eliminando archivo:', error);
-        showNotification('Error al eliminar el archivo', 'error');
-        hideModal();
-    }
-}
-
-// Función para mostrar notificaciones
-function showNotification(message, type = 'info') {
-    // Crear elemento de notificación
-    const notification = document.createElement('div');
-    notification.className = `notification ${type}`;
-    notification.textContent = message;
-    
-    // Agregar al DOM
-    document.body.appendChild(notification);
-    
-    // Mostrar con animación
-    setTimeout(() => {
-        notification.classList.add('show');
-    }, 100);
-    
-    // Ocultar después de 3 segundos
-    setTimeout(() => {
-        notification.classList.remove('show');
-        setTimeout(() => {
-            document.body.removeChild(notification);
-        }, 300);
-    }, 3000);
-}
-
-// Función para mostrar errores
-function showError(message) {
-    showNotification(message, 'error');
-}
-
-// Auto-refresh every minute to update expiration times
-setInterval(() => {
-    const links = document.querySelectorAll('[data-expires-at]');
-    links.forEach(linkElement => {
-        const expiresAt = new Date(linkElement.dataset.expiresAt);
-        const timeLeftElement = linkElement.querySelector('.time-left');
-        if (timeLeftElement) {
-            timeLeftElement.textContent = getTimeLeft(expiresAt);
-        }
-    });
-}, 60000); 

+ 0 - 48
temp.mysite.conf

@@ -1,48 +0,0 @@
-# Configuración de Apache para temp.mysite.net
-# Guardar en /etc/httpd/conf.d/temp.mysite.conf
-
-<VirtualHost *:80>
-    ServerName temp.mysite.net
-    ServerAlias www.temp.mysite.net
-    
-    # Logs
-    ErrorLog logs/temp.mysite.net-error.log
-    CustomLog logs/temp.mysite.net-access.log combined
-    
-    # Reverse Proxy simple
-    ProxyPreserveHost On
-    ProxyPass / http://127.0.0.1:5000/
-    ProxyPassReverse / http://127.0.0.1:5000/
-    
-    # Timeout para archivos grandes
-    ProxyTimeout 300
-    
-    # Configuración para manejar errores 413
-    ProxyErrorOverride Off
-</VirtualHost>
-
-# Configuración HTTPS (opcional)
-<VirtualHost *:443>
-    ServerName temp.mysite.net
-    ServerAlias www.temp.mysite.net
-    
-    # SSL
-    SSLEngine on
-    SSLCertificateFile /path/to/your/cert.crt
-    SSLCertificateKeyFile /path/to/your/key.key
-    
-    # Logs
-    ErrorLog logs/temp.mysite.net-ssl-error.log
-    CustomLog logs/temp.mysite.net-ssl-access.log combined
-    
-    # Reverse Proxy
-    ProxyPreserveHost On
-    ProxyPass / http://127.0.0.1:5000/
-    ProxyPassReverse / http://127.0.0.1:5000/
-    
-    # Timeout para archivos grandes
-    ProxyTimeout 300
-    
-    # Configuración para manejar errores 413
-    ProxyErrorOverride Off
-</VirtualHost> 

+ 158 - 152
templates/index.html

@@ -1,175 +1,181 @@
 <!DOCTYPE html>
-<html lang="es">
+<html lang="en">
 <head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>hokoritemp</title>
-    <link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='favicon.svg') }}">
-    <script src="https://cdn.tailwindcss.com"></script>
-    <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
-</head>
-<body class="bg-gray-50 min-h-screen flex items-center justify-center">
-    <div class="container mx-auto px-4 py-8 w-full max-w-4xl">
-
-        <!-- Main Container -->
-        <div class="max-w-4xl mx-auto">
-            <div class="space-y-8">
-                
-                <!-- Upload Form -->
-                <div class="bg-white rounded-lg shadow-lg p-6">
-                    <h2 class="text-xl font-semibold text-gray-800 mb-4">Subir Archivo</h2>
-                    <form id="uploadForm" enctype="multipart/form-data">
-                        <div class="mb-6">
-                            <label class="block text-sm font-medium text-gray-700 mb-2">
-                                Selecciona un archivo para subir
-                            </label>
-                            <div id="dropZone" class="drop-zone border-2 border-dashed border-gray-300 rounded-lg p-8 text-center cursor-pointer hover:border-gray-400 transition-colors">
-                                <div class="space-y-4">
-                                    <svg class="mx-auto h-12 w-12 text-gray-400" stroke="currentColor" fill="none" viewBox="0 0 48 48">
-                                        <path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
-                                    </svg>
-                                    <div>
-                                        <p class="text-lg font-medium text-gray-700">Arrastra y suelta tu archivo aquí</p>
-                                        <p class="text-sm text-gray-500">o haz clic para seleccionar</p>
-                                    </div>
-                                </div>
-                                <input type="file" id="fileInput" class="hidden" accept="*/*">
-                            </div>
-                        </div>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>HokoriTemp</title>
 
-                        <!-- Tiempo de vida del archivo -->
-                        <div class="mb-6">
-                            <label class="block text-sm font-medium text-gray-700 mb-2">
-                                Tiempo de vida del archivo
-                            </label>
-                            <select id="expiresHours" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
-                                <option value="1">1 hora</option>
-                                <option value="6">6 horas</option>
-                                <option value="12">12 horas</option>
-                                <option value="24" selected>24 horas (por defecto)</option>
-                                <option value="48">2 días</option>
-                                <option value="72">3 días</option>
-                                <option value="168">7 días</option>
-                            </select>
-                            <p class="text-sm text-gray-500 mt-1">El archivo se eliminará automáticamente después de este tiempo</p>
-                        </div>
-
-                        <div id="fileInfo" class="hidden mb-6 p-4 bg-blue-50 rounded-lg">
-                            <div class="flex items-center justify-between">
-                                <div class="flex-1 min-w-0 mr-3">
-                                    <p class="font-medium text-blue-900 truncate" id="fileName" title=""></p>
-                                    <p class="text-sm text-blue-700" id="fileSize"></p>
-                                </div>
-                                <button type="button" id="removeFile" class="text-red-600 hover:text-red-800 flex-shrink-0">
-                                    <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
-                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
-                                    </svg>
-                                </button>
-                            </div>
-                        </div>
+<!-- SVG Favicon -->
+<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
 
-                        <!-- Progress Bar -->
-                        <div id="progressContainer" class="hidden mb-4">
-                            <div class="flex items-center justify-between mb-2">
-                                <span class="text-sm font-medium text-gray-700">Progreso de subida</span>
-                                <span id="progressText" class="text-sm text-gray-500">0%</span>
-                            </div>
-                            <div class="w-full bg-gray-200 rounded-full h-2">
-                                <div id="progressBar" class="bg-blue-600 h-2 rounded-full transition-all duration-300 ease-out" style="width: 0%"></div>
-                            </div>
-                            <div class="flex items-center justify-between mt-1">
-                                <span id="uploadedSize" class="text-xs text-gray-500">0 KB</span>
-                                <span id="totalSize" class="text-xs text-gray-500">0 KB</span>
-                            </div>
-                        </div>
-
-                        <button type="submit" id="uploadBtn" class="w-full bg-blue-600 text-white py-3 px-6 rounded-lg font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed">
-                            <span id="uploadBtnText">Subir Archivo</span>
-                            <span id="uploadBtnLoading" class="hidden">
-                                <svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white inline" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
-                                    <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
-                                    <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
-                                </svg>
-                                Subiendo...
-                            </span>
-                        </button>
-                    </form>
+<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 with Theme Toggle -->
+    <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">Sube archivos temporalmente con expiración automática</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>
 
-                    <!-- Error Section -->
-                    <div id="errorSection" class="hidden mt-6 bg-red-50 border border-red-200 rounded-lg p-4">
-                        <div class="flex">
-                            <div class="flex-shrink-0">
-                                <svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
-                                    <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
+    <!-- Upload Section -->
+    <div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-8 transition-colors duration-300">
+        <h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Upload New File</h2>
+        
+        <!-- File Upload Form -->
+        <form id="uploadForm" class="space-y-4">
+            <div>
+                <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
+                    Select File
+                </label>
+                
+                <!-- Compact File Input -->
+                <div class="space-y-3">
+                    <!-- File Selection Area -->
+                    <div class="flex items-center justify-center w-full">
+                        <label for="file-upload" class="flex flex-col items-center justify-center w-full h-32 border-2 border-gray-300 dark:border-gray-600 border-dashed rounded-lg cursor-pointer bg-gray-50 dark:bg-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors">
+                            <div class="flex flex-col items-center justify-center pt-3 pb-3">
+                                <svg class="w-8 h-8 mb-2 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="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path>
                                 </svg>
+                                <p class="text-sm text-gray-500 dark:text-gray-400">
+                                    <span class="font-semibold">Click to upload</span> or drag and drop
+                                </p>
+                                <p class="text-xs text-gray-500 dark:text-gray-400">Max {{ max_file_size }}MB</p>
                             </div>
-                            <div class="ml-3">
-                                <h3 class="text-sm font-medium text-red-800" id="errorMessage"></h3>
-                            </div>
-                        </div>
+                            <input id="file-upload" type="file" class="hidden" accept="*/*" />
+                        </label>
                     </div>
-                </div>
-
-                <!-- Result Section -->
-                <div id="resultSection" class="hidden bg-white rounded-lg shadow-lg p-6 result-section">
-                    <div class="text-center">
-                        <div class="mb-4">
-                            <svg class="mx-auto h-12 w-12 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
-                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
-                            </svg>
-                        </div>
-                        <h3 class="text-lg font-medium text-gray-900 mb-2">¡Archivo subido exitosamente!</h3>
-                        <p class="text-gray-600 mb-4">Tu archivo estará disponible por <span id="selectedExpiresTime">24 horas</span></p>
-                        
-                        <div class="bg-gray-50 rounded-lg p-4 mb-4">
-                            <label class="block text-sm font-medium text-gray-700 mb-2">Enlace de descarga:</label>
-                            <div class="flex">
-                                <input type="text" id="downloadLink" class="flex-1 px-3 py-2 border border-gray-300 rounded-l-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" readonly>
-                                <button onclick="copyLink()" class="px-4 py-2 bg-blue-600 text-white rounded-r-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500">
-                                    Copiar
-                                </button>
+                    
+                    <!-- Selected File Info -->
+                    <div id="file-info" class="hidden bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-3">
+                        <div class="flex items-center justify-between">
+                            <div class="flex items-center space-x-3">
+                                <svg class="w-5 h-5 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>
+                                    <p id="file-name" class="text-sm font-medium text-blue-900 dark:text-blue-100"></p>
+                                    <p id="file-size" class="text-xs text-blue-700 dark:text-blue-300"></p>
+                                </div>
                             </div>
-                        </div>
-
-                        <div class="flex space-x-4">
-                            <button onclick="openLink()" class="flex-1 bg-green-600 text-white py-2 px-4 rounded-lg hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500">
-                                Abrir Enlace
-                            </button>
-                            <button onclick="resetForm()" class="flex-1 bg-gray-600 text-white py-2 px-4 rounded-lg hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-500">
-                                Subir Otro
+                            <button type="button" id="clear-file" class="text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300 text-sm font-medium">
+                                Remove
                             </button>
                         </div>
                     </div>
                 </div>
+            </div>
 
-                <!-- Empty State for Result Section -->
-                <div id="emptyResultSection" class="hidden bg-gray-50 rounded-lg p-8 text-center result-section">
-                    <svg class="mx-auto h-12 w-12 text-gray-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>
-                    <h3 class="mt-2 text-sm font-medium text-gray-900">Listo para subir</h3>
-                    <p class="mt-1 text-sm text-gray-500">Selecciona un archivo para comenzar</p>
-                </div>
+            <div>
+                <label for="duration" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
+                    File Duration
+                </label>
+                <select id="duration" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white">
+                    <option value="1">1 Hour</option>
+                    <option value="6">6 Hours</option>
+                    <option value="24" selected>24 Hours</option>
+                    <option value="72">3 Days</option>
+                    <option value="168">1 Week</option>
+                </select>
+            </div>
+
+            <button type="submit" id="uploadBtn" class="w-full bg-blue-600 hover:bg-blue-700 dark:bg-blue-700 dark:hover:bg-blue-600 text-white font-medium py-2 px-4 rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 disabled:opacity-50 disabled:cursor-not-allowed">
+                <span id="uploadBtnText">Upload File</span>
+            </button>
+        </form>
+
+        <!-- Upload Progress -->
+        <div id="uploadProgress" class="mt-4 hidden">
+            <div class="flex items-center justify-between mb-2">
+                <span class="text-sm text-gray-600 dark:text-gray-400">Uploading...</span>
+                <span id="progressPercent" class="text-sm text-gray-600 dark:text-gray-400">0%</span>
+            </div>
+            <div class="bg-gray-200 dark:bg-gray-600 rounded-full h-2">
+                <div id="progressBar" class="bg-blue-600 dark:bg-blue-500 h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
+            </div>
+            <div class="flex items-center justify-between mt-1">
+                <span id="uploadSpeed" class="text-xs text-gray-500 dark:text-gray-400"></span>
+                <span id="timeRemaining" class="text-xs text-gray-500 dark:text-gray-400"></span>
             </div>
         </div>
     </div>
 
-    <!-- Floating Action Button -->
-    <div class="fixed bottom-6 right-6 z-50">
-        <a href="/myfiles" class="bg-blue-600 text-white w-14 h-14 rounded-full flex items-center justify-center hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 shadow-lg transition-all duration-200 hover:scale-110" title="Ver mis enlaces activos">
-            <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+    <!-- Files List Section -->
+    <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">
+            <h2 class="text-xl font-semibold text-gray-900 dark:text-white">Your Files</h2>
+            <button id="refreshBtn" class="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 text-sm font-medium">
+                Refresh
+            </button>
+        </div>
+        
+        <div id="filesList" class="space-y-3">
+            <!-- Files will be loaded here -->
+        </div>
+        
+        <div id="noFiles" class="text-center py-8 text-gray-500 dark:text-gray-400">
+            <svg class="w-12 h-12 mx-auto mb-3 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>
-        </a>
+            <p>No files uploaded yet</p>
+        </div>
     </div>
+</div>
 
-    <script>
-        // Configuración del servidor
-        const SERVER_CONFIG = {
-            maxFileSize: parseInt('{{ max_file_size }}'),
-            maxFileSizeMB: parseInt('{{ max_file_size_mb }}')
-        };
-    </script>
-    <script src="{{ url_for('static', filename='app.js') }}"></script>
+<!-- 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>
+<!-- Tooltip personalizado -->
+<!-- El tooltip se genera dinámicamente por JS -->
+<script src="/static/js/main.js"></script>
+
+<!-- Modal de confirmación de eliminación -->
+<div id="deleteModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-40 hidden">
+  <div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6 w-full max-w-sm animate-fade-in">
+    <h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">¿Eliminar archivo?</h3>
+    <p class="text-gray-600 dark:text-gray-300 mb-6">¿Seguro que deseas eliminar este archivo? Esta acción no se puede deshacer.</p>
+    <div class="flex justify-end gap-3">
+      <button id="cancelDeleteBtn" class="px-4 py-2 rounded-md bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors">Cancelar</button>
+      <button id="confirmDeleteBtn" class="px-4 py-2 rounded-md bg-red-600 text-white hover:bg-red-700 transition-colors">Eliminar</button>
+    </div>
+  </div>
+</div>
 </body>
-</html> 
+</html>

+ 0 - 100
templates/myfiles.html

@@ -1,100 +0,0 @@
-<!DOCTYPE html>
-<html lang="es">
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>hokoritemp</title>
-    <link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='favicon.svg') }}">
-    <script src="https://cdn.tailwindcss.com"></script>
-    <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
-</head>
-<body class="bg-gray-50 min-h-screen flex items-center justify-center">
-    <div class="container mx-auto px-4 py-8 w-full max-w-4xl">
-
-        <!-- Main Container -->
-        <div class="max-w-4xl mx-auto">
-            <div class="space-y-8">
-                
-                <!-- Links Container -->
-                <div class="bg-white rounded-lg shadow-lg p-6">
-                    <h2 class="text-xl font-semibold text-gray-800 mb-4">Mis Enlaces Temporales</h2>
-                    
-                    <div id="linksContainer" class="space-y-4">
-                        <!-- Links will be loaded here -->
-                    </div>
-
-                    <!-- Empty State -->
-                    <div id="emptyState" class="hidden text-center py-12">
-                        <svg class="mx-auto h-12 w-12 text-gray-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>
-                        <h3 class="mt-2 text-sm font-medium text-gray-900">No hay enlaces guardados</h3>
-                        <p class="mt-1 text-sm text-gray-500">Los enlaces de los archivos que subas aparecerán aquí.</p>
-                        <div class="mt-6">
-                            <a href="/" class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
-                                Subir mi primer archivo
-                            </a>
-                        </div>
-                    </div>
-
-                    <!-- Loading State -->
-                    <div id="loadingState" class="text-center py-12">
-                        <svg class="animate-spin mx-auto h-8 w-8 text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
-                            <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
-                            <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
-                        </svg>
-                        <p class="mt-2 text-sm text-gray-500">Cargando enlaces...</p>
-                    </div>
-                </div>
-
-                <!-- Empty State for Result Section -->
-                <div id="emptyResultSection" class="hidden bg-gray-50 rounded-lg p-8 text-center">
-                    <svg class="mx-auto h-12 w-12 text-gray-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>
-                    <h3 class="mt-2 text-sm font-medium text-gray-900">Listo para ver enlaces</h3>
-                    <p class="mt-1 text-sm text-gray-500">Tus enlaces aparecerán aquí</p>
-                </div>
-            </div>
-        </div>
-    </div>
-
-    <!-- Modal de Confirmación -->
-    <div id="confirmModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full hidden z-50">
-        <div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
-            <div class="mt-3 text-center">
-                <div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100">
-                    <svg class="h-6 w-6 text-red-600" 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>
-                </div>
-                <h3 class="text-lg font-medium text-gray-900 mt-4">Confirmar eliminación</h3>
-                <div class="mt-2 px-7 py-3">
-                    <p class="text-sm text-gray-500">
-                        ¿Estás seguro de que quieres eliminar este archivo permanentemente? Esta acción no se puede deshacer y el archivo se borrará completamente del servidor.
-                    </p>
-                </div>
-                <div class="items-center px-4 py-3">
-                    <button id="confirmDeleteBtn" class="px-4 py-2 bg-red-600 text-white text-base font-medium rounded-md w-24 mr-2 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500">
-                        Eliminar
-                    </button>
-                    <button id="cancelDeleteBtn" class="px-4 py-2 bg-gray-500 text-white text-base font-medium rounded-md w-24 hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-500">
-                        Cancelar
-                    </button>
-                </div>
-            </div>
-        </div>
-    </div>
-
-    <!-- Floating Action Button -->
-    <div class="fixed bottom-6 right-6 z-50">
-        <a href="/" class="bg-blue-600 text-white w-14 h-14 rounded-full flex items-center justify-center hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 shadow-lg transition-all duration-200 hover:scale-110" title="Volver a subir archivos">
-            <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
-                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"></path>
-            </svg>
-        </a>
-    </div>
-
-    <script src="{{ url_for('static', filename='view.js') }}"></script>
-</body>
-</html>