22import time
33import requests
44import hashlib
5- from github import Github
5+ from github import Github , Auth
6+
7+
8+ REQUEST_TIMEOUT = (10 , 300 ) # (connect timeout, read timeout)
9+ # 上传可能较慢,单独设置更长的读超时
10+ UPLOAD_TIMEOUT = (10 , 900 )
11+ MAX_DOWNLOAD_RETRIES = 3
12+ MAX_UPLOAD_RETRIES = 5
613
714
815def get_github_latest_release ():
9- g = Github ()
16+ # 使用令牌提升速率配额
17+ gh_token = os .environ .get ('GH_TOKEN' ) or os .environ .get ('GITHUB_TOKEN' )
18+ if gh_token :
19+ g = Github (auth = Auth .Token (gh_token ))
20+ else :
21+ g = Github ()
1022 repo = g .get_repo ("xOS/ServerStatus" )
1123 release = repo .get_latest_release ()
1224 if release :
@@ -17,13 +29,24 @@ def get_github_latest_release():
1729 url = asset .browser_download_url
1830 name = asset .name
1931
20- response = requests .get (url )
21- if response .status_code == 200 :
22- with open (name , 'wb' ) as f :
23- f .write (response .content )
24- print (f"Downloaded { name } " )
32+ # 下载资产(流式 + 超时 + 有限重试)
33+ for attempt in range (1 , MAX_DOWNLOAD_RETRIES + 1 ):
34+ try :
35+ with requests .get (url , stream = True , timeout = REQUEST_TIMEOUT ) as r :
36+ if r .status_code == 200 :
37+ with open (name , 'wb' ) as f :
38+ for chunk in r .iter_content (chunk_size = 1024 * 256 ):
39+ if chunk :
40+ f .write (chunk )
41+ print (f"Downloaded { name } " )
42+ break
43+ else :
44+ print (f"Failed to download { name } , status: { r .status_code } , attempt { attempt } /{ MAX_DOWNLOAD_RETRIES } " )
45+ except requests .RequestException as e :
46+ print (f"Download error for { name } : { e } (attempt { attempt } /{ MAX_DOWNLOAD_RETRIES } )" )
47+ time .sleep (2 ** (attempt - 1 ))
2548 else :
26- print (f"Failed to download { name } " )
49+ raise RuntimeError (f"Failed to download { name } after { MAX_DOWNLOAD_RETRIES } attempts " )
2750 file_abs_path = get_abs_path (asset .name )
2851 files .append (file_abs_path )
2952 sync_to_gitee (release .tag_name , release .body , files )
@@ -32,89 +55,134 @@ def get_github_latest_release():
3255
3356
3457def delete_gitee_releases (latest_id , client , uri , token ):
35- get_data = {
36- 'access_token' : token
37- }
58+ # 仅当 latest_id 有效时执行保留最新
59+ if not latest_id :
60+ print ('Skip delete_gitee_releases: latest_id is empty' )
61+ return
3862
63+ params = {'access_token' : token , 'page' : 1 , 'per_page' : 100 }
3964 release_info = []
40- release_response = client .get (uri , json = get_data )
65+ release_response = client .get (uri , params = params , timeout = REQUEST_TIMEOUT )
4166 if release_response .status_code == 200 :
4267 release_info = release_response .json ()
4368 else :
44- print (
45- f"Request failed with status code { release_response . status_code } " )
69+ print (f"List releases failed: { release_response . status_code } { release_response . text } " )
70+ return
4671
4772 release_ids = []
4873 for block in release_info :
4974 if 'id' in block :
5075 release_ids .append (block ['id' ])
5176
5277 print (f'Current release ids: { release_ids } ' )
53- release_ids .remove (latest_id )
54-
55- for id in release_ids :
56- release_uri = f"{ uri } /{ id } "
57- delete_data = {
58- 'access_token' : token
59- }
60- delete_response = client .delete (release_uri , json = delete_data )
78+ if latest_id in release_ids :
79+ release_ids .remove (latest_id )
80+
81+ for rid in release_ids :
82+ release_uri = f"{ uri } /{ rid } "
83+ delete_response = client .delete (release_uri , params = {'access_token' : token }, timeout = REQUEST_TIMEOUT )
6184 if delete_response .status_code == 204 :
62- print (f'Successfully deleted release #{ id } .' )
85+ print (f'Successfully deleted release #{ rid } .' )
6386 else :
64- raise ValueError (
65- f"Request failed with status code { delete_response .status_code } " )
87+ raise ValueError (f"Delete release #{ rid } failed: { delete_response .status_code } { delete_response .text } " )
6688
6789
6890def sync_to_gitee (tag : str , body : str , files : slice ):
6991 release_id = ""
70- owner = " Ten"
71- repo = " ServerStatus"
92+ owner = os . environ . get ( 'GITEE_OWNER' , ' Ten' )
93+ repo = os . environ . get ( 'GITEE_REPO' , ' ServerStatus' )
7294 release_api_uri = f"https://gitee.com/api/v5/repos/{ owner } /{ repo } /releases"
7395 api_client = requests .Session ()
7496 api_client .headers .update ({
7597 'Accept' : 'application/json' ,
76- 'Content-Type' : 'application/json'
98+ # 对于 form 提交不强制指定 JSON 头
7799 })
78100
79101 access_token = os .environ ['GITEE_TOKEN' ]
80- release_data = {
102+ release_form = {
81103 'access_token' : access_token ,
82104 'tag_name' : tag ,
83105 'name' : tag ,
84106 'body' : body ,
85- 'prerelease' : False ,
107+ 'prerelease' : 'false' ,
86108 'target_commitish' : 'master'
87109 }
88- release_api_response = api_client .post (release_api_uri , json = release_data )
110+ # 优先尝试创建(表单提交)
111+ release_api_response = api_client .post (release_api_uri , data = release_form , timeout = REQUEST_TIMEOUT )
89112 if release_api_response .status_code == 201 :
90113 release_info = release_api_response .json ()
91114 release_id = release_info .get ('id' )
92115 else :
93- print (
94- f"Request failed with status code { release_api_response .status_code } " )
116+ print (f"Create release failed: { release_api_response .status_code } { release_api_response .text } " )
117+ # 如果已存在同名 tag,则尝试查找已存在的发布并复用其 id
118+ list_resp = api_client .get (release_api_uri , params = {'access_token' : access_token , 'page' : 1 , 'per_page' : 100 }, timeout = REQUEST_TIMEOUT )
119+ if list_resp .status_code == 200 :
120+ for rel in list_resp .json ():
121+ if rel .get ('tag_name' ) == tag :
122+ release_id = rel .get ('id' )
123+ print (f"Found existing Gitee release id: { release_id } for tag { tag } " )
124+ break
125+ else :
126+ print (f"List releases failed: { list_resp .status_code } { list_resp .text } " )
95127
96128 print (f"Gitee release id: { release_id } " )
129+ if not release_id :
130+ raise RuntimeError (f"No Gitee release id available for tag { tag } . Please ensure repo { owner } /{ repo } exists and token has permission." )
97131 asset_api_uri = f"{ release_api_uri } /{ release_id } /attach_files"
132+ # 列出现有资产,避免重复上传导致 400/405
133+ existing_assets = {}
134+ try :
135+ # 获取单个 release 详情,其中包含 assets 列表
136+ release_detail_resp = api_client .get (f"{ release_api_uri } /{ release_id } " , params = {'access_token' : access_token }, timeout = REQUEST_TIMEOUT )
137+ if release_detail_resp .status_code == 200 :
138+ data = release_detail_resp .json ()
139+ assets = data .get ('assets' ) or []
140+ for a in assets :
141+ name = a .get ('name' )
142+ if name :
143+ existing_assets [name ] = a .get ('id' )
144+ print (f"Existing assets: { list (existing_assets .keys ())} " )
145+ else :
146+ print (f"Get release detail failed: { release_detail_resp .status_code } { release_detail_resp .text } " )
147+ except requests .RequestException as e :
148+ print (f"List assets error: { e } " )
98149
99150 for file_path in files :
151+ file_name = os .path .basename (file_path )
100152 success = False
101-
102- while not success :
103- files = {
104- 'file' : open (file_path , 'rb' )
105- }
106-
107- asset_api_response = requests .post (
108- asset_api_uri , params = {'access_token' : access_token }, files = files )
109-
110- if asset_api_response .status_code == 201 :
111- asset_info = asset_api_response .json ()
112- asset_name = asset_info .get ('name' )
113- print (f"Successfully uploaded { asset_name } !" )
114- success = True
115- else :
116- print (
117- f"Request failed with status code { asset_api_response .status_code } " )
153+ for attempt in range (1 , MAX_UPLOAD_RETRIES + 1 ):
154+ try :
155+ # 如果已存在同名资产,则跳过上传
156+ if file_name in existing_assets :
157+ print (f"Skip upload { file_name } : already exists in release (asset id { existing_assets [file_name ]} )" )
158+ success = True
159+ break
160+ with open (file_path , 'rb' ) as fh :
161+ resp = api_client .post (
162+ asset_api_uri ,
163+ params = {'access_token' : access_token },
164+ files = {'file' : (file_name , fh , 'application/octet-stream' )},
165+ timeout = UPLOAD_TIMEOUT ,
166+ )
167+ if resp .status_code == 201 :
168+ asset_info = resp .json ()
169+ asset_name = asset_info .get ('name' )
170+ print (f"Successfully uploaded { asset_name } !" )
171+ success = True
172+ break
173+ else :
174+ # 某些情况下返回400且文案提示已存在,视为成功
175+ txt = resp .text
176+ if resp .status_code in (400 , 409 , 422 ) and ('已存在' in txt or 'already exists' in txt ):
177+ print (f"Asset { file_name } already exists, treat as success." )
178+ success = True
179+ break
180+ print (f"Upload failed (status { resp .status_code } ) for { file_path } , attempt { attempt } /{ MAX_UPLOAD_RETRIES } . Body: { txt [:256 ]} " )
181+ except requests .RequestException as e :
182+ print (f"Upload error for { file_path } : { e } (attempt { attempt } /{ MAX_UPLOAD_RETRIES } )" )
183+ time .sleep (min (60 , 2 ** (attempt - 1 )))
184+ if not success :
185+ raise RuntimeError (f"Failed to upload { file_path } after { MAX_UPLOAD_RETRIES } attempts" )
118186
119187 # 仅保留最新 Release 以防超出 Gitee 仓库配额
120188 try :
0 commit comments