fix: wrap AsyncSession in context manager across all API blueprints

Replace all bare `session = AsyncSession(bind=conn)` assignments with
`async with AsyncSession(bind=conn) as session:` in downloads.py,
credentials.py, settings.py, sources.py, and subscriptions.py to
ensure sessions are properly closed on all code paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-18 23:38:58 -04:00
parent dc146f4a16
commit 080a1254c7
5 changed files with 771 additions and 800 deletions
+104 -109
View File
@@ -81,24 +81,23 @@ async def get_setting_value(session: AsyncSession, key: str, default=None):
async def get_all_settings():
"""Get all application settings."""
async with current_app.db_engine.connect() as conn:
session = AsyncSession(bind=conn)
async with AsyncSession(bind=conn) as session:
result = await session.execute(select(Setting))
settings = result.scalars().all()
result = await session.execute(select(Setting))
settings = result.scalars().all()
# Build settings dict with defaults
settings_dict = dict(DEFAULT_SETTINGS)
for s in settings:
settings_dict[s.key] = s.value
# Build settings dict with defaults
settings_dict = dict(DEFAULT_SETTINGS)
for s in settings:
settings_dict[s.key] = s.value
# Ensure extension API key exists
api_key = await get_or_create_extension_api_key(session)
settings_dict[EXTENSION_API_KEY_SETTING] = api_key
# Ensure extension API key exists
api_key = await get_or_create_extension_api_key(session)
settings_dict[EXTENSION_API_KEY_SETTING] = api_key
# Get cached storage statistics (calculated by background task)
storage_stats = await get_cached_storage_stats(session)
# 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})
return jsonify({"settings": settings_dict, "storage_stats": storage_stats})
@bp.route("", methods=["PATCH"])
@@ -110,68 +109,66 @@ async def update_settings():
return jsonify({"error": "No settings provided"}), 400
async with current_app.db_engine.connect() as conn:
session = AsyncSession(bind=conn)
async with AsyncSession(bind=conn) as session:
for key, value in data.items():
# Check if setting exists
result = await session.execute(select(Setting).where(Setting.key == key))
setting = result.scalar()
for key, value in data.items():
# Check if setting exists
result = await session.execute(select(Setting).where(Setting.key == key))
setting = result.scalar()
if setting:
current_app.logger.info(f"Updating setting '{key}': {setting.value} -> {value} (type: {type(value).__name__})")
setting.value = value
else:
current_app.logger.info(f"Creating setting '{key}': {value} (type: {type(value).__name__})")
setting = Setting(key=key, value=value)
session.add(setting)
if setting:
current_app.logger.info(f"Updating setting '{key}': {setting.value} -> {value} (type: {type(value).__name__})")
setting.value = value
else:
current_app.logger.info(f"Creating setting '{key}': {value} (type: {type(value).__name__})")
setting = Setting(key=key, value=value)
session.add(setting)
await session.commit()
await session.commit()
# Return updated settings
result = await session.execute(select(Setting))
settings = result.scalars().all()
settings_dict = dict(DEFAULT_SETTINGS)
for s in settings:
settings_dict[s.key] = s.value
# Return updated settings
result = await session.execute(select(Setting))
settings = result.scalars().all()
settings_dict = dict(DEFAULT_SETTINGS)
for s in settings:
settings_dict[s.key] = s.value
current_app.logger.info(f"Updated settings: {list(data.keys())}")
return jsonify({"settings": settings_dict})
current_app.logger.info(f"Updated settings: {list(data.keys())}")
return jsonify({"settings": settings_dict})
@bp.route("/api-key", methods=["GET"])
async def get_extension_api_key():
"""Get the extension API key."""
async with current_app.db_engine.connect() as conn:
session = AsyncSession(bind=conn)
api_key = await get_or_create_extension_api_key(session)
return jsonify({"api_key": api_key})
async with AsyncSession(bind=conn) as session:
api_key = await get_or_create_extension_api_key(session)
return jsonify({"api_key": api_key})
@bp.route("/api-key/regenerate", methods=["POST"])
async def regenerate_extension_api_key():
"""Regenerate the extension API key."""
async with current_app.db_engine.connect() as conn:
session = AsyncSession(bind=conn)
async with AsyncSession(bind=conn) as session:
# Find existing key
result = await session.execute(
select(Setting).where(Setting.key == EXTENSION_API_KEY_SETTING)
)
setting = result.scalar()
# Find existing key
result = await session.execute(
select(Setting).where(Setting.key == EXTENSION_API_KEY_SETTING)
)
setting = result.scalar()
# Generate new key
new_key = generate_api_key()
# Generate new key
new_key = generate_api_key()
if setting:
setting.value = new_key
else:
setting = Setting(key=EXTENSION_API_KEY_SETTING, value=new_key)
session.add(setting)
if setting:
setting.value = new_key
else:
setting = Setting(key=EXTENSION_API_KEY_SETTING, value=new_key)
session.add(setting)
await session.commit()
await session.commit()
current_app.logger.info("Extension API key regenerated")
return jsonify({"api_key": new_key, "message": "API key regenerated"})
current_app.logger.info("Extension API key regenerated")
return jsonify({"api_key": new_key, "message": "API key regenerated"})
@bp.route("/gallery-dl", methods=["GET"])
@@ -246,28 +243,27 @@ async def get_logs():
limit = min(int(request.args.get("limit", 50)), 200)
async with current_app.db_engine.connect() as conn:
session = AsyncSession(bind=conn)
async with AsyncSession(bind=conn) as session:
# Get recent downloads with output logs
query = select(Download).order_by(desc(Download.created_at)).limit(limit)
result = await session.execute(query)
downloads = result.scalars().all()
# Get recent downloads with output logs
query = select(Download).order_by(desc(Download.created_at)).limit(limit)
result = await session.execute(query)
downloads = result.scalars().all()
logs = []
for dl in downloads:
metadata = dl.metadata_ or {}
if metadata.get("stdout") or metadata.get("stderr") or dl.error_message:
logs.append({
"id": dl.id,
"url": dl.url,
"status": dl.status,
"created_at": dl.created_at.isoformat() + "Z" if dl.created_at else None,
"stdout": metadata.get("stdout", ""),
"stderr": metadata.get("stderr", ""),
"error_message": dl.error_message,
})
logs = []
for dl in downloads:
metadata = dl.metadata_ or {}
if metadata.get("stdout") or metadata.get("stderr") or dl.error_message:
logs.append({
"id": dl.id,
"url": dl.url,
"status": dl.status,
"created_at": dl.created_at.isoformat() + "Z" if dl.created_at else None,
"stdout": metadata.get("stdout", ""),
"stderr": metadata.get("stderr", ""),
"error_message": dl.error_message,
})
return jsonify({"logs": logs, "count": len(logs)})
return jsonify({"logs": logs, "count": len(logs)})
@bp.route("/system-info", methods=["GET"])
@@ -333,39 +329,38 @@ async def debug_schedule_interval():
the worker would read.
"""
async with current_app.db_engine.connect() as conn:
session = AsyncSession(bind=conn)
async with AsyncSession(bind=conn) as session:
# Query the raw setting from database
result = await session.execute(
select(Setting).where(Setting.key == "download.schedule_interval")
)
setting = result.scalar()
# Query the raw setting from database
result = await session.execute(
select(Setting).where(Setting.key == "download.schedule_interval")
)
setting = result.scalar()
if setting:
raw_value = setting.value
db_info = {
"found_in_database": True,
"raw_value": raw_value,
"raw_type": type(raw_value).__name__,
"as_int": int(raw_value) if raw_value is not None else None,
"updated_at": setting.updated_at.isoformat() if setting.updated_at else None,
}
else:
db_info = {
"found_in_database": False,
"raw_value": None,
"raw_type": None,
"as_int": None,
"updated_at": None,
}
if setting:
raw_value = setting.value
db_info = {
"found_in_database": True,
"raw_value": raw_value,
"raw_type": type(raw_value).__name__,
"as_int": int(raw_value) if raw_value is not None else None,
"updated_at": setting.updated_at.isoformat() if setting.updated_at else None,
}
else:
db_info = {
"found_in_database": False,
"raw_value": None,
"raw_type": None,
"as_int": None,
"updated_at": None,
}
# What the worker would use
fallback_value = DEFAULT_SETTINGS.get("download.schedule_interval", 3600)
worker_would_use = db_info["as_int"] if db_info["found_in_database"] else fallback_value
# What the worker would use
fallback_value = DEFAULT_SETTINGS.get("download.schedule_interval", 3600)
worker_would_use = db_info["as_int"] if db_info["found_in_database"] else fallback_value
return jsonify({
"database": db_info,
"default_settings_value": fallback_value,
"worker_would_use": worker_would_use,
"worker_would_use_hours": worker_would_use / 3600 if worker_would_use else None,
})
return jsonify({
"database": db_info,
"default_settings_value": fallback_value,
"worker_would_use": worker_would_use,
"worker_would_use_hours": worker_would_use / 3600 if worker_would_use else None,
})