浏览代码

Merge branch 'implementar-base-datos' of nekomataokayu/hokoritemp into master

Geovanny Andrés Vega Mite 5 月之前
父节点
当前提交
d5b52e7195
共有 10 个文件被更改,包括 1088 次插入193 次删除
  1. 221 0
      DATABASE.md
  2. 106 18
      app.py
  3. 152 0
      cleanup.py
  4. 150 0
      database.py
  5. 176 77
      static/app.js
  6. 38 0
      static/styles.css
  7. 96 27
      static/view.js
  8. 49 16
      templates/index.html
  9. 100 0
      templates/myfiles.html
  10. 0 55
      templates/view.html

+ 221 - 0
DATABASE.md

@@ -0,0 +1,221 @@
+# 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
+``` 

+ 106 - 18
app.py

@@ -7,6 +7,7 @@ from flask import Flask, render_template, request, jsonify, send_file, redirect,
 from werkzeug.utils import secure_filename
 from werkzeug.utils import secure_filename
 from dotenv import load_dotenv
 from dotenv import load_dotenv
 import json
 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
 
 
 # Cargar variables de entorno desde .env
 # Cargar variables de entorno desde .env
 load_dotenv()
 load_dotenv()
@@ -38,6 +39,12 @@ def ensure_upload_folder():
         os.makedirs(app.config['UPLOAD_FOLDER'])
         os.makedirs(app.config['UPLOAD_FOLDER'])
         print(f"Carpeta '{app.config['UPLOAD_FOLDER']}' creada automáticamente.")
         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
 # Configurar argumentos de línea de comandos
 def parse_arguments():
 def parse_arguments():
     parser = argparse.ArgumentParser(description='Servidor de archivos temporales')
     parser = argparse.ArgumentParser(description='Servidor de archivos temporales')
@@ -58,10 +65,10 @@ def index():
                          max_file_size=max_file_size,
                          max_file_size=max_file_size,
                          max_file_size_mb=max_file_size_mb)
                          max_file_size_mb=max_file_size_mb)
 
 
-@app.route('/view')
-def view():
+@app.route('/myfiles')
+def myfiles():
     """Página para ver enlaces guardados en localStorage"""
     """Página para ver enlaces guardados en localStorage"""
-    return render_template('view.html')
+    return render_template('myfiles.html')
 
 
 @app.route('/upload', methods=['POST'])
 @app.route('/upload', methods=['POST'])
 def upload_file():
 def upload_file():
@@ -75,6 +82,13 @@ def upload_file():
         if file.filename == '':
         if file.filename == '':
             return jsonify({'error': 'No se seleccionó ningún archivo'}), 400
             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)
+        
+        # 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
+        
         # Validar tamaño del archivo antes de procesar
         # Validar tamaño del archivo antes de procesar
         max_size = app.config['MAX_CONTENT_LENGTH']
         max_size = app.config['MAX_CONTENT_LENGTH']
         max_size_mb = max_size // (1024 * 1024)
         max_size_mb = max_size // (1024 * 1024)
@@ -98,7 +112,11 @@ def upload_file():
                 }), 413
                 }), 413
             
             
             # Generar nombre único para el archivo
             # Generar nombre único para el archivo
-            filename = secure_filename(file.filename)
+            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:
             if not filename:
                 return jsonify({'error': 'Nombre de archivo no válido'}), 400
                 return jsonify({'error': 'Nombre de archivo no válido'}), 400
                 
                 
@@ -110,7 +128,10 @@ def upload_file():
             file_path = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename)
             file_path = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename)
             file.save(file_path)
             file.save(file_path)
             
             
-            # Crear enlace temporal (expira en 24 horas)
+            # 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
             # Usar el dominio configurado o el host actual
             domain = os.getenv('DOMAIN', request.host)
             domain = os.getenv('DOMAIN', request.host)
             protocol = 'https' if args.https else 'http'
             protocol = 'https' if args.https else 'http'
@@ -120,8 +141,8 @@ def upload_file():
                 'success': True,
                 'success': True,
                 'download_url': download_url,
                 'download_url': download_url,
                 'file_id': file_id,
                 'file_id': file_id,
-                'original_filename': filename,
-                'expires_at': (datetime.now() + timedelta(hours=24)).isoformat()
+                'original_filename': original_filename,
+                'expires_at': (datetime.now() + timedelta(hours=expires_hours)).isoformat()
             })
             })
         else:
         else:
             return jsonify({'error': 'No se pudo procesar el archivo'}), 400
             return jsonify({'error': 'No se pudo procesar el archivo'}), 400
@@ -133,25 +154,92 @@ def upload_file():
 @app.route('/download/<file_id>')
 @app.route('/download/<file_id>')
 def download_file(file_id):
 def download_file(file_id):
     """Descargar archivo por ID"""
     """Descargar archivo por ID"""
-    # Buscar archivo por ID
-    for filename in os.listdir(app.config['UPLOAD_FOLDER']):
-        if filename.startswith(file_id):
-            file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
-            return send_file(file_path, as_attachment=True)
+    # 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
     
     
-    return "Archivo no encontrado", 404
+    # 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')
 @app.route('/api/links')
 def get_links():
 def get_links():
-    """API para obtener enlaces guardados (simulado)"""
-    # En una implementación real, esto vendría de una base de datos
-    return jsonify([])
+    """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
+
+@app.route('/api/stats')
+def get_stats():
+    """API para obtener estadísticas"""
+    try:
+        stats = get_file_stats()
+        return jsonify(stats)
+    except Exception as e:
+        print(f"Error obteniendo estadísticas: {str(e)}")
+        return jsonify({'error': 'Error interno del servidor'}), 500
 
 
 if __name__ == '__main__':
 if __name__ == '__main__':
     args = parse_arguments()
     args = parse_arguments()
     
     
-    # Asegurar que existe la carpeta de uploads
-    ensure_upload_folder()
+    # Inicializar aplicación (carpeta de uploads y base de datos)
+    init_app()
     
     
     # Configurar protocolo
     # Configurar protocolo
     protocol = 'https' if args.https else 'http'
     protocol = 'https' if args.https else 'http'

+ 152 - 0
cleanup.py

@@ -0,0 +1,152 @@
+#!/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() 

+ 150 - 0
database.py

@@ -0,0 +1,150 @@
+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
+        } 

+ 176 - 77
static/app.js

@@ -23,6 +23,13 @@ const errorSection = document.getElementById('errorSection');
 const errorMessage = document.getElementById('errorMessage');
 const errorMessage = document.getElementById('errorMessage');
 const downloadLink = document.getElementById('downloadLink');
 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
 // Event Listeners
 dropZone.addEventListener('click', () => fileInput.click());
 dropZone.addEventListener('click', () => fileInput.click());
 dropZone.addEventListener('dragover', handleDragOver);
 dropZone.addEventListener('dragover', handleDragOver);
@@ -97,6 +104,8 @@ function removeSelectedFile() {
     uploadBtn.disabled = true;
     uploadBtn.disabled = true;
     // Ocultar resultado si hay uno mostrado
     // Ocultar resultado si hay uno mostrado
     hideResult();
     hideResult();
+    // Ocultar barra de progreso
+    hideProgress();
 }
 }
 
 
 function formatFileSize(bytes) {
 function formatFileSize(bytes) {
@@ -127,110 +136,158 @@ async function handleUpload(e) {
     uploadBtnLoading.classList.remove('hidden');
     uploadBtnLoading.classList.remove('hidden');
     hideError();
     hideError();
     hideResult();
     hideResult();
+    showProgress();
+    totalSize.textContent = formatFileSize(selectedFile.size);
 
 
     const formData = new FormData();
     const formData = new FormData();
     formData.append('file', selectedFile);
     formData.append('file', selectedFile);
+    
+    // Agregar tiempo de vida del archivo
+    const expiresHours = document.getElementById('expiresHours').value;
+    formData.append('expires_hours', expiresHours);
 
 
-    try {
-        const response = await fetch('/upload', {
-            method: 'POST',
-            body: formData
+    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);
+            }
         });
         });
-
-        // Verificar si la respuesta es JSON válido
-        const contentType = response.headers.get('content-type');
-        const isJson = contentType && contentType.includes('application/json');
-
-        // Manejar diferentes códigos de respuesta
-        if (response.status === 413) {
-            showError(`El archivo es demasiado grande. Tamaño máximo: ${getMaxFileSizeMB()}MB`);
-            return;
-        }
-
-        if (!response.ok) {
-            let errorMessage = `Error del servidor (${response.status})`;
+        
+        // Configurar eventos de respuesta
+        xhr.addEventListener('load', function() {
+            hideProgress();
             
             
-            if (isJson) {
-                try {
-                    const errorData = await response.json();
-                    errorMessage = errorData.error || errorMessage;
-                } catch (e) {
-                    console.error('Error parsing JSON response:', e);
+            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;
                 }
                 }
-            } else {
-                // Si no es JSON, intentar leer como texto
-                try {
-                    const errorText = await response.text();
-                    console.error('Error response:', errorText);
+                
+                if (xhr.status !== 200) {
+                    let errorMessage = `Error del servidor (${xhr.status})`;
                     
                     
-                    // 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`;
+                    if (isJson) {
+                        try {
+                            const errorData = JSON.parse(xhr.responseText);
+                            errorMessage = errorData.error || errorMessage;
+                        } catch (e) {
+                            console.error('Error parsing JSON response:', e);
+                        }
                     } else {
                     } else {
-                        errorMessage = `Error del servidor: ${response.statusText}`;
+                        // 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}`;
+                        }
                     }
                     }
