mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-25 18:40:06 -04:00
* db: request-path library functions use the request's connection Library functions an API request reaches now take the request's Session (first argument) instead of opening their own session or cursor, so a request needs one SQLite connection. Raw SQL runs through the new `wrolpi.db.session_curs(session)`, a cursor on the Session's connection; `RequestDB.curs()` uses it. - Search: files, videos, archives, flasher, and docs search share the request connection for the total, the page of ids, and the page's models (3 connections -> 1). - Zim search, including tag-filtered search and estimates (2 -> 1). - Channel detail (`Channel.get_statistics` reads on the Channel's own Session) and the channel list. - Video detail, edit, description, captions, comments and transcode. `get_video_for_app` and `update_video` no longer commit; their handlers use `request.ctx.db.write()`, so editing a Video and reading it back is one transaction. - Tags (list, overlaps, recent), file statistics and estimates, other estimates, bulk tag preview, the download list, retry, kill and the status summary, and docs/videos/archive statistics. Not converted: file operations that refresh files inside the request (delete, rename, upload, channel and collection tag/move) commit page by page in code shared with the file worker; the cached zim entry lookup; config import. The connection-count tests now assert one connection for every endpoint they cover (no xfails left), and cover the videos and archive statistics endpoints, which had no API test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * downloads: startup retries in a write session of its own start_single_tasks still called retry_downloads() without a Session after it began requiring one, so any start after a completed refresh raised TypeError there: incomplete downloads were not reset and the download_on_startup enable after it never ran. Startup is not a request, so DownloadManager.retry_downloads_on_startup() opens and commits its own short write transaction. Tested through the method, not start_single_tasks: importing main registers its startup listeners on the shared app, and every later test in that worker then runs them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
55 lines
2.2 KiB
Python
55 lines
2.2 KiB
Python
import asyncio
|
|
from http import HTTPStatus
|
|
from urllib.parse import unquote
|
|
|
|
from sanic import Blueprint, Request, response
|
|
from sanic_ext import validate
|
|
from sanic_ext.extensions.openapi import openapi
|
|
|
|
from modules.flasher import lib, schema
|
|
from modules.flasher.config import get_flasher_config
|
|
from wrolpi.api_utils import json_response
|
|
|
|
flasher_bp = Blueprint('Flasher', '/api/flasher')
|
|
|
|
|
|
@flasher_bp.post('/search')
|
|
@openapi.definition(
|
|
summary='Search .bin firmware and filter to ESP images (optionally for a specific chip)',
|
|
body=schema.FlasherSearchRequest,
|
|
)
|
|
@validate(schema.FlasherSearchRequest)
|
|
async def post_flasher_search(request: Request, body: schema.FlasherSearchRequest):
|
|
# Reading many firmware headers touches the disk; run off the event loop.
|
|
file_groups, total = await asyncio.to_thread(
|
|
lib.search_esp_firmware, request.ctx.session, body.chip, body.path, body.limit)
|
|
return json_response(dict(file_groups=file_groups, totals=dict(file_groups=total)), HTTPStatus.OK)
|
|
|
|
|
|
@flasher_bp.get('/configs')
|
|
@openapi.definition(summary='List saved firmware flashing configurations')
|
|
async def get_flasher_configs(_: Request):
|
|
return json_response(dict(configurations=get_flasher_config().configurations), HTTPStatus.OK)
|
|
|
|
|
|
@flasher_bp.post('/configs')
|
|
@openapi.definition(
|
|
summary='Save (add or replace by name) a firmware flashing configuration',
|
|
body=schema.FlasherSaveConfigRequest,
|
|
)
|
|
@validate(schema.FlasherSaveConfigRequest)
|
|
async def save_flasher_config(_: Request, body: schema.FlasherSaveConfigRequest):
|
|
try:
|
|
configuration = get_flasher_config().save_configuration(body.name, body.files, body.erase_all)
|
|
except ValueError as e:
|
|
return response.json({'error': str(e)}, HTTPStatus.BAD_REQUEST)
|
|
return json_response(dict(configuration=configuration), HTTPStatus.CREATED)
|
|
|
|
|
|
@flasher_bp.delete('/configs/<name:str>')
|
|
@openapi.definition(summary='Delete a saved firmware flashing configuration by name')
|
|
async def delete_flasher_config(_: Request, name: str):
|
|
name = unquote(name)
|
|
if get_flasher_config().delete_configuration(name):
|
|
return response.empty()
|
|
return response.json({'error': f'No saved configuration named {name!r}'}, HTTPStatus.NOT_FOUND)
|