Skip to content

Commit 5d0baca

Browse files
committed
fix(dashboard): 修复首页存储空间显示为 0
仪表板 _build_storage 原先仅按 library_storage 选取存储,用户只配置 本地下载目录而未填媒体库存储时存储集合为空,直接返回 0;而设置页经 LocalStorage.usage 同时统计下载目录与媒体库目录,故显示正常。 改为汇总下载目录的 storage 与媒体库目录的 library_storage,与 usage 口径对齐。存储名用 set 去重,同一存储只查询一次;磁盘层面 space_usage 已按 st_dev / Btrfs FSID 去重,相同磁盘的不同目录不会重复累加。 closes #6268
1 parent 233e8c6 commit 5d0baca

2 files changed

Lines changed: 164 additions & 1 deletion

File tree

app/api/endpoints/dashboard.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,13 @@ def _build_storage() -> schemas.Storage:
6464
dirs = DirectoryHelper().get_dirs()
6565
if not dirs:
6666
return schemas.Storage(total_storage=total, used_storage=total - available)
67-
storages = set([d.library_storage for d in dirs if d.library_storage])
67+
# 下载目录按 storage、媒体库目录按 library_storage 汇总存储集合,
68+
# 用 set 去重存储名,避免同一存储被重复统计;
69+
# 各存储的 usage 内部已按磁盘(st_dev / Btrfs FSID)去重,相同磁盘的不同目录不会重复累加。
70+
storages = set(
71+
[d.storage for d in dirs if d.download_path and d.storage]
72+
+ [d.library_storage for d in dirs if d.library_path and d.library_storage]
73+
)
6874
for _storage in storages:
6975
_result = StorageChain().manage_storage(storage=_storage, action=StorageAction.USAGE.value)
7076
_usage = _result.get("data") if _result.get("success") else None

