From 13366ab3b4ec2d2e39cbb9ec7ada046c7f352a1c Mon Sep 17 00:00:00 2001 From: shenlei Date: Mon, 20 Jul 2026 17:09:28 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20OSS=20=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E4=B8=8E=E5=8E=86=E5=8F=B2=E7=AD=9B=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/routers/tasks.py | 6 ++- backend/services/distribution.py | 51 +++++++++++++++++++--- frontend/src/__tests__/HistoryView.test.js | 12 ++--- frontend/src/views/HistoryView.vue | 15 +++---- tests/test_api_tasks.py | 4 +- tests/test_distribution.py | 22 ++++++++++ 6 files changed, 88 insertions(+), 22 deletions(-) diff --git a/backend/routers/tasks.py b/backend/routers/tasks.py index 4538e01..cf5ca80 100644 --- a/backend/routers/tasks.py +++ b/backend/routers/tasks.py @@ -180,7 +180,11 @@ async def delete_task(task_id: str, db: Session = Depends(get_db)): build_config = json.loads(task.config_json) from .config import load_config from ..services.distribution import DistributionError, delete_published_artifacts - delete_published_artifacts(build_config, load_config().get("upload", {})) + delete_published_artifacts( + build_config, + load_config().get("upload", {}), + task.oss_url, + ) except (json.JSONDecodeError, DistributionError) as exc: raise HTTPException(status_code=502, detail=f"远端产物删除失败,记录未删除:{exc}") from exc diff --git a/backend/services/distribution.py b/backend/services/distribution.py index 3c93b43..b81a3fb 100644 --- a/backend/services/distribution.py +++ b/backend/services/distribution.py @@ -5,7 +5,7 @@ import posixpath import re import shutil from pathlib import Path -from urllib.parse import quote +from urllib.parse import quote, unquote, urlparse import httpx @@ -115,16 +115,53 @@ def _delete_oss(config: dict, remote_paths: list[str]): raise DistributionError(f"OSS 删除失败: {relative_path}") -def delete_published_artifacts(build_config: dict, upload_config: dict): - """删除任务对应的远端分发产物。""" +def _remote_paths_from_published_url( + build_config: dict, upload_config: dict, published_url: str, +) -> list[str] | None: + """从已保存的公开链接还原实际对象键,兼容历史命名规则。""" + parsed = urlparse(published_url) + if not parsed.path: + return None + + remote_path = unquote(parsed.path).lstrip("/") + # base_url 允许带路径前缀;该前缀是公开地址的一部分,不属于 OSS 对象键。 + base_url = upload_config.get("oss", {}).get("base_url", "") + base = urlparse(base_url) + if base_url and base.netloc == parsed.netloc: + base_path = unquote(base.path).strip("/") + if base_path and remote_path.startswith(f"{base_path}/"): + remote_path = remote_path[len(base_path) + 1:] + + path = Path(remote_path) + if path.suffix not in {".ipa", ".plist", ".html", ".png"}: + return None + if build_config.get("BUILD_TYPE") == "App_Store": + return [str(path.with_suffix(".ipa"))] + return [str(path.with_suffix(suffix)) for suffix in (".ipa", ".plist", ".html", ".png")] + + +def delete_published_artifacts( + build_config: dict, upload_config: dict, published_url: str = "", +): + """删除任务对应的远端分发产物。 + + 优先按任务保存的下载链接还原对象键。这样即使后续升级了文件命名 + 规则,或历史快照含有新的分支字段,仍会删除当时实际上传的文件。 + """ mode = upload_config.get("mode", "") if mode not in {"oss", "webdav"}: raise DistributionError("请选择 OSS 或 WebDAV 上传方式") - ipa_remote, manifest_remote, html_remote, qr_remote = _remote_paths(build_config) - remote_paths = [ipa_remote] - if build_config.get("BUILD_TYPE") != "App_Store": - remote_paths.extend([manifest_remote, html_remote, qr_remote]) + remote_paths = None + if mode == "oss" and published_url: + remote_paths = _remote_paths_from_published_url( + build_config, upload_config, published_url, + ) + if not remote_paths: + ipa_remote, manifest_remote, html_remote, qr_remote = _remote_paths(build_config) + remote_paths = [ipa_remote] + if build_config.get("BUILD_TYPE") != "App_Store": + remote_paths.extend([manifest_remote, html_remote, qr_remote]) deleter = _delete_oss if mode == "oss" else _delete_webdav deleter(upload_config, remote_paths) diff --git a/frontend/src/__tests__/HistoryView.test.js b/frontend/src/__tests__/HistoryView.test.js index 2cc5256..b133e78 100644 --- a/frontend/src/__tests__/HistoryView.test.js +++ b/frontend/src/__tests__/HistoryView.test.js @@ -78,7 +78,7 @@ describe('HistoryView.vue', () => { expect(wrapper.text()).toContain('等待中') }) - it('按打包类型过滤', async () => { + it('版本号筛选为下拉选项', async () => { const router = createMockRouter() const wrapper = mount(HistoryView, { global: { plugins: [router] }, @@ -86,7 +86,9 @@ describe('HistoryView.vue', () => { await flushPromises() const selects = wrapper.findAll('.filter-select') - await selects[2].setValue('Ad_Hoc') + expect(selects).toHaveLength(3) + expect(wrapper.text()).not.toContain('全部类型') + await selects[1].setValue('2.196.0') await wrapper.vm.$nextTick() const rows = wrapper.findAll('tbody tr') @@ -101,14 +103,14 @@ describe('HistoryView.vue', () => { await flushPromises() const selects = wrapper.findAll('.filter-select') - await selects[3].setValue('failed') + await selects[2].setValue('failed') await wrapper.vm.$nextTick() const rows = wrapper.findAll('tbody tr') expect(rows.length).toBe(1) // App2 }) - it('按 App 名称和版本号筛选', async () => { + it('按 App 名称和版本号下拉选项筛选', async () => { const router = createMockRouter() const wrapper = mount(HistoryView, { global: { plugins: [router] }, @@ -117,7 +119,7 @@ describe('HistoryView.vue', () => { const selects = wrapper.findAll('.filter-select') await selects[0].setValue('App1') - await wrapper.find('.version-filter').setValue('2.196') + await wrapper.find('.version-filter').setValue('2.196.0') await wrapper.vm.$nextTick() const rows = wrapper.findAll('tbody tr') diff --git a/frontend/src/views/HistoryView.vue b/frontend/src/views/HistoryView.vue index 180f858..b284cb4 100644 --- a/frontend/src/views/HistoryView.vue +++ b/frontend/src/views/HistoryView.vue @@ -8,11 +8,9 @@ - - + +