fix: keep release downloads on configured origin
This commit is contained in:
@@ -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
|
||||
)
|
||||
```
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 -
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user