improve ux, moved functionality for better ui experience on app and extension.
This commit is contained in:
+37
-36
@@ -1,54 +1,37 @@
|
||||
"""Settings API endpoints."""
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from quart import Blueprint, request, jsonify, current_app
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.setting import Setting, DEFAULT_SETTINGS, EXTENSION_API_KEY_SETTING, generate_api_key
|
||||
from app.models.setting import Setting, DEFAULT_SETTINGS, EXTENSION_API_KEY_SETTING, STORAGE_STATS_SETTING, generate_api_key
|
||||
from app.config import get_settings
|
||||
|
||||
bp = Blueprint("settings", __name__)
|
||||
|
||||
|
||||
def get_storage_stats():
|
||||
"""Calculate storage statistics for the downloads directory."""
|
||||
config = get_settings()
|
||||
downloads_path = Path(config.download_path)
|
||||
async def get_cached_storage_stats(session: AsyncSession) -> dict:
|
||||
"""Get cached storage statistics from the database.
|
||||
|
||||
if not downloads_path.exists():
|
||||
return {"total_size": 0, "total_size_formatted": "0 B", "file_count": 0}
|
||||
Returns cached stats if available, otherwise returns placeholder.
|
||||
Stats are calculated by a background Celery task.
|
||||
"""
|
||||
result = await session.execute(
|
||||
select(Setting).where(Setting.key == STORAGE_STATS_SETTING)
|
||||
)
|
||||
setting = result.scalar_one_or_none()
|
||||
|
||||
total_size = 0
|
||||
file_count = 0
|
||||
|
||||
try:
|
||||
for root, dirs, files in os.walk(downloads_path):
|
||||
for f in files:
|
||||
fp = os.path.join(root, f)
|
||||
try:
|
||||
total_size += os.path.getsize(fp)
|
||||
file_count += 1
|
||||
except OSError:
|
||||
pass
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Error calculating storage stats: {e}")
|
||||
return {"total_size": 0, "total_size_formatted": "Error", "file_count": 0}
|
||||
|
||||
# Format size
|
||||
def format_size(size):
|
||||
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
|
||||
if size < 1024:
|
||||
return f"{size:.1f} {unit}"
|
||||
size /= 1024
|
||||
return f"{size:.1f} PB"
|
||||
if setting and setting.value:
|
||||
return setting.value
|
||||
|
||||
# No cached stats yet - return placeholder
|
||||
return {
|
||||
"total_size": total_size,
|
||||
"total_size_formatted": format_size(total_size),
|
||||
"file_count": file_count,
|
||||
"total_size": 0,
|
||||
"total_size_formatted": "Calculating...",
|
||||
"file_count": 0,
|
||||
"calculated_at": None,
|
||||
}
|
||||
|
||||
|
||||
@@ -112,8 +95,8 @@ async def get_all_settings():
|
||||
api_key = await get_or_create_extension_api_key(session)
|
||||
settings_dict[EXTENSION_API_KEY_SETTING] = api_key
|
||||
|
||||
# Include storage statistics
|
||||
storage_stats = get_storage_stats()
|
||||
# Get cached storage statistics (calculated by background task)
|
||||
storage_stats = await get_cached_storage_stats(session)
|
||||
|
||||
return jsonify({"settings": settings_dict, "storage_stats": storage_stats})
|
||||
|
||||
@@ -231,6 +214,24 @@ async def update_gallery_dl_config():
|
||||
return jsonify({"error": f"Failed to write config: {e}"}), 500
|
||||
|
||||
|
||||
@bp.route("/storage-stats/refresh", methods=["POST"])
|
||||
async def refresh_storage_stats():
|
||||
"""Trigger a refresh of storage statistics.
|
||||
|
||||
This queues a background task to recalculate storage stats.
|
||||
"""
|
||||
from app.tasks.maintenance import update_storage_stats
|
||||
|
||||
try:
|
||||
# Queue the task
|
||||
update_storage_stats.delay()
|
||||
current_app.logger.info("Queued storage stats refresh")
|
||||
return jsonify({"message": "Storage stats refresh queued"})
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Failed to queue storage stats refresh: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@bp.route("/logs", methods=["GET"])
|
||||
async def get_logs():
|
||||
"""Get recent download logs from download metadata.
|
||||
|
||||
@@ -47,3 +47,6 @@ DEFAULT_SETTINGS = {
|
||||
|
||||
# Key for the extension API key setting
|
||||
EXTENSION_API_KEY_SETTING = "security.extension_api_key"
|
||||
|
||||
# Key for cached storage stats
|
||||
STORAGE_STATS_SETTING = "cache.storage_stats"
|
||||
|
||||
@@ -11,7 +11,7 @@ celery_app = Celery(
|
||||
"gallery_subscriber",
|
||||
broker=settings.redis_url,
|
||||
backend=settings.redis_url,
|
||||
include=["app.tasks.downloads"],
|
||||
include=["app.tasks.downloads", "app.tasks.maintenance"],
|
||||
)
|
||||
|
||||
# Celery configuration
|
||||
@@ -40,4 +40,8 @@ celery_app.conf.beat_schedule = {
|
||||
"task": "app.tasks.downloads.cleanup_old_downloads",
|
||||
"schedule": crontab(hour=3, minute=0), # Daily at 3 AM
|
||||
},
|
||||
"update-storage-stats": {
|
||||
"task": "tasks.update_storage_stats",
|
||||
"schedule": crontab(minute="*/30"), # Every 30 minutes
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
"""System maintenance Celery tasks."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
||||
|
||||
from app.config import get_settings
|
||||
from app.tasks.celery_app import celery_app
|
||||
from app.models.setting import Setting, STORAGE_STATS_SETTING
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
|
||||
def get_async_session():
|
||||
"""Get async session factory for Celery tasks."""
|
||||
engine = create_async_engine(settings.async_database_url, echo=False)
|
||||
session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
return session_factory, engine
|
||||
|
||||
|
||||
async def _cleanup_engine(engine):
|
||||
"""Properly dispose of the async engine."""
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
def format_size(size_bytes: int) -> str:
|
||||
"""Format bytes as human-readable size."""
|
||||
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
|
||||
if size_bytes < 1024:
|
||||
return f"{size_bytes:.1f} {unit}"
|
||||
size_bytes /= 1024
|
||||
return f"{size_bytes:.1f} PB"
|
||||
|
||||
|
||||
def calculate_storage_stats() -> dict:
|
||||
"""Calculate storage statistics for the downloads directory.
|
||||
|
||||
This runs synchronously since it's a filesystem operation.
|
||||
"""
|
||||
downloads_path = Path(settings.download_path)
|
||||
|
||||
if not downloads_path.exists():
|
||||
return {
|
||||
"total_size": 0,
|
||||
"total_size_formatted": "0 B",
|
||||
"file_count": 0,
|
||||
"calculated_at": datetime.utcnow().isoformat(),
|
||||
}
|
||||
|
||||
total_size = 0
|
||||
file_count = 0
|
||||
|
||||
try:
|
||||
for root, dirs, files in os.walk(downloads_path):
|
||||
for f in files:
|
||||
fp = os.path.join(root, f)
|
||||
try:
|
||||
total_size += os.path.getsize(fp)
|
||||
file_count += 1
|
||||
except OSError:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.error(f"Error calculating storage stats: {e}")
|
||||
return {
|
||||
"total_size": 0,
|
||||
"total_size_formatted": "Error",
|
||||
"file_count": 0,
|
||||
"error": str(e),
|
||||
"calculated_at": datetime.utcnow().isoformat(),
|
||||
}
|
||||
|
||||
return {
|
||||
"total_size": total_size,
|
||||
"total_size_formatted": format_size(total_size),
|
||||
"file_count": file_count,
|
||||
"calculated_at": datetime.utcnow().isoformat(),
|
||||
}
|
||||
|
||||
|
||||
async def _update_storage_stats_async() -> dict:
|
||||
"""Async implementation to calculate and store storage stats."""
|
||||
session_factory, engine = get_async_session()
|
||||
|
||||
try:
|
||||
# Calculate stats (synchronous filesystem operation)
|
||||
stats = calculate_storage_stats()
|
||||
logger.info(f"Calculated storage stats: {stats['total_size_formatted']}, {stats['file_count']} files")
|
||||
|
||||
# Store in database
|
||||
async with session_factory() as session:
|
||||
# Check if setting exists
|
||||
result = await session.execute(
|
||||
select(Setting).where(Setting.key == STORAGE_STATS_SETTING)
|
||||
)
|
||||
setting = result.scalar_one_or_none()
|
||||
|
||||
if setting:
|
||||
setting.value = stats
|
||||
else:
|
||||
setting = Setting(key=STORAGE_STATS_SETTING, value=stats)
|
||||
session.add(setting)
|
||||
|
||||
await session.commit()
|
||||
|
||||
return stats
|
||||
|
||||
finally:
|
||||
await _cleanup_engine(engine)
|
||||
|
||||
|
||||
@celery_app.task(name="tasks.update_storage_stats")
|
||||
def update_storage_stats() -> dict:
|
||||
"""Celery task to calculate and cache storage stats.
|
||||
|
||||
This task should be scheduled to run periodically (e.g., every hour).
|
||||
"""
|
||||
logger.info("Starting storage stats calculation...")
|
||||
result = asyncio.run(_update_storage_stats_async())
|
||||
logger.info(f"Storage stats updated: {result}")
|
||||
return result
|
||||
Reference in New Issue
Block a user