Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .cnb.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
main:
web_trigger_release:
- docker:
image: python:3.11
stages:
- name: 同步最新 GitHub Release
timeout: 21600000
script: |
python -m pip install --disable-pip-version-check requests==2.32.3
python tools/ci/sync_github_release_to_cnb.py
api_trigger_release:
- docker:
image: python:3.11
stages:
- name: 同步指定 GitHub Release
timeout: 21600000
script: |
test -n "$RELEASE_TAG"
python -m pip install --disable-pip-version-check requests==2.32.3
python tools/ci/sync_github_release_to_cnb.py --release-tag "$RELEASE_TAG"
5 changes: 5 additions & 0 deletions .cnb/web_trigger.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
branch:
- buttons:
- name: 触发上传
description: 上传Release
event: web_trigger_release
37 changes: 37 additions & 0 deletions .github/workflows/build-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -353,3 +353,40 @@ jobs:
gh workflow run --repo $GITHUB_REPOSITORY mirrorchyan-release_note.yml -f tag=${{ needs.build.outputs.tag }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Trigger CNB Release sync
if: ${{ github.repository == 'OneDragon-Anything/OneDragon-ScriptChainer' }}
continue-on-error: true
shell: pwsh
env:
CNB_TOKEN: ${{ secrets.CNB_TOKEN }}
RELEASE_TAG: ${{ needs.build.outputs.version }}
run: |
if ([string]::IsNullOrWhiteSpace($env:CNB_TOKEN)) {
throw 'Missing GitHub Actions secret: CNB_TOKEN'
}

$headers = @{
Accept = 'application/vnd.cnb.api+json'
Authorization = "Bearer $env:CNB_TOKEN"
}
$body = @{
branch = 'main'
event = 'api_trigger_release'
title = "Sync GitHub Release $env:RELEASE_TAG"
env = @{
RELEASE_TAG = $env:RELEASE_TAG
}
} | ConvertTo-Json -Depth 3

$response = Invoke-RestMethod `
-Uri 'https://api.cnb.cool/OneDragon-Anything/OneDragon-ScriptChainer/-/build/start' `
-Method Post `
-Headers $headers `
-ContentType 'application/json' `
-TimeoutSec 60 `
-Body $body
$response | ConvertTo-Json -Depth 5
if ($response.success -ne $true) {
throw "CNB build trigger failed: $($response.message)"
}
23 changes: 23 additions & 0 deletions .github/workflows/sync-cnb.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Sync to CNB

on:
push:
workflow_dispatch:

permissions:
contents: read

jobs:
sync:
if: ${{ github.repository == 'OneDragon-Anything/OneDragon-ScriptChainer' }}
runs-on: ubuntu-latest
steps:
- name: Push to CNB
env:
CNB_TOKEN: ${{ secrets.CNB_GIT_TOKEN }}
run: |
git clone --bare "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git" repository.git
git -C repository.git remote add cnb "https://cnb:${CNB_TOKEN}@cnb.cool/OneDragon-Anything/OneDragon-ScriptChainer.git"
git -C repository.git push --force --prune cnb \
'+refs/heads/*:refs/heads/*' \
'+refs/tags/*:refs/tags/*'
47 changes: 47 additions & 0 deletions config/repository.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
repositories:
primary: github
primary_branch: main
branches:
main:
label: 主分支
desc: ""
options:
github:
label: GitHub
url: "https://github.com/OneDragon-Anything/OneDragon-ScriptChainer.git"
use_proxy: true
cnb:
label: CNB
url: "https://cnb.cool/OneDragon-Anything/OneDragon-ScriptChainer.git"
use_proxy: false
resource_download:
default: github
options:
github:
label: GitHub
value: "https://example.com/resource"
sources:
github:
label: GitHub
use_proxy: true
cnb:
label: CNB
use_proxy: false
recommend:
zh: cnb
en: github
repos:
main:
github: "https://github.com/OneDragon-Anything/OneDragon-ScriptChainer/releases"
cnb: "https://cnb.cool/OneDragon-Anything/OneDragon-ScriptChainer/-/releases"
regions:
china_cnb:
label: 中国 - CNB
repository: cnb
values:
resource_source: cnb
oversea:
label: 海外
repository: github
values:
resource_source: github
1 change: 1 addition & 0 deletions deploy/OneDragon ScriptChainer.spec
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ a = Analysis(
binaries=[],
datas=[
('../config/project.yml', 'resources/config'),
('../config/repository.yml', 'resources/config'),
('../assets', 'resources/assets')
],
hiddenimports=[],
Expand Down
150 changes: 116 additions & 34 deletions src/one_dragon/base/web/common_downloader.py
Original file line number Diff line number Diff line change
@@ -1,39 +1,51 @@
import os
from collections.abc import Callable
from dataclasses import dataclass

from one_dragon.utils import http_utils
from one_dragon.utils.log_utils import log

# 使用 ghproxy 代理的源
GITHUB_SOURCE_ID: str = 'github'
AUTO_FALLBACK_MIN_BYTES_PER_SECOND: float = 100 * 1024


@dataclass(frozen=True, slots=True)
class ResourceDownloadProgress:
"""资源下载的结构化进度。"""

source_id: str = ''
phase: str = 'waiting'
downloaded_bytes: int = 0
total_bytes: int = 0
bytes_per_second: float = 0
progress: float = 0
message: str = ''


class CommonDownloaderParam:

def __init__(
self,
save_file_path: str,
save_file_name: str,
github_release_download_url: str | None = None,
gitee_release_download_url: str | None = None,
mirror_chan_download_url: str | None = None,
download_urls: dict[str, str] | None = None,
check_existed_list: list[str] | None = None,
unzip_dir_path: str | None = None,
):
"""
一个通用下载器 可提供3个下载源 并检查文件是否存在 如果存在则不进行下载
一个通用下载器 可提供多个下载源 并检查文件是否存在 如果存在则不进行下载

Args:
save_file_path (str): 文件保存的路径
save_file_name (str): 文件保存的名称
github_release_download_url (Optional[str], optional): Github Release下载地址. Defaults to None.
gitee_release_download_url (Optional[str], optional): Gitee Release下载地址. Defaults to None.
mirror_chan_download_url (Optional[str], optional): Mirror酱下载地址. Defaults to None.
download_urls (Optional[dict[str, str]], optional): 源ID到下载地址的映射. Defaults to None.
check_existed_list (Optional[list[str]], optional): 需要检查文件是否存在的列表 完整路径的列表. Defaults to None.
unzip_dir_path (Optional[str], optional): 解压目录路径,如果为None则解压到save_file_path. Defaults to None.
"""
self.save_file_path: str = save_file_path
self.save_file_name: str = save_file_name
self.github_release_download_url: str | None = github_release_download_url
self.gitee_release_download_url: str | None = gitee_release_download_url
self.mirror_chan_download_url: str | None = mirror_chan_download_url
self.download_urls: dict[str, str] = {} if download_urls is None else download_urls
self.check_existed_list: list[str] = [] if check_existed_list is None else check_existed_list
self.unzip_dir_path: str | None = unzip_dir_path

Expand All @@ -45,7 +57,7 @@ def __init__(
param: CommonDownloaderParam,
) -> None:
"""
一个通用下载器 可提供3个下载源 并检查文件是否存在 如果存在则不进行下载
一个通用下载器 可提供多个下载源 按候选顺序尝试 并检查文件是否存在 如果存在则不进行下载

Args:
param (CommonDownloaderParam): 下载参数
Expand All @@ -54,39 +66,109 @@ def __init__(

def download(
self,
download_by_github: bool = True,
download_by_gitee: bool = False,
download_by_mirror_chan: bool = False,
source_order: list[str] | None = None,
proxy_url: str | None = None,
ghproxy_url: str | None = None,
skip_if_existed: bool = True,
progress_signal: dict[str, str | None] | None = None,
progress_callback: Callable[[float, str], None] | None = None
progress_callback: Callable[[float, str], None] | None = None,
status_callback: Callable[[ResourceDownloadProgress], None] | None = None,
on_source_success: Callable[[str], None] | None = None,
on_source_failure: Callable[[str], None] | None = None,
fallback_on_slow: bool = False,
) -> bool:
"""
按候选源顺序依次尝试下载 直到成功

Args:
source_order: 候选源ID顺序 None或与下载地址无交集时使用参数中的地址顺序
proxy_url: 个人代理地址
ghproxy_url: ghproxy 代理地址 只对 github 源生效
skip_if_existed: 文件已存在时是否跳过下载
progress_signal: 进度控制信号 signal=cancel 时取消下载
progress_callback: 进度回调
status_callback: 结构化进度回调
on_source_success: 某个源下载成功后的回调 入参为源ID
on_source_failure: 某个源下载失败后的回调 入参为源ID
fallback_on_slow: 是否在候选源持续低速时尝试下一个源

Returns:
bool: 是否下载成功
"""
if skip_if_existed and self.is_file_existed():
return True

download_url: str = ''
if download_by_github and self.param.github_release_download_url is not None:
if ghproxy_url is not None:
download_url=f'{ghproxy_url}/{self.param.github_release_download_url}'
else:
download_url = self.param.github_release_download_url
elif download_by_gitee and self.param.gitee_release_download_url is not None:
download_url = self.param.gitee_release_download_url
elif download_by_mirror_chan and self.param.mirror_chan_download_url is not None:
download_url = self.param.mirror_chan_download_url

if download_url == '':
log.error('没有指定下载方法或对应的下载地址')
candidates: list[str] = []
if source_order is not None:
candidates = [i for i in source_order if i in self.param.download_urls]
if len(candidates) == 0:
candidates = list(self.param.download_urls.keys())

if len(candidates) == 0:
log.error('没有可用的下载源')
if status_callback is not None:
status_callback(ResourceDownloadProgress(phase='failed', message='没有可用的下载源'))
return False

return http_utils.download_file(
download_url=download_url,
save_file_path=os.path.join(self.param.save_file_path, self.param.save_file_name),
proxy=proxy_url,
progress_signal=progress_signal,
progress_callback=progress_callback)
save_file_path = os.path.join(self.param.save_file_path, self.param.save_file_name)
for index, source_id in enumerate(candidates):
download_url = self.param.download_urls[source_id]
if source_id == GITHUB_SOURCE_ID and ghproxy_url is not None:
proxy_prefix = ghproxy_url.rstrip('/')
if proxy_prefix:
download_url = f'{proxy_prefix}/{download_url}'

log.info(f'尝试从 {source_id} 下载 {self.param.save_file_name}')
if status_callback is not None:
status_callback(
ResourceDownloadProgress(
source_id=source_id,
phase='connecting',
message='正在连接',
)
)
success = http_utils.download_file(
download_url=download_url,
save_file_path=save_file_path,
proxy=proxy_url,
min_bytes_per_second=(
AUTO_FALLBACK_MIN_BYTES_PER_SECOND
if fallback_on_slow and index < len(candidates) - 1
else None
),
progress_signal=progress_signal,
progress_callback=progress_callback,
status_callback=(
lambda progress, current_source=source_id: status_callback(
ResourceDownloadProgress(
source_id=current_source,
phase=progress.phase,
downloaded_bytes=progress.downloaded_bytes,
total_bytes=progress.total_bytes,
bytes_per_second=progress.bytes_per_second,
progress=progress.progress,
message=progress.message,
)
)
if status_callback is not None
else None
),
)

if success:
if on_source_success is not None:
on_source_success(source_id)
return True

if progress_signal is not None and progress_signal.get('signal') == 'cancel':
# 用户主动取消 不再尝试其他源
return False

if on_source_failure is not None:
on_source_failure(source_id)
log.warning(f'从 {source_id} 下载失败 尝试下一个源')

return False

def is_file_existed(self) -> bool:
"""
Expand Down
Loading
Loading