release(v1.7.4-alpha): fix Install Update tar extraction + progress overshoot

apply_update was extracting the frontend tarball with
`tar -xzf -C /opt/archipelago`, but the tar contents are the *inside*
of web-ui/ (root entries are ./test-aiui.html, ./assets/, etc.). So
the files landed directly in /opt/archipelago instead of under web-ui/,
and tar bailed on nginx-owned paths mid-extraction. First end-to-end
OTA test (.198) found it: "tar: ./assets/SystemUpdate-…js: Cannot
open: No such file or directory".

Now extracts into web-ui.new, chowns, then atomically swaps: move
existing web-ui → web-ui.bak, then web-ui.new → web-ui. Same pattern
as the manual sideload that's been working.

Frontend: SystemUpdate.vue fake download progress was capped at "<90"
with a Math.random()*15 increment — the last tick could push to
~104.99%. Capped at 95% with a smaller step so it stops at 95 and the
real RPC completion jumps it to 100.

Artefacts:
  archipelago                                      a14ad7e4…2a2be3  40361984
  archipelago-frontend-1.7.4-alpha.tar.gz          4fb79664…0172e9  76984615

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian 2026-04-20 12:02:14 -04:00
parent 3db71adb55
commit 1e7df417a4
4 changed files with 50 additions and 30 deletions

2
core/Cargo.lock generated
View File

@ -80,7 +80,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]] [[package]]
name = "archipelago" name = "archipelago"
version = "1.7.3-alpha" version = "1.7.4-alpha"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"archipelago-container", "archipelago-container",

View File

@ -1,6 +1,6 @@
[package] [package]
name = "archipelago" name = "archipelago"
version = "1.7.3-alpha" version = "1.7.4-alpha"
edition = "2021" edition = "2021"
description = "Archipelago Bitcoin Node OS - Native backend" description = "Archipelago Bitcoin Node OS - Native backend"
authors = ["Archipelago Team"] authors = ["Archipelago Team"]

View File

@ -305,42 +305,60 @@ pub async fn apply_update(data_dir: &Path) -> Result<()> {
info!(name = %name, "Backend binary applied"); info!(name = %name, "Backend binary applied");
} }
_ if name.contains("frontend") && name.ends_with(".tar.gz") => { _ if name.contains("frontend") && name.ends_with(".tar.gz") => {
let web_ui_dir = Path::new("/opt/archipelago/web-ui"); // The tarball contents are the *inside* of web-ui/ — root
// Back up current frontend. /opt/archipelago is root-owned; // entries are `./test-aiui.html`, `./assets/`, etc. Extract
// the backup goes under our data_dir where we can write. // into a sibling staging dir, then swap atomically so
let frontend_backup = backup_dir.join("web-ui-backup.tar.gz"); // nginx never sees a half-written tree.
if web_ui_dir.exists() { let new_dir = "/opt/archipelago/web-ui.new";
let status = tokio::process::Command::new("sudo") let backup_path = "/opt/archipelago/web-ui.bak";
.args([ // Wipe any previous attempt's staging / backup dirs.
"tar", let _ = tokio::process::Command::new("sudo")
"-czf", .args(["rm", "-rf", new_dir, backup_path])
&frontend_backup.to_string_lossy(), .status()
"-C", .await;
"/opt/archipelago", let mk = tokio::process::Command::new("sudo")
"web-ui", .args(["mkdir", "-p", new_dir])
]) .status()
.status() .await
.await .context("Failed to create frontend staging dir")?;
.context("Failed to backup frontend")?; if !mk.success() {
if status.success() { anyhow::bail!("mkdir {} failed", new_dir);
info!("Frontend backed up");
}
} }
// Extract new frontend into /opt/archipelago (root-owned dir). // Extract INTO the staging dir — tar's ./ entries land at
// the right place (web-ui.new/assets/... etc.).
let status = tokio::process::Command::new("sudo") let status = tokio::process::Command::new("sudo")
.args(["tar", "-xzf", &src.to_string_lossy(), "-C", "/opt/archipelago"]) .args(["tar", "-xzf", &src.to_string_lossy(), "-C", new_dir])
.status() .status()
.await .await
.with_context(|| format!("Failed to extract {}", name))?; .with_context(|| format!("Failed to extract {}", name))?;
if !status.success() { if !status.success() {
anyhow::bail!("tar extraction failed for {}", name); anyhow::bail!("tar extraction failed for {}", name);
} }
// nginx serves this tree; keep ownership consistent with // Ownership: match what first-boot + the ISO expect.
// what first-boot + the ISO layout expect.
let _ = tokio::process::Command::new("sudo") let _ = tokio::process::Command::new("sudo")
.args(["chown", "-R", "archipelago:archipelago", "/opt/archipelago/web-ui"]) .args(["chown", "-R", "archipelago:archipelago", new_dir])
.status() .status()
.await; .await;
// Atomic-ish swap: move old aside, new into place.
let web_ui = "/opt/archipelago/web-ui";
if Path::new(web_ui).exists() {
let mv_old = tokio::process::Command::new("sudo")
.args(["mv", web_ui, backup_path])
.status()
.await
.context("Failed to rotate old web-ui")?;
if !mv_old.success() {
anyhow::bail!("failed to move old web-ui aside");
}
}
let mv_new = tokio::process::Command::new("sudo")
.args(["mv", new_dir, web_ui])
.status()
.await
.context("Failed to swap new web-ui into place")?;
if !mv_new.success() {
anyhow::bail!("failed to move new web-ui into place");
}
info!(name = %name, "Frontend archive extracted to /opt/archipelago/web-ui"); info!(name = %name, "Frontend archive extracted to /opt/archipelago/web-ui");
} }
_ => { _ => {

View File

@ -345,10 +345,12 @@ async function downloadUpdate() {
downloadPercent.value = 0 downloadPercent.value = 0
statusMessage.value = '' statusMessage.value = ''
// Simulate incremental progress while waiting for the RPC // Simulate incremental progress while waiting for the RPC. Capped at
// 95% so the bar never shows >100% before the real completion jumps it
// to 100 previously the random increment could overshoot.
const progressInterval = setInterval(() => { const progressInterval = setInterval(() => {
if (downloadPercent.value < 90) { if (downloadPercent.value < 95) {
downloadPercent.value += Math.random() * 15 downloadPercent.value = Math.min(95, downloadPercent.value + Math.random() * 3)
} }
}, 500) }, 500)