release(v1.7.15-alpha): bulletproof downloads — resume, retry, real progress
download_update
Each component download is now resumable via HTTP Range requests
(Range: bytes=N-) and retried up to 6 times with exponential
backoff (5/15/30/60/120/180s). On a dropped connection the next
attempt picks up at the last written byte offset instead of
restarting at zero. Streams via reqwest::Response::chunk() to the
staging file so a 160 MB frontend tarball doesn't sit in RAM. SHA
is verified over the complete file at the end of each component;
mismatch nukes the staged file and restarts from scratch.
Real download progress counters
New AtomicU64 globals DOWNLOAD_BYTES/DOWNLOAD_TOTAL are updated
from the chunk loop. update.status exposes them as
download_progress.{bytes_downloaded, total_bytes, active}. The
SystemUpdate.vue progress bar now polls update.status every
second instead of incrementing a fake random counter — and
crucially, if the user navigates away and back, the component
picks up the in-progress download from the backend atomics
immediately.
Update-check retries
handle_update_check now retries the manifest fetch up to 3 times
with a 5s gap if the first try hits a transport error, so a
momentary gitea hiccup doesn't make a node report "up to date"
when there actually is a new release. Tight 10s connect timeout
per attempt keeps the total bounded.
Artefacts:
archipelago 1070c87f…c081c162b 40584792
archipelago-frontend-1.7.15-alpha.tar.gz 8e630eba…63fd43f 162078068
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -324,6 +324,29 @@ const statusMessage = ref('')
|
||||
const statusIsError = ref(false)
|
||||
const downloadPercent = ref(0)
|
||||
const downloadPercentFormatted = computed(() => downloadPercent.value.toFixed(2))
|
||||
|
||||
// Poll the backend for the real bytes_downloaded / total_bytes so the
|
||||
// progress bar tracks actual download state (and survives route
|
||||
// changes). Returns true if a download is currently in progress.
|
||||
async function pollDownloadProgress(): Promise<boolean> {
|
||||
try {
|
||||
const res = await rpcClient.call<{
|
||||
download_progress?: {
|
||||
bytes_downloaded: number
|
||||
total_bytes: number
|
||||
active: boolean
|
||||
} | null
|
||||
}>({ method: 'update.status' })
|
||||
const p = res.download_progress
|
||||
if (p && p.total_bytes > 0) {
|
||||
downloadPercent.value = Math.min(100, (p.bytes_downloaded / p.total_bytes) * 100)
|
||||
return p.active
|
||||
}
|
||||
return false
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
// Shown next to the progress bar when the fake increment has maxed out
|
||||
// at 95% but the real RPC hasn't returned yet — lets the user know the
|
||||
// UI hasn't frozen while SHA verification and disk writes finish.
|
||||
@@ -486,14 +509,12 @@ async function downloadUpdate() {
|
||||
downloadPercent.value = 0
|
||||
statusMessage.value = ''
|
||||
|
||||
// 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(() => {
|
||||
if (downloadPercent.value < 95) {
|
||||
downloadPercent.value = Math.min(95, downloadPercent.value + Math.random() * 3)
|
||||
}
|
||||
}, 500)
|
||||
// Poll the backend's real byte counter every second instead of
|
||||
// faking progress. The backend exposes bytes_downloaded/total_bytes
|
||||
// via update.status, updated per chunk. This also means the bar
|
||||
// resumes correctly after navigating away and back — no more
|
||||
// "95% for some time" mystery.
|
||||
const progressInterval = setInterval(() => { void pollDownloadProgress() }, 1000)
|
||||
|
||||
try {
|
||||
const res = await rpcClient.call<{
|
||||
@@ -616,8 +637,24 @@ async function setSchedule(value: ScheduleValue) {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
Promise.all([loadStatus(), loadSchedule(), checkForUpdates()])
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadStatus(), loadSchedule(), checkForUpdates()])
|
||||
// If a download was already running when the user navigated here
|
||||
// (or refreshed), pick up the progress bar where it is and keep
|
||||
// polling until the backend reports done. No RPC call to start the
|
||||
// download — the backend's already running it.
|
||||
const active = await pollDownloadProgress()
|
||||
if (active) {
|
||||
downloading.value = true
|
||||
const resumeInterval = setInterval(async () => {
|
||||
const stillActive = await pollDownloadProgress()
|
||||
if (!stillActive) {
|
||||
clearInterval(resumeInterval)
|
||||
downloading.value = false
|
||||
downloaded.value = true
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user