|
|
@@ -0,0 +1,252 @@
|
|
|
+from flask import Flask, render_template, request, redirect, url_for, jsonify, session
|
|
|
+import sqlite3
|
|
|
+import string
|
|
|
+import random
|
|
|
+import os
|
|
|
+import sys
|
|
|
+import argparse
|
|
|
+from datetime import datetime, timedelta
|
|
|
+import uuid
|
|
|
+
|
|
|
+app = Flask(__name__)
|
|
|
+app.secret_key = 'tu_clave_secreta_aqui' # Cambiar en producción
|
|
|
+
|
|
|
+# Configuración de la base de datos
|
|
|
+DATABASE = 'url_shortener.db'
|
|
|
+
|
|
|
+def init_db():
|
|
|
+ """Inicializar la base de datos"""
|
|
|
+ conn = sqlite3.connect(DATABASE)
|
|
|
+ cursor = conn.cursor()
|
|
|
+
|
|
|
+ # Tabla para enlaces acortados
|
|
|
+ cursor.execute('''
|
|
|
+ CREATE TABLE IF NOT EXISTS urls (
|
|
|
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
+ original_url TEXT NOT NULL,
|
|
|
+ short_code TEXT UNIQUE NOT NULL,
|
|
|
+ user_session TEXT,
|
|
|
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
|
+ expires_at TIMESTAMP,
|
|
|
+ clicks INTEGER DEFAULT 0
|
|
|
+ )
|
|
|
+ ''')
|
|
|
+
|
|
|
+ # Agregar columna expires_at si no existe (para bases de datos existentes)
|
|
|
+ try:
|
|
|
+ cursor.execute('ALTER TABLE urls ADD COLUMN expires_at TIMESTAMP')
|
|
|
+ conn.commit()
|
|
|
+ except sqlite3.OperationalError:
|
|
|
+ # La columna ya existe
|
|
|
+ pass
|
|
|
+
|
|
|
+ conn.commit()
|
|
|
+ conn.close()
|
|
|
+
|
|
|
+def generate_short_code(length=6):
|
|
|
+ """Generar código corto aleatorio"""
|
|
|
+ characters = string.ascii_letters + string.digits
|
|
|
+ return ''.join(random.choice(characters) for _ in range(length))
|
|
|
+
|
|
|
+def get_user_session():
|
|
|
+ """Obtener o crear sesión de usuario anónimo"""
|
|
|
+ if 'user_id' not in session:
|
|
|
+ session['user_id'] = str(uuid.uuid4())
|
|
|
+ return session['user_id']
|
|
|
+
|
|
|
+@app.route('/')
|
|
|
+def index():
|
|
|
+ """Página principal"""
|
|
|
+ return render_template('index.html')
|
|
|
+
|
|
|
+@app.route('/my-links')
|
|
|
+def my_links():
|
|
|
+ """Página de enlaces del usuario"""
|
|
|
+ user_session = get_user_session()
|
|
|
+
|
|
|
+ # Obtener enlaces del usuario
|
|
|
+ conn = sqlite3.connect(DATABASE)
|
|
|
+ cursor = conn.cursor()
|
|
|
+ cursor.execute('''
|
|
|
+ SELECT original_url, short_code, created_at, clicks, expires_at
|
|
|
+ FROM urls
|
|
|
+ WHERE user_session = ?
|
|
|
+ ORDER BY created_at DESC
|
|
|
+ ''', (user_session,))
|
|
|
+
|
|
|
+ user_urls = cursor.fetchall()
|
|
|
+ conn.close()
|
|
|
+
|
|
|
+ return render_template('my_links.html', user_urls=user_urls)
|
|
|
+
|
|
|
+def calculate_expiration(duration):
|
|
|
+ """Calcular fecha de expiración basada en duración"""
|
|
|
+ if duration == 'never':
|
|
|
+ return None
|
|
|
+ elif duration == '1hour':
|
|
|
+ return datetime.now() + timedelta(hours=1)
|
|
|
+ elif duration == '1day':
|
|
|
+ return datetime.now() + timedelta(days=1)
|
|
|
+ elif duration == '1week':
|
|
|
+ return datetime.now() + timedelta(weeks=1)
|
|
|
+ elif duration == '1month':
|
|
|
+ return datetime.now() + timedelta(days=30)
|
|
|
+ elif duration == '1year':
|
|
|
+ return datetime.now() + timedelta(days=365)
|
|
|
+ else:
|
|
|
+ return None
|
|
|
+
|
|
|
+def is_url_expired(expires_at):
|
|
|
+ """Verificar si un URL ha expirado"""
|
|
|
+ if expires_at is None:
|
|
|
+ return False
|
|
|
+ return datetime.now() > datetime.fromisoformat(expires_at)
|
|
|
+
|
|
|
+@app.route('/shorten', methods=['POST'])
|
|
|
+def shorten_url():
|
|
|
+ """Acortar URL"""
|
|
|
+ # Obtener datos tanto de formulario como de JSON
|
|
|
+ if request.is_json:
|
|
|
+ original_url = request.json.get('url')
|
|
|
+ duration = request.json.get('duration', 'never')
|
|
|
+ else:
|
|
|
+ original_url = request.form.get('url')
|
|
|
+ duration = request.form.get('duration', 'never')
|
|
|
+
|
|
|
+ if not original_url:
|
|
|
+ return jsonify({'error': 'URL es requerida'}), 400
|
|
|
+
|
|
|
+ # Agregar http:// si no tiene protocolo
|
|
|
+ if not original_url.startswith(('http://', 'https://')):
|
|
|
+ original_url = 'http://' + original_url
|
|
|
+
|
|
|
+ user_session = get_user_session()
|
|
|
+ expires_at = calculate_expiration(duration)
|
|
|
+
|
|
|
+ # Generar código corto único
|
|
|
+ while True:
|
|
|
+ short_code = generate_short_code()
|
|
|
+ conn = sqlite3.connect(DATABASE)
|
|
|
+ cursor = conn.cursor()
|
|
|
+ cursor.execute('SELECT id FROM urls WHERE short_code = ?', (short_code,))
|
|
|
+ if not cursor.fetchone():
|
|
|
+ break
|
|
|
+ conn.close()
|
|
|
+
|
|
|
+ # Guardar en base de datos
|
|
|
+ cursor.execute('''
|
|
|
+ INSERT INTO urls (original_url, short_code, user_session, expires_at)
|
|
|
+ VALUES (?, ?, ?, ?)
|
|
|
+ ''', (original_url, short_code, user_session, expires_at))
|
|
|
+
|
|
|
+ conn.commit()
|
|
|
+ conn.close()
|
|
|
+
|
|
|
+ short_url = f"https://please.checkthis.space/s/{short_code}"
|
|
|
+
|
|
|
+ return jsonify({
|
|
|
+ 'success': True,
|
|
|
+ 'short_url': short_url,
|
|
|
+ 'short_code': short_code,
|
|
|
+ 'original_url': original_url,
|
|
|
+ 'expires_at': expires_at.isoformat() if expires_at else None
|
|
|
+ })
|
|
|
+
|
|
|
+@app.route('/s/<short_code>')
|
|
|
+def redirect_url(short_code):
|
|
|
+ """Redirigir a URL original"""
|
|
|
+ conn = sqlite3.connect(DATABASE)
|
|
|
+ cursor = conn.cursor()
|
|
|
+
|
|
|
+ # Buscar URL original y verificar expiración
|
|
|
+ cursor.execute('''
|
|
|
+ SELECT original_url, expires_at FROM urls WHERE short_code = ?
|
|
|
+ ''', (short_code,))
|
|
|
+
|
|
|
+ result = cursor.fetchone()
|
|
|
+
|
|
|
+ if result:
|
|
|
+ original_url, expires_at = result
|
|
|
+
|
|
|
+ # Verificar si el enlace ha expirado
|
|
|
+ if expires_at and is_url_expired(expires_at):
|
|
|
+ conn.close()
|
|
|
+ return render_template('404.html', error_message='Este enlace ha expirado'), 404
|
|
|
+
|
|
|
+ # Incrementar contador de clicks
|
|
|
+ cursor.execute('''
|
|
|
+ UPDATE urls SET clicks = clicks + 1 WHERE short_code = ?
|
|
|
+ ''', (short_code,))
|
|
|
+
|
|
|
+ conn.commit()
|
|
|
+ conn.close()
|
|
|
+
|
|
|
+ return redirect(original_url)
|
|
|
+ else:
|
|
|
+ conn.close()
|
|
|
+ return render_template('404.html'), 404
|
|
|
+
|
|
|
+@app.route('/delete/<short_code>', methods=['POST'])
|
|
|
+def delete_url(short_code):
|
|
|
+ """Eliminar URL acortada"""
|
|
|
+ user_session = get_user_session()
|
|
|
+
|
|
|
+ conn = sqlite3.connect(DATABASE)
|
|
|
+ cursor = conn.cursor()
|
|
|
+
|
|
|
+ # Verificar que el enlace pertenece al usuario
|
|
|
+ cursor.execute('''
|
|
|
+ DELETE FROM urls
|
|
|
+ WHERE short_code = ? AND user_session = ?
|
|
|
+ ''', (short_code, user_session))
|
|
|
+
|
|
|
+ conn.commit()
|
|
|
+ affected_rows = cursor.rowcount
|
|
|
+ conn.close()
|
|
|
+
|
|
|
+ if affected_rows > 0:
|
|
|
+ return jsonify({'success': True})
|
|
|
+ else:
|
|
|
+ return jsonify({'error': 'Enlace no encontrado o no autorizado'}), 404
|
|
|
+
|
|
|
+@app.route('/stats')
|
|
|
+def stats():
|
|
|
+ """Estadísticas generales"""
|
|
|
+ conn = sqlite3.connect(DATABASE)
|
|
|
+ cursor = conn.cursor()
|
|
|
+
|
|
|
+ # Estadísticas generales
|
|
|
+ cursor.execute('SELECT COUNT(*) FROM urls')
|
|
|
+ total_urls = cursor.fetchone()[0]
|
|
|
+
|
|
|
+ cursor.execute('SELECT SUM(clicks) FROM urls')
|
|
|
+ total_clicks = cursor.fetchone()[0] or 0
|
|
|
+
|
|
|
+ # URLs más populares
|
|
|
+ cursor.execute('''
|
|
|
+ SELECT original_url, short_code, clicks, created_at
|
|
|
+ FROM urls
|
|
|
+ ORDER BY clicks DESC
|
|
|
+ LIMIT 10
|
|
|
+ ''')
|
|
|
+ popular_urls = cursor.fetchall()
|
|
|
+
|
|
|
+ conn.close()
|
|
|
+
|
|
|
+ return render_template('stats.html',
|
|
|
+ total_urls=total_urls,
|
|
|
+ total_clicks=total_clicks,
|
|
|
+ popular_urls=popular_urls)
|
|
|
+
|
|
|
+if __name__ == '__main__':
|
|
|
+ init_db()
|
|
|
+
|
|
|
+ # Configurar argumentos de línea de comandos
|
|
|
+ parser = argparse.ArgumentParser(description='Acortador de URLs')
|
|
|
+ parser.add_argument('--port', '-p', type=int, default=5000,
|
|
|
+ help='Puerto en el que ejecutar la aplicación (por defecto: 5000)')
|
|
|
+
|
|
|
+ args = parser.parse_args()
|
|
|
+
|
|
|
+ print(f"Ejecutando en puerto {args.port}")
|
|
|
+ app.run(debug=True, host='0.0.0.0', port=args.port)
|