TASK-070: remove the old download page's publishing script
The old GitHub site is no longer used; releases and the version file live on the apk branch of the team's Gitea. Agent: main (opus) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
dd90f6932b
commit
85352d3a1c
@ -1542,3 +1542,13 @@ This is the append-only implementation and handoff record shared by Codex and Cl
|
||||
`apk` branch has the same SHA-256 as the one built here.
|
||||
- Caution: published without having been seen on a device, at the product owner's decision. The
|
||||
APK is the debug build signed with this PC's key.
|
||||
|
||||
## 2026-10-12 (continued) — TASK-070 follow-up: the old site is out of the repository
|
||||
|
||||
- Request: the product owner will not use the old GitHub site any more and asked for the version
|
||||
check to live on Gitea too.
|
||||
- What changed: the version check already reads Gitea since 0.2.2, so nothing in the app changes.
|
||||
`tools/publish-download.py`, the publisher for the old page, is removed; TASK-066 is marked
|
||||
superseded. Publishing is the three steps in `tasks/070-update-from-gitea.md`.
|
||||
- Caution: the page at `choimy0204.github.io/ubgitea/` still exists (password, 0.2.0). Copies of
|
||||
the app older than 0.2.2 keep reading it and see "up to date".
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# TASK-066: download page and in-app update check
|
||||
|
||||
Status: done
|
||||
Status: done; superseded by TASK-070 (the page and its publishing script are no longer used)
|
||||
Owner: Claude
|
||||
Reviewer: human product owner
|
||||
|
||||
|
||||
@ -19,14 +19,19 @@ owner no longer uses that page (2026-10-12) and asked for the app to use the Git
|
||||
## Scope
|
||||
|
||||
- Included: the two addresses, the wording, `version.json` on the `apk` branch.
|
||||
- Excluded: `tools/publish-download.py` and the `choimy0204.github.io/ubgitea/` page. Both are left
|
||||
as they are and are no longer part of publishing.
|
||||
- Follow-up (product owner, 2026-10-12: the old site is not used any more):
|
||||
`tools/publish-download.py`, which built the old page, is removed. Nothing in the repository
|
||||
points at the old site now.
|
||||
- Excluded: the `choimy0204.github.io/ubgitea/` page itself. It is in another repository and is
|
||||
left as it is.
|
||||
|
||||
## Writable paths
|
||||
|
||||
- `app/src/main/java/com/ubisam/ubgitea/core/update/AppUpdate.kt`
|
||||
- `app/src/main/java/com/ubisam/ubgitea/MainActivity.kt`
|
||||
- `app/src/main/res/values/strings_update.xml`
|
||||
- `tools/publish-download.py` (removed)
|
||||
- `tasks/066-download-page-and-update-check.md`
|
||||
- `tasks/070-update-from-gitea.md`
|
||||
- `docs/development-log.md`
|
||||
|
||||
|
||||
@ -1,187 +0,0 @@
|
||||
"""Builds the password-protected download page for one APK (TASK-066).
|
||||
|
||||
The APK is encrypted with AES-256-GCM under a key derived from the password with PBKDF2-SHA256,
|
||||
so the files can sit on a public static host: the page asks for the password and decrypts in the
|
||||
browser with WebCrypto. version.json beside it is plain; the app reads it to know a newer build
|
||||
is out.
|
||||
|
||||
python tools/publish-download.py <apk> <out-dir> --notes "what changed"
|
||||
|
||||
The password is read from UBGITEA_DOWNLOAD_PASSWORD, or asked for. It is never written anywhere.
|
||||
versionCode and versionName are read from app/build.gradle.kts.
|
||||
"""
|
||||
import argparse
|
||||
import base64
|
||||
import getpass
|
||||
import hashlib
|
||||
import html
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||
|
||||
ITERATIONS = 310_000
|
||||
ENCRYPTED_NAME = "ubgitea.apk.enc"
|
||||
CHECK_TEXT = b"UBGitea"
|
||||
|
||||
|
||||
def gradle_version(root: Path):
|
||||
text = (root / "app" / "build.gradle.kts").read_text(encoding="utf-8")
|
||||
code = re.search(r"versionCode\s*=\s*(\d+)", text)
|
||||
name = re.search(r'versionName\s*=\s*"([^"]+)"', text)
|
||||
if not code or not name:
|
||||
sys.exit("versionCode / versionName not found in app/build.gradle.kts")
|
||||
return int(code.group(1)), name.group(1)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("apk", type=Path)
|
||||
parser.add_argument("out", type=Path)
|
||||
parser.add_argument("--notes", default="")
|
||||
args = parser.parse_args()
|
||||
|
||||
password = os.environ.get("UBGITEA_DOWNLOAD_PASSWORD") or getpass.getpass("Download password: ")
|
||||
if not password:
|
||||
sys.exit("A password is required.")
|
||||
code, name = gradle_version(Path(__file__).resolve().parent.parent)
|
||||
|
||||
data = args.apk.read_bytes()
|
||||
salt = os.urandom(16)
|
||||
iv = os.urandom(12)
|
||||
key = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, ITERATIONS, 32)
|
||||
sealed = AESGCM(key).encrypt(iv, data, None)
|
||||
# A few sealed bytes the page opens first: a wrong password is refused before the APK is
|
||||
# fetched at all, so nothing is downloaded with it.
|
||||
check_iv = os.urandom(12)
|
||||
check = AESGCM(key).encrypt(check_iv, CHECK_TEXT, None)
|
||||
|
||||
args.out.mkdir(parents=True, exist_ok=True)
|
||||
(args.out / ENCRYPTED_NAME).write_bytes(sealed)
|
||||
release = {"versionCode": code, "versionName": name, "notes": args.notes}
|
||||
(args.out / "version.json").write_text(json.dumps(release, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
meta = {
|
||||
"salt": base64.b64encode(salt).decode(),
|
||||
"iv": base64.b64encode(iv).decode(),
|
||||
"iterations": ITERATIONS,
|
||||
"check": base64.b64encode(check).decode(),
|
||||
"checkIv": base64.b64encode(check_iv).decode(),
|
||||
"file": ENCRYPTED_NAME,
|
||||
"fileName": f"UBGitea-{name}.apk",
|
||||
"size": len(data),
|
||||
}
|
||||
page = PAGE.replace("{{META}}", json.dumps(meta)) \
|
||||
.replace("{{VERSION}}", html.escape(name)) \
|
||||
.replace("{{NOTES}}", html.escape(args.notes)) \
|
||||
.replace("{{SIZE}}", f"{len(data) / 1024 / 1024:.1f} MB")
|
||||
(args.out / "index.html").write_text(page, encoding="utf-8")
|
||||
print(f"UBGitea {name} (versionCode {code}) -> {args.out}")
|
||||
|
||||
|
||||
PAGE = """<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex">
|
||||
<title>UBGitea 다운로드</title>
|
||||
<style>
|
||||
:root { --bg: #f4f7fc; --card: #ffffff; --ink: #14213d; --muted: #5b6b86; --line: #d8e1f0;
|
||||
--blue: #1f5fd6; --blue-ink: #ffffff; --bad: #c62828; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root { --bg: #0d1524; --card: #16213a; --ink: #e8eefb; --muted: #9fb0cf; --line: #2a3a5c;
|
||||
--blue: #6d9cff; --blue-ink: #0d1524; --bad: #ff8a80; }
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; min-height: 100vh; background: var(--bg); color: var(--ink);
|
||||
font-family: system-ui, -apple-system, "Segoe UI", "Malgun Gothic", sans-serif;
|
||||
display: flex; align-items: center; justify-content: center; padding: 16px; }
|
||||
main { width: 100%; max-width: 420px; background: var(--card); border: 1px solid var(--line);
|
||||
border-radius: 20px; padding: 28px 24px; }
|
||||
h1 { margin: 0 0 4px; font-size: 24px; }
|
||||
.version { color: var(--blue); font-weight: 600; }
|
||||
p { color: var(--muted); line-height: 1.5; margin: 8px 0; }
|
||||
.notes { white-space: pre-line; color: var(--ink); background: var(--bg); border-radius: 12px;
|
||||
padding: 12px 14px; margin: 16px 0; }
|
||||
.notes:empty { display: none; }
|
||||
label { display: block; font-weight: 600; margin: 20px 0 8px; }
|
||||
input { width: 100%; font-size: 17px; padding: 14px; border-radius: 12px; border: 1px solid var(--line);
|
||||
background: var(--card); color: var(--ink); }
|
||||
button { width: 100%; margin-top: 12px; font-size: 17px; font-weight: 600; padding: 14px; border: 0;
|
||||
border-radius: 12px; background: var(--blue); color: var(--blue-ink); cursor: pointer; }
|
||||
button:disabled { opacity: .6; cursor: default; }
|
||||
#status { min-height: 1.5em; margin-top: 12px; }
|
||||
#status.bad { color: var(--bad); }
|
||||
ol { color: var(--muted); padding-left: 20px; line-height: 1.6; margin: 20px 0 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>UBGitea</h1>
|
||||
<div class="version">버전 {{VERSION}} · {{SIZE}}</div>
|
||||
<div class="notes">{{NOTES}}</div>
|
||||
<form id="form">
|
||||
<label for="password">다운로드 비밀번호</label>
|
||||
<input id="password" type="password" autocomplete="off" required>
|
||||
<button id="go" type="submit">APK 다운로드</button>
|
||||
</form>
|
||||
<p id="status" role="status" aria-live="polite"></p>
|
||||
<ol>
|
||||
<li>받은 APK 파일을 엽니다.</li>
|
||||
<li>"출처를 알 수 없는 앱" 설치를 이 브라우저에 허용합니다.</li>
|
||||
<li>설치(업데이트)를 누릅니다. 로그인과 설정은 그대로 남습니다.</li>
|
||||
</ol>
|
||||
</main>
|
||||
<script>
|
||||
const META = {{META}};
|
||||
const status = document.getElementById("status");
|
||||
const go = document.getElementById("go");
|
||||
const b64 = s => Uint8Array.from(atob(s), c => c.charCodeAt(0));
|
||||
function say(text, bad) { status.textContent = text; status.className = bad ? "bad" : ""; }
|
||||
document.getElementById("form").addEventListener("submit", async event => {
|
||||
event.preventDefault();
|
||||
go.disabled = true;
|
||||
try {
|
||||
say("비밀번호 확인 중…");
|
||||
const password = new TextEncoder().encode(document.getElementById("password").value);
|
||||
const base = await crypto.subtle.importKey("raw", password, "PBKDF2", false, ["deriveKey"]);
|
||||
const key = await crypto.subtle.deriveKey(
|
||||
{ name: "PBKDF2", salt: b64(META.salt), iterations: META.iterations, hash: "SHA-256" },
|
||||
base, { name: "AES-GCM", length: 256 }, false, ["decrypt"]);
|
||||
try {
|
||||
await crypto.subtle.decrypt({ name: "AES-GCM", iv: b64(META.checkIv) }, key, b64(META.check));
|
||||
} catch (e) {
|
||||
say("비밀번호가 맞지 않습니다.", true);
|
||||
return;
|
||||
}
|
||||
say("파일을 받는 중…");
|
||||
const response = await fetch(META.file, { cache: "no-store" });
|
||||
if (!response.ok) throw new Error("fetch");
|
||||
const sealed = await response.arrayBuffer();
|
||||
say("여는 중…");
|
||||
const apk = await crypto.subtle.decrypt({ name: "AES-GCM", iv: b64(META.iv) }, key, sealed);
|
||||
const url = URL.createObjectURL(new Blob([apk], { type: "application/vnd.android.package-archive" }));
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = META.fileName;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 60000);
|
||||
say("다운로드를 시작했습니다. 받은 파일을 열어 설치하세요.");
|
||||
} catch (e) {
|
||||
say("파일을 받지 못했습니다. 잠시 뒤 다시 시도하세요.", true);
|
||||
} finally {
|
||||
go.disabled = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
x
Reference in New Issue
Block a user