-                } catch (e) {
-                    console.error('Error reading response text:', e);
+                    
+                    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.');
             }
             }
             
             
-            showError(errorMessage);
-            return;
-        }
-
-        if (isJson) {
-            const result = await response.json();
-
-            if (result.success) {
-                // Save to localStorage
-                saveLinkToStorage(result);
-                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);
+            resolve();
+        });
         
         
-        // Determinar el tipo de error
-        if (error.name === 'TypeError' && error.message.includes('Failed to fetch')) {
+        // 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.');
             showError('Error de conexión. Verifica tu conexión a internet e intenta de nuevo.');
-        } else if (error.name === 'TypeError' && error.message.includes('JSON')) {
-            showError('Error al procesar la respuesta del servidor.');
-        } else {
-            showError('Error inesperado. Intenta de nuevo.');
-        }
-    } finally {
+            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
         // Reset loading state
         uploadBtn.disabled = false;
         uploadBtn.disabled = false;
         uploadBtnText.classList.remove('hidden');
         uploadBtnText.classList.remove('hidden');
         uploadBtnLoading.classList.add('hidden');
         uploadBtnLoading.classList.add('hidden');
-    }
+    });
 }
 }
 
 
-function saveLinkToStorage(result) {
-    const links = JSON.parse(localStorage.getItem('tempFileLinks') || '[]');
-    const newLink = {
-        id: result.file_id,
-        url: result.download_url,
-        filename: result.original_filename,
-        expiresAt: result.expires_at,
-        createdAt: new Date().toISOString()
-    };
-    links.push(newLink);
-    localStorage.setItem('tempFileLinks', JSON.stringify(links));
-}
+// Función eliminada - ya no se usa localStorage, los enlaces se guardan en la base de datos
 
 
 function showResult(url) {
 function showResult(url) {
     downloadLink.value = 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.remove('hidden');
     resultSection.classList.add('fade-in');
     resultSection.classList.add('fade-in');
     emptyResultSection.classList.add('hidden');
     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) {
 function showError(message) {
@@ -248,6 +305,12 @@ function hideResult() {
     resultSection.classList.remove('fade-in');
     resultSection.classList.remove('fade-in');
     emptyResultSection.classList.remove('hidden');
     emptyResultSection.classList.remove('hidden');
     emptyResultSection.classList.add('fade-in');
     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() {
 function copyLink() {
@@ -271,6 +334,14 @@ function resetForm() {
     removeSelectedFile();
     removeSelectedFile();
     hideResult();
     hideResult();
     hideError();
     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
     // Mostrar el estado vacío del resultado
     emptyResultSection.classList.remove('hidden');
     emptyResultSection.classList.remove('hidden');
 }
 }
@@ -319,4 +390,32 @@ function validateFile(file) {
     }
     }
     
     
     return { valid: true };
     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';
 } 
 } 

+ 38 - 0
static/styles.css

@@ -96,4 +96,42 @@
     background-color: #fef3c7;
     background-color: #fef3c7;
     border: 1px solid #f59e0b;
     border: 1px solid #f59e0b;
     color: #92400e;
     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);
 } 
 } 

