fix: keep release downloads on configured origin

This commit is contained in:
Qiufeng
2026-08-05 06:23:39 +08:00
parent 5d7112ce31
commit a4924e3a2c
9 changed files with 76 additions and 47 deletions
+1 -1
View File
@@ -211,7 +211,7 @@ curl -fsSL https://git.awaioi.com/awaioi/ERP/raw/branch/main/install.sh | bash
tmp="$(mktemp)"
trap 'rm -f -- "$tmp"' EXIT
curl -fsSL https://git.awaioi.com/awaioi/ERP/raw/tag/v0.3.8/install.sh -o "$tmp"
printf '%s %s\n' '716f6a9ea75659be3c7162d031f1b1fe5e8aa0e82e0101296153842dff9b35d6' "$tmp" | sha256sum -c -
printf '%s %s\n' 'ae6a6613d4abe37ba24b41e1901eaa3b29cc32939583302c7f62277922fbbe9b' "$tmp" | sha256sum -c -
sudo -E bash "$tmp" --version 0.3.8
)
```
+15 -7
View File
@@ -220,12 +220,15 @@ if channel.lower() == "stable" and release.get("prerelease"):
raise SystemExit("prerelease rejected on stable channel")
if channel.lower() == "stable" and "-" in version.split("+", 1)[0]:
raise SystemExit("prerelease tag rejected on stable channel")
assets = {str(item.get("name")): str(item.get("browser_download_url") or "") for item in release.get("assets", [])}
assets = {
str(item.get("name") or "")
for item in release.get("assets", [])
if isinstance(item, dict)
}
names = [f"kaidi-erp-{version}.tar.gz", "SHA256SUMS", "SHA256SUMS.sig"]
urls = [assets.get(name, "") for name in names]
if any(not value or any(ch.isspace() for ch in value) for value in urls):
if any(name not in assets for name in names):
raise SystemExit("release assets are incomplete")
print("\t".join([version, *urls]))
print("\t".join([version, tag]))
PY
}
@@ -371,7 +374,8 @@ install_release() {
|| fail "健康检查超时时间配置无效"
[[ "$HEALTH_POLL_SECONDS" =~ ^[1-9][0-9]*$ ]] \
|| fail "健康检查间隔配置无效"
[[ "$REPOSITORY" =~ ^[^/[:space:]]+/[^/[:space:]]+$ ]] || fail "更新仓库配置无效"
[[ "$REPOSITORY" =~ ^[A-Za-z0-9][A-Za-z0-9._-]{0,99}/[A-Za-z0-9][A-Za-z0-9._-]{0,99}$ ]] \
|| fail "更新仓库配置无效"
require_command curl
require_command python3
require_command tar
@@ -386,9 +390,13 @@ install_release() {
local selection="$TMP_DIR/selection"
select_release "$release_json" "$requested" "$selection" || fail "Release 元数据验证失败" 10
local archive_url sums_url signature_url
IFS=$'\t' read -r TARGET_VERSION archive_url sums_url signature_url < "$selection"
local release_tag release_base archive_url sums_url signature_url
IFS=$'\t' read -r TARGET_VERSION release_tag < "$selection"
local archive_name="kaidi-erp-${TARGET_VERSION}.tar.gz"
release_base="${GITEA_BASE_URL%/}/$owner/$repo/releases/download/$release_tag"
archive_url="$release_base/$archive_name"
sums_url="$release_base/SHA256SUMS"
signature_url="$release_base/SHA256SUMS.sig"
local archive="$TMP_DIR/$archive_name" sums="$TMP_DIR/SHA256SUMS" signature="$TMP_DIR/SHA256SUMS.sig"
write_state DOWNLOADING 20 "正在下载版本 ${TARGET_VERSION}" "$TARGET_VERSION"
+1 -1
View File
@@ -59,7 +59,7 @@ Linux 生产服务要求主机使用 systemd;没有 systemd 的容器、WSL
tmp="$(mktemp)"
trap 'rm -f -- "$tmp"' EXIT
curl -fsSL https://git.awaioi.com/awaioi/ERP/raw/tag/v0.3.8/install.sh -o "$tmp"
printf '%s %s\n' '716f6a9ea75659be3c7162d031f1b1fe5e8aa0e82e0101296153842dff9b35d6' "$tmp" | sha256sum -c -
printf '%s %s\n' 'ae6a6613d4abe37ba24b41e1901eaa3b29cc32939583302c7f62277922fbbe9b' "$tmp" | sha256sum -c -
sudo -E bash "$tmp" --version 0.3.8
)
```
+16 -7
View File
@@ -200,7 +200,8 @@ download() {
}
download_release() {
[[ "$REPOSITORY" =~ ^[^/[:space:]]+/[^/[:space:]]+$ ]] || fail 'invalid Gitea repository'
[[ "$REPOSITORY" =~ ^[A-Za-z0-9][A-Za-z0-9._-]{0,99}/[A-Za-z0-9][A-Za-z0-9._-]{0,99}$ ]] \
|| fail 'invalid Gitea repository'
local owner="${REPOSITORY%%/*}" repo="${REPOSITORY#*/}"
local release_json="$TMP_DIR/release.json"
download "${GITEA_BASE_URL%/}/api/v1/repos/$owner/$repo/releases/latest" "$release_json" \
@@ -223,17 +224,20 @@ if "-" in version.split("+", 1)[0]:
requested = sys.argv[2].removeprefix("v")
if requested and requested != version:
raise SystemExit("requested version does not match latest release")
assets = {str(a.get("name")): str(a.get("browser_download_url") or "") for a in release.get("assets", [])}
assets = {
str(a.get("name") or "")
for a in release.get("assets", [])
if isinstance(a, dict)
}
names = [
f"kaidi-erp-{version}.tar.gz",
f"kaidi-erp-installer-{version}.jar",
"SHA256SUMS",
"SHA256SUMS.sig",
]
urls = [assets.get(name, "") for name in names]
if any(not url or any(ch.isspace() for ch in url) for url in urls):
if any(name not in assets for name in names):
raise SystemExit("release assets missing")
print("\t".join([version, *urls]))
print("\t".join([version, tag]))
PY
then
local reason="invalid Release metadata"
@@ -241,10 +245,15 @@ PY
fail "$reason"
fi
local archive_url installer_url sums_url signature_url
IFS=$'\t' read -r VERSION archive_url installer_url sums_url signature_url < "$selection"
local release_tag release_base archive_url installer_url sums_url signature_url
IFS=$'\t' read -r VERSION release_tag < "$selection"
ARCHIVE_NAME="kaidi-erp-${VERSION}.tar.gz"
INSTALLER_NAME="kaidi-erp-installer-${VERSION}.jar"
release_base="${GITEA_BASE_URL%/}/$owner/$repo/releases/download/$release_tag"
archive_url="$release_base/$ARCHIVE_NAME"
installer_url="$release_base/$INSTALLER_NAME"
sums_url="$release_base/SHA256SUMS"
signature_url="$release_base/SHA256SUMS.sig"
ARCHIVE_PATH="$TMP_DIR/$ARCHIVE_NAME"
INSTALLER_PATH="$TMP_DIR/$INSTALLER_NAME"
download "$archive_url" "$ARCHIVE_PATH"
@@ -50,7 +50,6 @@ public class SystemUpdateService {
private static final int MAX_RELEASE_HISTORY = 20;
private static final int MAX_RELEASE_ASSETS = 32;
private static final int MAX_ASSET_NAME_CHARS = 255;
private static final int MAX_ASSET_URL_CHARS = 4_096;
private final UpdateProperties properties;
private final ObjectMapper objectMapper;
@@ -266,6 +265,16 @@ public class SystemUpdateService {
+ encode(repository[1]) + "/releases" + suffix);
}
private URI releaseAssetUri(String tag, String name) {
String[] repository = properties.getRepository().split("/", 2);
if (repository.length != 2 || repository[0].isBlank() || repository[1].isBlank()) {
throw new ApiException(500, "更新仓库配置无效");
}
String base = properties.getGiteaBaseUrl().replaceAll("/+$", "");
return URI.create(base + "/" + encode(repository[0]) + "/" + encode(repository[1])
+ "/releases/download/" + encode(tag) + "/" + encode(name));
}
private JsonNode fetchReleaseJson(URI uri) {
HttpRequest.Builder builder = HttpRequest.newBuilder(uri)
.timeout(Duration.ofSeconds(Math.max(1, properties.getRequestTimeoutSeconds())))
@@ -327,15 +336,12 @@ public class SystemUpdateService {
List<ReleaseAsset> assets = new ArrayList<>();
for (JsonNode node : root.path("assets")) {
String name = text(node, "name");
String downloadUrl = text(node, "browser_download_url");
if (name.isBlank() || name.length() > MAX_ASSET_NAME_CHARS
|| downloadUrl.isBlank() || downloadUrl.length() > MAX_ASSET_URL_CHARS) {
if (name.isBlank() || name.length() > MAX_ASSET_NAME_CHARS) {
throw new ApiException(502, "Release 文件信息无效");
}
validateAssetUrl(downloadUrl);
assets.add(new ReleaseAsset(
name,
downloadUrl,
releaseAssetUri(tag, name).toString(),
node.path("size").asLong(0)
));
}
@@ -43,8 +43,7 @@ class SystemUpdateServiceTest {
void checksLatestGiteaReleaseAndRequiresSignedAssetSet() throws Exception {
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
server.createContext("/api/v1/repos/awaioi/ERP/releases/latest", exchange -> {
String origin = "http://127.0.0.1:" + exchange.getLocalAddress().getPort();
byte[] body = ("""
byte[] body = """
{
"tag_name": "v0.2.0",
"body": "PostgreSQL production release",
@@ -52,12 +51,12 @@ class SystemUpdateServiceTest {
"prerelease": false,
"published_at": "2026-08-03T10:00:00Z",
"assets": [
{"name":"kaidi-erp-0.2.0.tar.gz","browser_download_url":"%s/app","size":123},
{"name":"SHA256SUMS","browser_download_url":"%s/sums","size":64},
{"name":"SHA256SUMS.sig","browser_download_url":"%s/sig","size":64}
{"name":"kaidi-erp-0.2.0.tar.gz","browser_download_url":"http://legacy.invalid/app","size":123},
{"name":"SHA256SUMS","browser_download_url":"http://legacy.invalid/sums","size":64},
{"name":"SHA256SUMS.sig","browser_download_url":"http://legacy.invalid/sig","size":64}
]
}
""").formatted(origin, origin, origin).getBytes(StandardCharsets.UTF_8);
""".getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().add("Content-Type", "application/json");
exchange.sendResponseHeaders(200, body.length);
exchange.getResponseBody().write(body);
@@ -75,6 +74,9 @@ class SystemUpdateServiceTest {
assertThat(status.latestVersion()).isEqualTo("0.2.0");
assertThat(status.assets()).extracting(SystemUpdateService.ReleaseAsset::name)
.containsExactly("kaidi-erp-0.2.0.tar.gz", "SHA256SUMS", "SHA256SUMS.sig");
assertThat(status.assets()).extracting(SystemUpdateService.ReleaseAsset::downloadUrl)
.allMatch(url -> url.startsWith("http://127.0.0.1:" + server.getAddress().getPort()
+ "/awaioi/ERP/releases/download/v0.2.0/"));
} finally {
server.stop(0);
}
@@ -198,7 +200,7 @@ class SystemUpdateServiceTest {
}
@Test
void rejectsReleaseAssetsFromAnotherOrigin() throws Exception {
void ignoresReleaseAssetOriginsAndBuildsTrustedDownloadUrls() throws Exception {
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
server.createContext("/api/v1/repos/awaioi/ERP/releases/latest", exchange -> {
byte[] body = """
@@ -214,9 +216,11 @@ class SystemUpdateServiceTest {
server.start();
try {
SystemUpdateService service = service(configuredProperties(server.getAddress().getPort()), "0.3.0");
assertThatThrownBy(service::check)
.isInstanceOf(ApiException.class)
.hasMessageContaining("未受信任");
SystemUpdateService.UpdateStatus status = service.check();
assertThat(status.assets()).extracting(SystemUpdateService.ReleaseAsset::downloadUrl)
.allMatch(url -> url.startsWith("http://127.0.0.1:" + server.getAddress().getPort()
+ "/awaioi/ERP/releases/download/v0.4.0/"));
} finally {
server.stop(0);
}
+1 -1
View File
@@ -3,7 +3,7 @@ set -Eeuo pipefail
TARGET=/root/kaidi-erp-reinstall-v038.sh
URL=https://git.awaioi.com/awaioi/ERP/raw/tag/v0.3.8/reinstall-centos9.sh
SHA256=77c21df88b23596ce0b0ffd40c1eb341c7a0653871cdd359ef13af22c6ffe8c4
SHA256=a8496345fa5aaf6fb55180805098fb0e05bdc68b21c455b1c0b1901bba57fedd
curl -fsSL --connect-timeout 15 --max-time 300 "$URL" -o "$TARGET"
printf '%s %s\n' "$SHA256" "$TARGET" | sha256sum -c -
+1 -1
View File
@@ -5,7 +5,7 @@ GITEA_BASE_URL="https://git.awaioi.com"
REPOSITORY="awaioi/ERP"
VERSION="0.3.8"
TAG="v${VERSION}"
INSTALL_SHA256="716f6a9ea75659be3c7162d031f1b1fe5e8aa0e82e0101296153842dff9b35d6"
INSTALL_SHA256="ae6a6613d4abe37ba24b41e1901eaa3b29cc32939583302c7f62277922fbbe9b"
UNINSTALL_SHA256="98c56fed2fd4d01874e4ab5a1a4f3ec42ec3e29b315ffd87385a95488d587546"
PG_ROOT="${ERP_RECOVERY_PG_ROOT:-/www/server/pgsql}"
PG_DATA="${ERP_RECOVERY_PG_DATA:-/www/server/pgsql/data}"
+15 -13
View File
@@ -323,22 +323,23 @@ class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/api/v1/repos/awaioi/ERP/releases/latest":
base = f"http://127.0.0.1:{self.server.server_port}/assets"
legacy = "http://legacy.invalid/assets"
payload = {
"tag_name": "v" + version,
"draft": False,
"prerelease": False,
"assets": [
{"name": archive, "browser_download_url": base + "/" + archive},
{"name": f"kaidi-erp-installer-{version}.jar", "browser_download_url": base + f"/kaidi-erp-installer-{version}.jar"},
{"name": "SHA256SUMS", "browser_download_url": base + "/SHA256SUMS"},
{"name": "SHA256SUMS.sig", "browser_download_url": base + "/SHA256SUMS.sig"},
{"name": archive, "browser_download_url": legacy + "/" + archive},
{"name": f"kaidi-erp-installer-{version}.jar", "browser_download_url": legacy + f"/kaidi-erp-installer-{version}.jar"},
{"name": "SHA256SUMS", "browser_download_url": legacy + "/SHA256SUMS"},
{"name": "SHA256SUMS.sig", "browser_download_url": legacy + "/SHA256SUMS.sig"},
],
}
self.send_bytes(200, json.dumps(payload).encode(), "application/json")
return
if self.path.startswith("/assets/"):
name = self.path.removeprefix("/assets/")
release_prefix = f"/awaioi/ERP/releases/download/v{version}/"
if self.path.startswith(release_prefix):
name = self.path.removeprefix(release_prefix)
if name not in {archive, f"kaidi-erp-installer-{version}.jar", "SHA256SUMS", "SHA256SUMS.sig"}:
self.send_bytes(404, b"not found", "text/plain")
return
@@ -437,15 +438,15 @@ class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/api/v1/repos/awaioi/ERP/releases/latest":
base = f"http://127.0.0.1:{self.server.server_port}"
legacy = "http://legacy.invalid"
payload = {
"tag_name": "v2.0.0",
"draft": False,
"prerelease": False,
"assets": [
{"name": "kaidi-erp-2.0.0.tar.gz", "browser_download_url": base + "/assets/kaidi-erp-2.0.0.tar.gz"},
{"name": "SHA256SUMS", "browser_download_url": base + "/assets/SHA256SUMS"},
{"name": "SHA256SUMS.sig", "browser_download_url": base + "/assets/SHA256SUMS.sig"},
{"name": "kaidi-erp-2.0.0.tar.gz", "browser_download_url": legacy + "/assets/kaidi-erp-2.0.0.tar.gz"},
{"name": "SHA256SUMS", "browser_download_url": legacy + "/assets/SHA256SUMS"},
{"name": "SHA256SUMS.sig", "browser_download_url": legacy + "/assets/SHA256SUMS.sig"},
],
}
self.send_bytes(200, json.dumps(payload).encode(), "application/json")
@@ -455,8 +456,9 @@ class Handler(BaseHTTPRequestHandler):
healthy = current == "2.0.0" if mode == "success" else current == "1.0.0"
self.send_bytes(200 if healthy else 503, b'{"status":"UP"}' if healthy else b'{"status":"DOWN"}', "application/json")
return
if self.path.startswith("/assets/"):
name = self.path.removeprefix("/assets/")
release_prefix = "/awaioi/ERP/releases/download/v2.0.0/"
if self.path.startswith(release_prefix):
name = self.path.removeprefix(release_prefix)
if name not in {"kaidi-erp-2.0.0.tar.gz", "SHA256SUMS", "SHA256SUMS.sig"}:
self.send_bytes(404, b"not found", "text/plain")
return