tests/test_dashboard_storage.py

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
"""仪表板存储空间统计口径测试。
2+
3+
覆盖 issue #6268:仪表板此前仅按 ``library_storage`` 选存储,
4+
只配置本地下载目录(未填媒体库存储)时会退化为 0,而设置页正常。
5+
"""
6+
from typing import Any, Dict, List
7+
from unittest.mock import patch
8+
9+
from app import schemas
10+
from app.api.endpoints import dashboard as dashboard_endpoint
11+
from app.schemas.types import StorageAction
12+
13+
14+
def _usage_result(total: float, available: float) -> Dict[str, Any]:
15+
"""构造 storage_manage 契约的 usage 返回结构。"""
16+
return {"success": True, "data": {"total": total, "available": available}}
17+
18+
19+
def _patch_dirs(dirs: List[schemas.TransferDirectoryConf]):
20+
"""替换仪表板读取的目录配置。"""
21+
return patch.object(dashboard_endpoint.DirectoryHelper, "get_dirs", return_value=dirs)
22+
23+
24+
def test_storage_counts_local_download_dir_without_library_storage():
25+
"""只配置本地下载目录时仍应统计到该存储(issue #6268 回归)。"""
26+
dirs = [
27+
schemas.TransferDirectoryConf(
28+
name="下载目录",
29+
storage="local",
30+
download_path="/downloads",
31+
)
32+
]
33+
34+
with _patch_dirs(dirs), patch.object(
35+
dashboard_endpoint.StorageChain,
36+
"manage_storage",
37+
return_value=_usage_result(1000.0, 400.0),
38+
) as mocked_usage:
39+
ret = dashboard_endpoint._build_storage()
40+
41+
mocked_usage.assert_called_once_with(storage="local", action=StorageAction.USAGE.value)
42+
assert ret.total_storage == 1000.0
43+
assert ret.used_storage == 600.0
44+
45+
46+
def test_storage_queries_each_storage_once():
47+
"""同一存储被多个目录引用时只查询一次,避免重复累加。"""
48+
dirs = [
49+
schemas.TransferDirectoryConf(
50+
name="下载与媒体库同盘",
51+
storage="local",
52+
download_path="/downloads",
53+
library_path="/media",
54+
library_storage="local",
55+
),
56+
schemas.TransferDirectoryConf(
57+
name="另一个本地目录",
58+
storage="local",
59+
download_path="/downloads2",
60+
library_path="/media2",
61+
library_storage="local",
62+
),
63+
]
64+
65+
with _patch_dirs(dirs), patch.object(
66+
dashboard_endpoint.StorageChain,
67+
"manage_storage",
68+
return_value=_usage_result(1000.0, 400.0),
69+
) as mocked_usage:
70+
ret = dashboard_endpoint._build_storage()
71+
72+
assert mocked_usage.call_count == 1
73+
assert ret.total_storage == 1000.0
74+
assert ret.used_storage == 600.0
75+
76+
77+
def test_storage_sums_distinct_storages():
78+
"""不同存储分别统计并累加。"""
79+
dirs = [
80+
schemas.TransferDirectoryConf(
81+
name="本地下载",
82+
storage="local",
83+
download_path="/downloads",
84+
),
85+
schemas.TransferDirectoryConf(
86+
name="网盘媒体库",
87+
storage="local",
88+
download_path="/downloads",
89+
library_path="/cloud/media",
90+
library_storage="u115",
91+
),
92+
]
93+
usages = {
94+
"local": _usage_result(1000.0, 400.0),
95+
"u115": _usage_result(500.0, 100.0),
96+
}
97+
98+
with _patch_dirs(dirs), patch.object(
99+
dashboard_endpoint.StorageChain,
100+
"manage_storage",
101+
side_effect=lambda storage, action: usages[storage],
102+
) as mocked_usage:
103+
ret = dashboard_endpoint._build_storage()
104+
105+
assert {call.kwargs["storage"] for call in mocked_usage.call_args_list} == {"local", "u115"}
106+
assert ret.total_storage == 1500.0
107+
assert ret.used_storage == 1000.0
108+
109+
110+
def test_storage_skips_dirs_without_storage_fields():
111+
"""目录未填存储标识时不参与统计。"""
112+
dirs = [
113+
schemas.TransferDirectoryConf(name="空配置", download_path="/downloads"),
114+
schemas.TransferDirectoryConf(name="空媒体库存储", library_path="/media"),
115+
]
116+
117+
with _patch_dirs(dirs), patch.object(
118+
dashboard_endpoint.StorageChain, "manage_storage"
119+
) as mocked_usage:
120+
ret = dashboard_endpoint._build_storage()
121+
122+
mocked_usage.assert_not_called()
123+
assert ret.total_storage == 0
124+
assert ret.used_storage == 0
125+
126+
127+
def test_storage_returns_zero_without_dirs():
128+
"""未配置任何目录时返回 0。"""
129+
with _patch_dirs([]), patch.object(
130+
dashboard_endpoint.StorageChain, "manage_storage"
131+
) as mocked_usage:
132+
ret = dashboard_endpoint._build_storage()
133+
134+
mocked_usage.assert_not_called()
135+
assert ret.total_storage == 0
136+
assert ret.used_storage == 0
137+
138+
139+
def test_storage_ignores_failed_usage_result():
140+
"""存储查询失败时不计入统计。"""
141+
dirs = [
142+
schemas.TransferDirectoryConf(
143+
name="下载目录",
144+
storage="local",
145+
download_path="/downloads",
146+
)
147+
]
148+
149+
with _patch_dirs(dirs), patch.object(
150+
dashboard_endpoint.StorageChain,
151+
"manage_storage",
152+
return_value={"success": False, "message": "该存储类型未启用或不支持此管理动作"},
153+
):
154+
ret = dashboard_endpoint._build_storage()
155+
156+
assert ret.total_storage == 0
157+
assert ret.used_storage == 0

0 commit comments

Comments
 (0)