+ 96 - 27
static/view.js

@@ -4,30 +4,55 @@
 const linksContainer = document.getElementById('linksContainer');
 const linksContainer = document.getElementById('linksContainer');
 const emptyState = document.getElementById('emptyState');
 const emptyState = document.getElementById('emptyState');
 const loadingState = document.getElementById('loadingState');
 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
 // Load links on page load
 document.addEventListener('DOMContentLoaded', loadLinks);
 document.addEventListener('DOMContentLoaded', loadLinks);
 
 
-function loadLinks() {
-    const links = JSON.parse(localStorage.getItem('tempFileLinks') || '[]');
-    
-    // Filter expired links
-    const currentTime = new Date();
-    const validLinks = links.filter(link => {
-        const expiresAt = new Date(link.expiresAt);
-        return expiresAt > currentTime;
-    });
+// Event listeners para el modal
+confirmDeleteBtn.addEventListener('click', confirmDelete);
+cancelDeleteBtn.addEventListener('click', hideModal);
 
 
-    // Update localStorage with only valid links
-    localStorage.setItem('tempFileLinks', JSON.stringify(validLinks));
+// Cerrar modal al hacer clic fuera de él
+confirmModal.addEventListener('click', (e) => {
+    if (e.target === confirmModal) {
+        hideModal();
+    }
+});
 
 
-    // Hide loading
-    loadingState.classList.add('hidden');
+// Cerrar modal con Escape
+document.addEventListener('keydown', (e) => {
+    if (e.key === 'Escape' && !confirmModal.classList.contains('hidden')) {
+        hideModal();
+    }
+});
 
 
-    if (validLinks.length === 0) {
-        showEmptyState();
-    } else {
-        showLinks(validLinks);
+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');
     }
     }
 }
 }
 
 
