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.
|
`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
|
- 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.
|
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
|
# 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
|
Owner: Claude
|
||||||
Reviewer: human product owner
|
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
|
## Scope
|
||||||
|
|
||||||
- Included: the two addresses, the wording, `version.json` on the `apk` branch.
|
- 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
|
- Follow-up (product owner, 2026-10-12: the old site is not used any more):
|
||||||
as they are and are no longer part of publishing.
|
`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
|
## Writable paths
|
||||||
|
|
||||||
- `app/src/main/java/com/ubisam/ubgitea/core/update/AppUpdate.kt`
|
- `app/src/main/java/com/ubisam/ubgitea/core/update/AppUpdate.kt`
|
||||||
- `app/src/main/java/com/ubisam/ubgitea/MainActivity.kt`
|
- `app/src/main/java/com/ubisam/ubgitea/MainActivity.kt`
|
||||||
- `app/src/main/res/values/strings_update.xml`
|
- `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`
|
- `tasks/070-update-from-gitea.md`
|
||||||
- `docs/development-log.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