feat(widget): 显示下载上传速度和剩余磁盘空间 (#7177)

This commit is contained in:
jxxghp
2026-10-25 11:28:36 +08:00
committed by GitHub
parent 86ededa739
commit d5ecf2927c
10 changed files with 389 additions and 37 deletions

View File

@@ -0,0 +1,86 @@
"""
为仪表盘和受限小组件装配同一下载器与本地目录采样口径。
"""
import math
from pathlib import Path
from app.adapters.system.host import SystemUtils
from app.application.directory import DirectoryHelper
from app.chain.dashboard import DashboardChain
from app.schemas.dashboard import DownloaderInfo
def _download_free_space(*, btrfs_fsid_dedup: bool, require_source: bool) -> float | None:
"""
复用目录空间去重,严格读取不把未配置或不完整的容量误报为零。
:param btrfs_fsid_dedup: 原仪表盘使用的文件系统去重配置
:param require_source: 小组件需要可确认的完整本地目录来源
:return: 可确认的剩余字节数;严格模式的未知容量为 None
:raises OSError: 普通仪表盘保留原有的空间读取异常
"""
try:
directories = DirectoryHelper().get_local_download_dirs()
paths = [Path(directory.download_path) for directory in directories if directory.download_path]
if require_source and (not paths or not all(path.exists() for path in paths)):
return None
total, available = SystemUtils.space_usage(paths, btrfs_fsid_dedup=btrfs_fsid_dedup)
if require_source and (
not math.isfinite(total) or not math.isfinite(available) or not 0 <= available <= total or total <= 0
):
return None
return available
except OSError:
if require_source:
return None
raise
def _check_speeds(info: DownloaderInfo) -> None:
"""
拒绝缺失、负数和非有限的原始速度,真实零速率仍是有效采样。
:param info: 单个下载器或汇总后的原始字节统计
:raises ValueError: 速率来源不能代表本次真实采样
"""
if any(
value is None or not math.isfinite(value) or value < 0 for value in (info.download_speed, info.upload_speed)
):
raise ValueError("下载器速度来源暂不可用")
def sample_downloader(
name: str | None = None,
*,
btrfs_fsid_dedup: bool = False,
require_source: bool = False,
) -> DownloaderInfo:
"""
汇总原下载器的字节速率和累计量,包含下载列表之外的做种上传。
:param name: 原仪表盘可选择的下载器,None 表示全部
:param btrfs_fsid_dedup: 本次请求的原目录空间去重配置
:param require_source: 仅小组件严格拒绝缺失或非法速度;普通页面保留零值兼容
:return: 共用口径的统计,严格模式的未知磁盘容量为 None
:raises ValueError: 严格模式下缺少真实下载器采样或速度非法
"""
free_space = _download_free_space(btrfs_fsid_dedup=btrfs_fsid_dedup, require_source=require_source)
transfer_infos = DashboardChain().downloader_info(name)
if not transfer_infos:
if require_source:
raise ValueError("下载器统计来源暂不可用")
return DownloaderInfo()
if require_source:
for info in transfer_infos:
_check_speeds(info)
result = DownloaderInfo(
download_speed=sum(info.download_speed or 0 for info in transfer_infos),
upload_speed=sum(info.upload_speed or 0 for info in transfer_infos),
download_size=sum(info.download_size or 0 for info in transfer_infos),
upload_size=sum(info.upload_size or 0 for info in transfer_infos),
free_space=free_space,
)
if require_source:
_check_speeds(result)
return result

View File

@@ -8,8 +8,10 @@ from typing import NoReturn
from fastapi import Depends, HTTPException, Response
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from app.api.context import get_host_runtime
from app.api.context import get_api_runtime_config, get_host_runtime
from app.api.dependencies.dashboard import sample_downloader
from app.api.dependencies.history import get_dashboard_query_service, get_history_query_service
from app.application.configuration import ApiRuntimeConfig
from app.application.dashboard import DashboardQueryService
from app.application.history import HistoryQueryService
from app.application.query import get_configured_data_query_service
@@ -74,12 +76,14 @@ def get_widget_grant(
def get_widget_snapshot_service(
dashboard: DashboardQueryService = Depends(get_dashboard_query_service),
history: HistoryQueryService = Depends(get_history_query_service),
runtime_config: ApiRuntimeConfig = Depends(get_api_runtime_config),
) -> WidgetSnapshotService:
"""
只连接现有的查询与业务链,普通页面不会调用小组件专用接口。
:param dashboard: 原仪表板管理员查询服务
:param history: 原历史列表查询服务
:param runtime_config: 本次请求的空间去重配置,不从组件凭据或参数读取
:return: 使用原有 owner、计数和成功筛选的快照投影服务
"""
return WidgetSnapshotService(
@@ -87,5 +91,6 @@ def get_widget_snapshot_service(
history=history,
dashboard=dashboard,
downloads=partial(DownloadChain().downloading, require_source=True),
downloader=partial(sample_downloader, btrfs_fsid_dedup=runtime_config.btrfs_fsid_dedup, require_source=True),
storage=partial(DashboardChain().storage, require_source=True),
)

View File

@@ -2,7 +2,6 @@
汇总仪表盘所需的服务统计、系统状态与活动数据。
"""
from pathlib import Path
from typing import Annotated, Any, List, Optional
from fastapi import Depends
@@ -11,6 +10,7 @@ from app.adapters.system.host import SystemUtils
from app.adapters.web.security.access import verify_apitoken
from app.api.context import get_api_runtime_config, resolve_api_runtime_config
from app.api.dependencies.auth import get_current_active_superuser
from app.api.dependencies.dashboard import sample_downloader
from app.api.dependencies.history import get_dashboard_query_service
from app.api.response import (
CompatibleCountParam,
@@ -19,7 +19,6 @@ from app.api.response import (
)
from app.application.configuration import ApiRuntimeConfig
from app.application.dashboard import DashboardQueryService
from app.application.directory import DirectoryHelper
from app.application.scheduling import get_scheduler
from app.chain.dashboard import DashboardChain
from app.runtime.execution import run_in_threadpool
@@ -49,25 +48,13 @@ def _build_downloader(
btrfs_fsid_dedup: bool = False,
) -> _SchemaDownloaderInfo:
"""
构建下载器统计信息。
使用共用采样口径构建下载器统计,保持普通页面的宽容来源语义。
:param name: 下载器名称,None 表示全部
:param btrfs_fsid_dedup: 当前请求的 Btrfs 文件系统去重配置
:return: 原仪表盘的全局字节统计与本地目录可用容量
"""
# 下载目录空间
download_dirs = DirectoryHelper().get_local_download_dirs()
_, free_space = SystemUtils.space_usage(
[Path(d.download_path) for d in download_dirs if d.download_path],
btrfs_fsid_dedup=btrfs_fsid_dedup,
)
# 下载器信息
downloader_info = _SchemaDownloaderInfo()
transfer_infos = DashboardChain().downloader_info(name)
if transfer_infos:
for transfer_info in transfer_infos:
downloader_info.download_speed = (downloader_info.download_speed or 0) + (transfer_info.download_speed or 0)
downloader_info.upload_speed = (downloader_info.upload_speed or 0) + (transfer_info.upload_speed or 0)
downloader_info.download_size = (downloader_info.download_size or 0) + (transfer_info.download_size or 0)
downloader_info.upload_size = (downloader_info.upload_size or 0) + (transfer_info.upload_size or 0)
downloader_info.free_space = free_space
return downloader_info
return sample_downloader(name, btrfs_fsid_dedup=btrfs_fsid_dedup)
@router.get("/statistic", summary="媒体数量统计", response_model=_SchemaStatistic)