@@ -41,10 +66,11 @@ function showLinks(links) {
 }
 }
 
 
 function createLinkCard(link) {
 function createLinkCard(link) {
-    const createdAt = new Date(link.createdAt);
-    const expiresAt = new Date(link.expiresAt);
+    const createdAt = new Date(link.created_at);
+    const expiresAt = new Date(link.expires_at);
     const timeLeft = getTimeLeft(expiresAt);
     const timeLeft = getTimeLeft(expiresAt);
     const isExpired = expiresAt <= new Date();
     const isExpired = expiresAt <= new Date();
+    const fileSize = formatFileSize(link.file_size);
 
 
     return `
     return `
         <div class="bg-white rounded-lg shadow-lg p-6 mb-4 ${isExpired ? 'opacity-50' : ''}">
         <div class="bg-white rounded-lg shadow-lg p-6 mb-4 ${isExpired ? 'opacity-50' : ''}">
@@ -56,7 +82,7 @@ function createLinkCard(link) {
                         </svg>
                         </svg>
                         <div>
                         <div>
                             <h3 class="text-lg font-medium text-gray-900">${link.filename}</h3>
                             <h3 class="text-lg font-medium text-gray-900">${link.filename}</h3>
-                            <p class="text-sm text-gray-500">Creado: ${formatDate(createdAt)}</p>
+                            <p class="text-sm text-gray-500">Creado: ${formatDate(createdAt)} • ${fileSize} • ${link.download_count} descarga${link.download_count !== 1 ? 's' : ''}</p>
                         </div>
                         </div>
                     </div>
                     </div>
                     
                     
@@ -81,7 +107,7 @@ function createLinkCard(link) {
                                 </button>
                                 </button>
                             `}
                             `}
                         </div>
                         </div>
-                        <button onclick="removeLink('${link.id}')" class="text-sm text-red-600 hover:text-red-800 underline">
+                        <button onclick="showDeleteModal('${link.id}')" class="text-sm text-red-600 hover:text-red-800 underline">
                             Eliminar
                             Eliminar
                         </button>
                         </button>
                     </div>
                     </div>
@@ -117,6 +143,14 @@ function formatDate(date) {
     });
     });
 }
 }
 
 
+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) {
 function copyLink(url) {
     navigator.clipboard.writeText(url).then(() => {
     navigator.clipboard.writeText(url).then(() => {
         // Show feedback
         // Show feedback
@@ -153,13 +187,43 @@ function openLink(url) {
     window.open(url, '_blank');
     window.open(url, '_blank');
 }
 }
 
 
-function removeLink(linkId) {
-    const links = JSON.parse(localStorage.getItem('tempFileLinks') || '[]');
-    const updatedLinks = links.filter(link => link.id !== linkId);
-    localStorage.setItem('tempFileLinks', JSON.stringify(updatedLinks));
+function showDeleteModal(linkId) {
+    fileToDelete = linkId;
+    confirmModal.classList.remove('hidden');
+}
+
+function hideModal() {
+    confirmModal.classList.add('hidden');
+    fileToDelete = null;
+}
+
+async function confirmDelete() {
+    if (!fileToDelete) return;
     
     
-    // Reload the page to update the display
-    location.reload();
+    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
 // Función para mostrar notificaciones
@@ -186,6 +250,11 @@ function showNotification(message, type = 'info') {
     }, 3000);
     }, 3000);
 }
 }
 
 
+// Función para mostrar errores
+function showError(message) {
+    showNotification(message, 'error');
+}
+
 // Auto-refresh every minute to update expiration times
 // Auto-refresh every minute to update expiration times
 setInterval(() => {
 setInterval(() => {
     const links = document.querySelectorAll('[data-expires-at]');
     const links = document.querySelectorAll('[data-expires-at]');

+ 49 - 16
templates/index.html

@@ -3,25 +3,17 @@
 <head>
 <head>
     <meta charset="UTF-8">
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>HokoriTemp</title>
+    <title>hokoritemp</title>
     <link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='favicon.svg') }}">
     <link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='favicon.svg') }}">
     <script src="https://cdn.tailwindcss.com"></script>
     <script src="https://cdn.tailwindcss.com"></script>
     <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
     <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
 </head>
 </head>
