fix: use non-paginated Gitea release attachment endpoint

This commit is contained in:
Release Archive
2026-09-22 11:28:35 +08:00
parent e964d7fd1f
commit 4fd166d953
2 changed files with 21 additions and 3 deletions
+10 -3
View File
@@ -103,6 +103,13 @@ class API:
raise SyncError('Attachment download failed after retries') from None raise SyncError('Attachment download failed after retries') from None
time.sleep(2 ** attempt) time.sleep(2 ** attempt)
def list_assets(self, release_id):
# Gitea's assets endpoint is NOT paginated; page/limit are ignored.
data = self.request('GET', f'/releases/{release_id}/assets')
if not isinstance(data, list):
raise SyncError('Expected attachment list')
return data
def digest(path): def digest(path):
h = hashlib.sha256() h = hashlib.sha256()
@@ -151,7 +158,7 @@ def release_body(source, source_url, hashes, source_sha):
def sync_one(src, dst, source, anchor, dry_run, directory): def sync_one(src, dst, source, anchor, dry_run, directory):
tag = source['tag_name'] tag = source['tag_name']
key = urllib.parse.quote(tag, safe='') key = urllib.parse.quote(tag, safe='')
source_assets = src.pages(f'/releases/{source["id"]}/assets') source_assets = src.list_assets(source['id'])
validate_names(source_assets) validate_names(source_assets)
source_tag = src.request('GET', '/tags/' + key) source_tag = src.request('GET', '/tags/' + key)
if not source_tag or not source_tag.get('commit', {}).get('sha'): if not source_tag or not source_tag.get('commit', {}).get('sha'):
@@ -162,7 +169,7 @@ def sync_one(src, dst, source, anchor, dry_run, directory):
p = Path(directory) / ('source-' + str(a['id'])) p = Path(directory) / ('source-' + str(a['id']))
hashes[a['name']] = src.download(a, p) hashes[a['name']] = src.download(a, p)
files[a['name']] = p files[a['name']] = p
fresh = src.pages(f'/releases/{source["id"]}/assets') fresh = src.list_assets(source['id'])
identity = lambda rows: sorted((a['id'], a['name'], a['size']) for a in rows) identity = lambda rows: sorted((a['id'], a['name'], a['size']) for a in rows)
if identity(fresh) != identity(source_assets): if identity(fresh) != identity(source_assets):
raise SyncError('Source attachment list changed during download; retry later') raise SyncError('Source attachment list changed during download; retry later')
@@ -175,7 +182,7 @@ def sync_one(src, dst, source, anchor, dry_run, directory):
result = {'tag': tag, 'source_sha': source_sha, 'created': target is None, 'assets': [], 'metadata_changed': False} result = {'tag': tag, 'source_sha': source_sha, 'created': target is None, 'assets': [], 'metadata_changed': False}
if target is None and not dry_run: if target is None and not dry_run:
target = dst.request('POST', '/releases', dict(desired, tag_name=tag, target_commitish=anchor, draft=True)) target = dst.request('POST', '/releases', dict(desired, tag_name=tag, target_commitish=anchor, draft=True))
assets = dst.pages(f'/releases/{target["id"]}/assets') if target else [] assets = dst.list_assets(target['id']) if target else []
for source_asset in source_assets: for source_asset in source_assets:
name, sha = source_asset['name'], hashes[source_asset['name']] name, sha = source_asset['name'], hashes[source_asset['name']]
existing = next((a for a in assets if a['name'] == name), None) existing = next((a for a in assets if a['name'] == name), None)
+11
View File
@@ -33,6 +33,8 @@ class Fake:
return copy.deepcopy(a) return copy.deepcopy(a)
def pages(self, path): def pages(self, path):
return copy.deepcopy(self.assets) return copy.deepcopy(self.assets)
def list_assets(self, release_id):
return copy.deepcopy(self.assets)
def download(self, a, p): def download(self, a, p):
Path(p).write_bytes(self.data[a['id']]) Path(p).write_bytes(self.data[a['id']])
validate_file(p, a) validate_file(p, a)
@@ -118,6 +120,15 @@ class Tests(unittest.TestCase):
api = API('https://example.invalid', 'a/b', 'secret') api = API('https://example.invalid', 'a/b', 'secret')
with self.assertRaises(SyncError): with self.assertRaises(SyncError):
api.download({'browser_download_url': 'https://other.invalid/file'}, '/tmp/unused') api.download({'browser_download_url': 'https://other.invalid/file'}, '/tmp/unused')
def test_assets_endpoint_is_not_paginated(self):
api = API('https://example.invalid', 'a/b', '')
calls = []
def request(method, path):
calls.append(path)
return [{'id': 1}]
api.request = request
self.assertEqual([{'id': 1}], api.list_assets(2))
self.assertEqual(['/releases/2/assets'], calls)
if __name__ == '__main__': unittest.main() if __name__ == '__main__': unittest.main()