Migrate storage from JSON to SQLite with enhanced MVC architecture

- Replace JSON file storage with SQLite database for improved concurrency and data integrity
- Implement repository pattern with dedicated model layer (admin_model, file_model, chunk_model)
- Add database.py with automatic migration from existing JSON files
- Enable WAL mode for thread-safe concurrent access across Gunicorn workers
- Store database files in dedicated db/ folder
- Update controllers to use model layer instead of direct JSON access
- Add admin authentication system with TOTP 2FA support
This commit is contained in:
2025-12-27 14:00:39 -05:00
parent 8eac7b90c6
commit 872ae74668
14 changed files with 2442 additions and 175 deletions

199
src/models/file_model.py Normal file
View File

@@ -0,0 +1,199 @@
import sqlite3
import os
from datetime import datetime, timedelta
from models.database import get_db
import utils.mLogger as log
def create_file(code, filename, path, max_downloads, expiry_hours):
"""Create a new file metadata record
Args:
code: 6-character unique code
filename: Original filename
path: Absolute path to file
max_downloads: Maximum number of downloads allowed
expiry_hours: Hours until file expires
Returns:
bool: True if successful, False otherwise
"""
try:
with get_db() as conn:
expiry_time = datetime.now() + timedelta(hours=expiry_hours)
conn.execute("""
INSERT INTO files (code, filename, path, max_downloads, downloads, expiry_time)
VALUES (?, ?, ?, ?, 0, ?)
""", (code, filename, path, max_downloads, expiry_time.isoformat()))
log.i(f"Created file record: {code} - {filename}")
return True
except sqlite3.Error as e:
log.e(f"Error creating file record: {e}")
return False
def get_file(code):
"""Get file metadata by code
Args:
code: 6-character file code
Returns:
dict: File metadata with keys: code, filename, path, max_downloads, downloads, expiry_time
None: If file not found
"""
try:
with get_db() as conn:
cursor = conn.cursor()
row = cursor.execute("""
SELECT code, filename, path, max_downloads, downloads, expiry_time
FROM files
WHERE code = ?
""", (code,)).fetchone()
if row:
return dict(row)
return None
except sqlite3.Error as e:
log.e(f"Error getting file {code}: {e}")
return None
def increment_download(code):
"""Atomically increment download counter for a file
Args:
code: 6-character file code
Returns:
int: New download count, or None if file not found
"""
try:
with get_db() as conn:
cursor = conn.cursor()
cursor.execute("""
UPDATE files
SET downloads = downloads + 1
WHERE code = ?
""", (code,))
if cursor.rowcount > 0:
# Get the new download count
row = cursor.execute("""
SELECT downloads FROM files WHERE code = ?
""", (code,)).fetchone()
if row:
new_count = row['downloads']
log.i(f"Incremented download counter for {code}: {new_count}")
return new_count
return None
except sqlite3.Error as e:
log.e(f"Error incrementing download for {code}: {e}")
return None
def delete_file(code):
"""Delete file metadata record
Args:
code: 6-character file code
Returns:
bool: True if deleted, False if not found or error
"""
try:
with get_db() as conn:
cursor = conn.cursor()
cursor.execute("DELETE FROM files WHERE code = ?", (code,))
if cursor.rowcount > 0:
log.i(f"Deleted file record: {code}")
return True
return False
except sqlite3.Error as e:
log.e(f"Error deleting file {code}: {e}")
return False
def get_expired_files():
"""Get list of expired files or files that exceeded download limit
Returns:
list: List of dicts with file metadata
"""
try:
with get_db() as conn:
cursor = conn.cursor()
now = datetime.now().isoformat()
rows = cursor.execute("""
SELECT code, filename, path, max_downloads, downloads, expiry_time
FROM files
WHERE expiry_time < ? OR downloads >= max_downloads
""", (now,)).fetchall()
return [dict(row) for row in rows]
except sqlite3.Error as e:
log.e(f"Error getting expired files: {e}")
return []
def cleanup_expired():
"""Delete expired file records and files that reached download limit
Returns:
int: Number of files cleaned up
"""
try:
# Get expired files first so we can delete physical files
expired_files = get_expired_files()
if not expired_files:
return 0
with get_db() as conn:
cursor = conn.cursor()
now = datetime.now().isoformat()
# Delete from database
cursor.execute("""
DELETE FROM files
WHERE expiry_time < ? OR downloads >= max_downloads
""", (now,))
count = cursor.rowcount
# Delete physical files
for file_info in expired_files:
if os.path.exists(file_info['path']):
try:
os.remove(file_info['path'])
log.i(f"Deleted expired file: {file_info['path']}")
except Exception as e:
log.w(f"Could not delete file {file_info['path']}: {e}")
if count > 0:
log.i(f"Cleaned up {count} expired file(s)")
return count
except sqlite3.Error as e:
log.e(f"Error cleaning up expired files: {e}")
return 0
def get_all_files():
"""Get all file metadata records
Returns:
list: List of dicts with file metadata
"""
try:
with get_db() as conn:
cursor = conn.cursor()
rows = cursor.execute("""
SELECT code, filename, path, max_downloads, downloads, expiry_time, created_at
FROM files
ORDER BY created_at DESC
""").fetchall()
return [dict(row) for row in rows]
except sqlite3.Error as e:
log.e(f"Error getting all files: {e}")
return []