-<body class="bg-gray-50 min-h-screen">
-    <div class="container mx-auto px-4 py-8">
-        <!-- Header -->
-        <div class="text-center mb-8">
-            <h1 class="text-4xl font-bold text-gray-800 mb-2">Subir Archivos Temporales</h1>
-            <p class="text-gray-600">Comparte archivos de forma segura y temporal</p>
-            <div class="mt-4">
-                <a href="/view" class="text-blue-600 hover:text-blue-800 underline">Ver mis enlaces activos</a>
-            </div>
-        </div>
+<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 -->
         <!-- Main Container -->
-        <div class="max-w-6xl mx-auto">
-            <div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
+        <div class="max-w-4xl mx-auto">
+            <div class="space-y-8">
                 
                 
                 <!-- Upload Form -->
                 <!-- Upload Form -->
                 <div class="bg-white rounded-lg shadow-lg p-6">
                 <div class="bg-white rounded-lg shadow-lg p-6">
@@ -45,6 +37,23 @@
                             </div>
                             </div>
                         </div>
                         </div>
 
 
+                        <!-- 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 id="fileInfo" class="hidden mb-6 p-4 bg-blue-50 rounded-lg">
                             <div class="flex items-center justify-between">
                             <div class="flex items-center justify-between">
                                 <div class="flex-1 min-w-0 mr-3">
                                 <div class="flex-1 min-w-0 mr-3">
@@ -59,6 +68,21 @@
                             </div>
                             </div>
                         </div>
                         </div>
 
 
+                        <!-- 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">
                         <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="uploadBtnText">Subir Archivo</span>
                             <span id="uploadBtnLoading" class="hidden">
                             <span id="uploadBtnLoading" class="hidden">
@@ -95,7 +119,7 @@
                             </svg>
                             </svg>
                         </div>
                         </div>
                         <h3 class="text-lg font-medium text-gray-900 mb-2">¡Archivo subido exitosamente!</h3>
                         <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 24 horas</p>
+                        <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">
                         <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>
                             <label class="block text-sm font-medium text-gray-700 mb-2">Enlace de descarga:</label>
@@ -130,11 +154,20 @@
         </div>
         </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">
+                <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>
+    </div>
+
     <script>
     <script>
         // Configuración del servidor
         // Configuración del servidor
         const SERVER_CONFIG = {
         const SERVER_CONFIG = {
-            maxFileSize: {{ max_file_size }},
-            maxFileSizeMB: {{ max_file_size_mb }}
+            maxFileSize: parseInt('{{ max_file_size }}'),
+            maxFileSizeMB: parseInt('{{ max_file_size_mb }}')
         };
         };
     </script>
     </script>
     <script src="{{ url_for('static', filename='app.js') }}"></script>
     <script src="{{ url_for('static', filename='app.js') }}"></script>

+ 100 - 0
templates/myfiles.html

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

+ 0 - 55
templates/view.html

@@ -1,55 +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 - Enlaces Temporales</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">
-    <div class="container mx-auto px-4 py-8">
-        <!-- Header -->
-        <div class="text-center mb-8">
-            <h1 class="text-4xl font-bold text-gray-800 mb-2">Mis Enlaces Temporales</h1>
-            <p class="text-gray-600">Gestiona tus archivos subidos temporalmente</p>
-            <div class="mt-4">
-                <a href="/" class="text-blue-600 hover:text-blue-800 underline">← Volver a subir archivos</a>
-            </div>
-        </div>
-
-        <!-- Links Container -->
-        <div class="max-w-4xl mx-auto">
-            <div id="linksContainer">
-                <!-- 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 que generes 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>
-    </div>
-
-    <script src="{{ url_for('static', filename='view.js') }}"></script>
-</body>
-</html>