Skip to content

Commit 025a95d

Browse files
committed
feat: 初步支持 S3 储存(什
1 parent b96e084 commit 025a95d

9 files changed

Lines changed: 774 additions & 67 deletions

File tree

core/config.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def load(self):
3939
self.cfg = yaml.load(f.read(), Loader=yaml.FullLoader) or {}
4040

4141
def get(self, key: str, def_: Any = None) -> Any:
42-
value = os.environ.get(key, None) or self._get_value(self.cfg, key.split("."))
42+
value = os.environ.get(key, None) or self._getValue(self.cfg, key.split("."))
4343
if value is None and def_ is None:
4444
print(f"[Config] {key} is not set, does it exist?")
4545
if key in defaults:
@@ -49,23 +49,23 @@ def get(self, key: str, def_: Any = None) -> Any:
4949
return value
5050

5151
def set(self, key: str, value: Any):
52-
self._set_value(self.cfg, key.split("."), value)
52+
self._setValue(self.cfg, key.split("."), value)
5353
self.save()
5454

5555
def save(self):
5656
self.file.parent.mkdir(parents=True, exist_ok=True)
5757
with open(self.file, "w", encoding="utf-8") as f:
5858
yaml.dump(data=self.cfg, stream=f, allow_unicode=True)
5959

60-
def _get_value(self, dict_obj, keys):
60+
def _getValue(self, dict_obj, keys):
6161
for key in keys:
6262
if key in dict_obj:
6363
dict_obj = dict_obj[key]
6464
else:
6565
return None
6666
return dict_obj
6767

68-
def _set_value(self, dict_obj, keys, value):
68+
def _setValue(self, dict_obj, keys, value):
6969
for _, key in enumerate(keys[:-1]):
7070
if key not in dict_obj:
7171
dict_obj[key] = {}

core/router.py

Lines changed: 2 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
from core.orm import writeAgent
2-
from core.config import Config
32
from core.utils import checkSign
43
from core.api import getStatus
54
from core.storages import AListStorage
65
from core.logger import logger
7-
from aiohttp import web, WSMsgType
6+
from aiohttp import web
87
import aiohttp
98
import random
109

@@ -21,7 +20,6 @@ def __init__(self, app: web.Application, cluster) -> None:
2120
self.connection = 0
2221

2322
def init(self) -> None:
24-
logger.add(self.pushLog, level="DEBUG")
2523
@self.route.get("/download/{hash}")
2624
async def _(
2725
request: web.Request,
@@ -87,26 +85,6 @@ async def _(_: web.Request) -> web.Response:
8785
data = await session.get("/openbmclapi/metric/rank")
8886
response = web.json_response(await data.json())
8987
return response
90-
91-
@self.route.get("/ws/logs")
92-
async def _(request: web.Request) -> web.WebSocketResponse:
93-
ws = web.WebSocketResponse()
94-
await ws.prepare(request)
95-
96-
self.ws_clients.append(ws)
97-
logger.debug("WebSocket client connected.")
98-
99-
try:
100-
while True:
101-
msg = await ws.receive()
102-
if msg.type == WSMsgType.TEXT:
103-
pass
104-
105-
except Exception:
106-
pass
107-
finally:
108-
self.ws_clients.remove(ws)
109-
return ws
11088

11189
@self.route.get("/")
11290
async def _(_: web.Request) -> web.HTTPFound:
@@ -119,12 +97,4 @@ async def _(_: web.Request) -> web.FileResponse:
11997

12098
self.route.static("/", "./assets/dashboard")
12199

122-
self.app.add_routes(self.route)
123-
124-
async def pushLog(self, message: str) -> None:
125-
if self.ws_clients:
126-
for ws in self.ws_clients:
127-
try:
128-
await ws.send_str(message)
129-
except Exception:
130-
pass
100+
self.app.add_routes(self.route)

core/storages/alist.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ async def getSize() -> int:
251251
retry=delay,
252252
)
253253
await asyncio.sleep(delay)
254+
logger.terror("storage.error.alist.write_file.failed", file=file.hash)
254255
return False
255256

256257
async def recycleFiles(files):

core/storages/s3.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
from core.classes import Storage, FileInfo, FileList
2+
from core.logger import logger
3+
from botocore.config import Config
4+
from botocore.exceptions import ClientError
5+
from typing import Literal, Union
6+
from tqdm import tqdm
7+
import boto3
8+
import humanize
9+
import io
10+
import asyncio
11+
import secrets
12+
13+
14+
class S3Storage(Storage):
15+
def __init__(
16+
self,
17+
endpoint: str,
18+
access_key_id: str,
19+
secret_access_key: str,
20+
signature_version: str,
21+
bucket: str,
22+
addressing_style: Literal["auto", "path", "virtual"] = "auto",
23+
session_token: Union[None, str] = None,
24+
):
25+
self.client = boto3.client(
26+
"s3",
27+
aws_access_key_id=access_key_id,
28+
aws_secret_access_key=secret_access_key,
29+
aws_session_token=session_token,
30+
endpoint_url=endpoint,
31+
config=Config(s3={"addressing_style": addressing_style}, signature_version=signature_version),
32+
)
33+
self.bucket = bucket
34+
35+
async def init(self) -> None:
36+
pass
37+
38+
async def writeFile(
39+
self, file: FileInfo, content: io.BytesIO, delay: int, retry: int
40+
) -> bool:
41+
file_path = f"{file.hash[:2]}/{file.hash}"
42+
try:
43+
response = self.client.head_object(Bucket=self.bucket, Key=file_path)
44+
if response["ContentLength"] == len(content.getvalue()):
45+
return True
46+
except Exception:
47+
pass
48+
49+
for _ in range(retry):
50+
try:
51+
self.client.put_object(Bucket=self.bucket, Key=file_path, Body=content.getbuffer())
52+
response = self.client.head_object(Bucket=self.bucket, Key=file_path)
53+
uploaded_size = response["ContentLength"]
54+
if uploaded_size == file.size:
55+
return True
56+
else:
57+
logger.terror(
58+
"storage.error.s3.write_file.size_mismatch",
59+
file=file.hash,
60+
file_size=humanize.naturalsize(file.size, binary=True),
61+
actual_file_size=humanize.naturalsize(uploaded_size, binary=True),
62+
)
63+
return False
64+
65+
except ClientError as e:
66+
logger.terror(
67+
"storage.error.s3.write_file.retry",
68+
file=file.hash,
69+
e=e,
70+
retry=delay,
71+
)
72+
73+
await asyncio.sleep(delay)
74+
75+
logger.terror("storage.error.s3.write_file.failed", file=file.hash)
76+
return False
77+
78+
async def check(self) -> None:
79+
file_path = secrets.token_hex(8)
80+
try:
81+
self.client.put_object(Bucket=self.bucket, Key=file_path, Body=b"")
82+
self.client.head_object(Bucket=self.bucket, Key=file_path)
83+
self.client.delete_object(Bucket=self.bucket, Key=file_path)
84+
logger.tsuccess("storage.success.s3.check")
85+
except Exception as e:
86+
logger.terror("storage.error.s3.check", e=e)
87+
88+
async def getMissingFiles(self, files: FileList, pbar: tqdm) -> FileList:
89+
async def checkFile(file: FileInfo, pbar: tqdm) -> bool:
90+
pbar.update(1)
91+
file_path = f"{file.hash[:2]}/{file.hash}"
92+
try:
93+
response = self.client.head_object(Bucket=self.bucket, Key=file_path)
94+
s3_file_size = response["ContentLength"]
95+
return s3_file_size != file.size
96+
except Exception:
97+
pass
98+
99+
results = await asyncio.gather(*[checkFile(file, pbar) for file in files.files])
100+
missing_files = [
101+
file for file, is_missing in zip(files.files, results) if is_missing
102+
]
103+
return FileList(files=missing_files)

dashboard/src/api/index.ts

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -50,26 +50,3 @@ export async function fetchRank() {
5050
return res.data
5151
}
5252

53-
function getWebSocketURL(path: string) {
54-
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
55-
const host = window.location.host
56-
return `${protocol}//${host}${path}`
57-
}
58-
59-
const ws = new WebSocket(getWebSocketURL('/ws/logs'))
60-
61-
ws.onmessage = function(event) {
62-
console.log('Received log:', event.data)
63-
}
64-
65-
ws.onopen = function() {
66-
console.log("WebSocket connection established.")
67-
}
68-
69-
ws.onclose = function() {
70-
console.log("WebSocket connection closed.")
71-
}
72-
73-
ws.onerror = function(error) {
74-
console.error("WebSocket error:", error)
75-
}

dashboard/src/component/ChartsComponent.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ const formatDays = (day: number) => {
4040
return ''
4141
}
4242
const date = new Date(Date.UTC(currentTime.value.year, currentTime.value.month + 1, day))
43-
return `${date.getMonth()} 月 ${date.getDate()} 日`
43+
return `${date.getMonth() == 0 ? 12 : date.getMonth()} 月 ${date.getDate()} 日`
4444
}
4545
4646
const formatMonths = (month: number) => {

i18n/zh_cn.json

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,26 @@
1515
"storage.success.local.no_need_to_recycle": "当前无需要回收的文件。",
1616
"storage.error.local.check": "本地储存测试异常:${e}。",
1717
"storage.error.local.get_missing": "在尝试获取缺失文件列表时发生错误:${e}",
18-
"storage.error.local.write_file.retry": "在尝试写入本地文件 ${file} 时遇到错误:${e},将在 ${retry}s 后重试。",
19-
"storage.error.local.write_file.failed": "无法写入本地文件 ${file},已达到最高重试次数。",
20-
"storage.error.local.write_file.size_mismatch": "无法校验文件 ${file} 的大小。理论值:${file_size},实际值:${actual_file_size}。",
18+
"storage.error.local.write_file.retry": "在尝试写入本地储存文件 ${file} 时遇到错误:${e},将在 ${retry}s 后重试。",
19+
"storage.error.local.write_file.failed": "无法写入本地储存文件 ${file},已达到最高重试次数。",
20+
"storage.error.local.write_file.size_mismatch": "无法校验本地储存文件 ${file} 的大小。理论值:${file_size},实际值:${actual_file_size}。",
2121
"storage.success.get_missing": "成功获取缺失文件列表!缺失文件总数量:${count},缺失文件总大小:${size}。",
2222
"storage.info.alist.fetch_token": "正在获取 AList Token……",
2323
"storage.success.alist.fetch_token": "成功获取 AList Token!有效期:48 小时。",
2424
"storage.error.alist.fetch_token": "无法获取 AList Token:${e}。",
2525
"storage.success.alist.check": "AList 储存测试成功!",
2626
"storage.error.alist.check": "AList 储存测试失败:${e}。",
27-
"storage.error.alist.write_file.retry": "在尝试写入 AList 文件 ${file} 时遇到错误:${e},将在 ${retry}s 后重试。",
28-
"storage.error.alist.write_file.size_mismatch": "无法校验文件 ${file} 的大小。理论值:${file_size},实际值:${actual_file_size}。",
27+
"storage.error.alist.write_file.retry": "在尝试写入 AList 储存文件 ${file} 时遇到错误:${e},将在 ${retry}s 后重试。",
28+
"storage.error.alist.write_file.size_mismatch": "无法校验 AList 储存文件 ${file} 的大小。理论值:${file_size},实际值:${actual_file_size}。",
29+
"storage.error.alist.write_file.failed": "无法写入 AList 储存文件 ${file},已达到最高重试次数。",
2930
"storage.error.alist.upload": "无法上传测速文件:${e}。",
3031
"storage.error.alist.measure": "无法服务测速文件:${e}。",
3132
"storage.tqdm.alist.get_filelist": "获取 AList 储存文件列表中",
33+
"storage.error.s3.check": "S3 储存测试失败:${e}。",
34+
"storage.success.s3.check": "S3 储存测试成功!",
35+
"storage.error.s3.write_file.size_mismatch": "无法校验 S3 储存文件 ${file} 的大小。理论值:${file_size},实际值:${actual_file_size}。",
36+
"storage.error.s3.write_file.retry": "在尝试写入 S3 储存文件 ${file} 时遇到错误:${e},将在 ${retry}s 后重试。",
37+
"storage.error.s3.write_file.failed": "无法写入 S3 储存文件 ${file},已达到最高重试次数。",
3238
"i18n.prompt.failed": "(i18n 字符串解析失败)",
3339
"cluster.info.filelist.fetching": "正在获取文件列表……",
3440
"cluster.success.filelist.fetched": "成功获取文件列表!",

poetry.lock

Lines changed: 647 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,18 @@ humanize = "^4.10.0"
2020
python-socketio = "^5.11.3"
2121
sqlalchemy = "^2.0.32"
2222
psutil = "^6.0.0"
23+
boto3 = "^1.35.90"
2324

2425

2526
[[tool.poetry.source]]
2627
name = "mirrors"
2728
url = "https://pypi.tuna.tsinghua.edu.cn/simple/"
2829
priority = "primary"
2930

31+
32+
[tool.poetry.group.dev.dependencies]
33+
boto3-stubs = {version = "1.35.90", extras = ["s3"]}
34+
3035
[build-system]
3136
requires = ["poetry-core"]
3237
build-backend = "poetry.core.masonry.api"

0 commit comments

Comments
 (0)