Skip to content

Commit 2ffe174

Browse files
authored
Download from Digital Ocean if Github download fails (#4265)
# Description of Changes <!-- Please describe your change, mention any related tickets, and so on here. --> Since Github has been going down *a lot* recently, we would like to add support for downloading from our mirror instead. This PR updates: - The install script for both windows and linux to try the mirror if Github is down - The `spacetime version install ...` to install a specific version - The `spacetime upgrade` which grabs the latest version from Github. # API and ABI breaking changes <!-- If this is an API or ABI breaking change, please apply the corresponding GitHub label. --> None # Expected complexity level and risk 3 - This is touching both the windows and linux/macos install scripts which are quite sensitive. Proper manual testing is needed on this (see Testing below) <!-- How complicated do you think these changes are? Grade on a scale from 1 to 5, where 1 is a trivial change, and 5 is a deep-reaching and complex change. This complexity rating applies not only to the complexity apparent in the diff, but also to its interactions with existing and future code. If you answered more than a 2, explain what is complex about the PR, and what other components it interacts with in potentially concerning ways. --> # Testing <!-- Describe any testing you've done, and any testing you'd like your reviewers to do, so that you're confident that all the changes work as expected! --> For these tests I executed the respective install script locally. To simulate github being down I set the github download to http instead of https and then I spun up a local server which returns 500. I also added a `127.0.0.1 github.com` entry to my hosts file (this works on both Windows, linux and macOS). Linux - [x] Tested that the Github download works normally - [x] Tested that if Github returns a 500 the digital ocean download works normally The download failed here and then it correctly retried with the mirror - I wasn't able to capture the retry part because it's part of the progress spinner so it just updates dynamically but this was the final output: ``` boppy@geralt:~/clockwork/SpacetimeDB$ spacetime-install The SpacetimeDB command line tool will now be installed: CLI configuration directory: /home/boppy/.config/spacetime/ `spacetime` binary: /home/boppy/.local/bin/spacetime directory for installed SpacetimeDB versions: /home/boppy/.local/share/spacetime/bin database directory: /home/boppy/.local/share/spacetime/data Would you like to continue? yes Downloading latest version... Installing v1.12.0: done! The `spacetime` command has been installed as /home/boppy/.local/bin/spacetime The install process is complete; check out our quickstart guide to get started! <https://spacetimedb.com/docs/getting-started> ``` Windows - [x] Tested that the Github download works normally - [x] Tested that if Github returns a 500 the digital ocean download works normally This worked as well: ``` PS C:\Users\boppy\clockwork\SpacetimeDB\crates\update> .\spacetime-install.ps1 vcruntime140.dll is already installed at C:\WINDOWS\System32\vcruntime140.dll Downloading installer... Download failed, trying mirror... We have added spacetimedb to your Path. You may have to logout and log back in to reload your environment. ``` macOS - [x] Tested that the Github download works normally - [x] Tested that if Github returns a 500 the digital ocean download works normally ``` boppy@Johns-Laptop update % bash ./spacetime-install.sh Downloading installer... curl: (22) The requested URL returned error: 500 Download failed, trying mirror... The SpacetimeDB command line tool will now be installed: CLI configuration directory: /Users/boppy/.config/spacetime/ `spacetime` binary: /Users/boppy/.local/bin/spacetime directory for installed SpacetimeDB versions: /Users/boppy/.local/share/spacetime/bin database directory: /Users/boppy/.local/share/spacetime/data Would you like to continue? yes Downloading latest version... Installing v1.12.0: done! The `spacetime` command has been installed as /Users/boppy/.local/bin/spacetime The install process is complete; check out our quickstart guide to get started! <https://spacetimedb.com/docs/getting-started> ```
1 parent 98585e8 commit 2ffe174

4 files changed

Lines changed: 194 additions & 64 deletions

File tree

crates/update/spacetime-install.ps1

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ function Install {
3030
Start-Process -Wait -FilePath $Installer -ArgumentList "/quiet", "/install"
3131
}
3232

33-
$DownloadUrl = "https://github.com/clockworklabs/SpacetimeDB/releases/latest/download/spacetimedb-update-x86_64-pc-windows-msvc.exe"
33+
$AssetName = "spacetimedb-update-x86_64-pc-windows-msvc.exe"
34+
$DownloadUrl = "https://github.com/clockworklabs/SpacetimeDB/releases/latest/download/$AssetName"
35+
$MirrorBase = "https://spacetimedb-client-binaries.nyc3.digitaloceanspaces.com"
3436
Write-Output "Downloading installer..."
3537

3638
function UpdatePathIfNotExists {
@@ -44,7 +46,14 @@ function Install {
4446
}
4547

4648
$Executable = Join-Path ([System.IO.Path]::GetTempPath()) "spacetime-install.exe"
47-
Invoke-WebRequest $DownloadUrl -OutFile $Executable -UseBasicParsing
49+
try {
50+
Invoke-WebRequest $DownloadUrl -OutFile $Executable -UseBasicParsing
51+
} catch {
52+
Write-Output "Download failed, trying mirror..."
53+
$Tag = (Invoke-WebRequest "$MirrorBase/latest-version" -UseBasicParsing).Content.Trim()
54+
$MirrorUrl = "$MirrorBase/refs/tags/$Tag/$AssetName"
55+
Invoke-WebRequest $MirrorUrl -OutFile $Executable -UseBasicParsing
56+
}
4857
Start-Process -Wait -FilePath $Executable
4958

5059
# TODO: do this in spacetimedb-update

crates/update/spacetime-install.sh

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,18 +63,36 @@ main() {
6363
check_for_help_flag "$@"
6464

6565
# Define the latest SpacetimeDB download url
66-
local _url="$SPACETIME_DOWNLOAD_ROOT/spacetimedb-update-$_host$_ext"
66+
local _asset_name="spacetimedb-update-$_host$_ext"
67+
local _url="$SPACETIME_DOWNLOAD_ROOT/$_asset_name"
68+
local _mirror_base="https://spacetimedb-client-binaries.nyc3.digitaloceanspaces.com"
6769
echo "Downloading installer..."
70+
local _ok=false
6871
if [ "$_downloader" = curl ]; then
69-
local _httpstatus
70-
if ! _httpstatus="$(curl -sSfL -w '%{http_code}' --progress-bar "$_url" -o "$_download_file")"; then
71-
if [ "$_httpstatus" = 404 ]; then
72-
err "It seems like we may not have prebuilt binaries for your platform."
73-
fi
74-
exit 1
72+
if curl -sSfL --progress-bar "$_url" -o "$_download_file"; then
73+
_ok=true
7574
fi
7675
elif [ "$_downloader" = wget ]; then
77-
wget "$_url" -O "$_download_file" || exit 1
76+
if wget "$_url" -O "$_download_file"; then
77+
_ok=true
78+
fi
79+
fi
80+
81+
if [ "$_ok" = false ]; then
82+
echo "Download failed, trying mirror..."
83+
local _tag
84+
if [ "$_downloader" = curl ]; then
85+
_tag="$(curl -sSfL "$_mirror_base/latest-version")" || err "Mirror also unavailable"
86+
elif [ "$_downloader" = wget ]; then
87+
_tag="$(wget -qO- "$_mirror_base/latest-version")" || err "Mirror also unavailable"
88+
fi
89+
_tag="$(echo "$_tag" | tr -d '[:space:]')"
90+
local _mirror_url="$_mirror_base/refs/tags/$_tag/$_asset_name"
91+
if [ "$_downloader" = curl ]; then
92+
curl -sSfL --progress-bar "$_mirror_url" -o "$_download_file" || err "Mirror download also failed"
93+
elif [ "$_downloader" = wget ]; then
94+
wget "$_mirror_url" -O "$_download_file" || err "Mirror download also failed"
95+
fi
7896
fi
7997

8098
ensure chmod u+x "$_download_file"

crates/update/src/cli/install.rs

Lines changed: 122 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,45 @@ fn releases_url() -> String {
5959
.unwrap_or_else(|_| "https://api.github.com/repos/clockworklabs/SpacetimeDB/releases".to_owned())
6060
}
6161

62+
const MIRROR_BASE_URL: &str = "https://spacetimedb-client-binaries.nyc3.digitaloceanspaces.com";
63+
64+
pub(super) fn mirror_asset_url(version: &semver::Version, asset_name: &str) -> String {
65+
format!("{MIRROR_BASE_URL}/refs/tags/v{version}/{asset_name}")
66+
}
67+
68+
async fn mirror_release(
69+
client: &reqwest::Client,
70+
version: Option<&semver::Version>,
71+
download_name: &str,
72+
) -> anyhow::Result<(semver::Version, Release)> {
73+
let tag = match version {
74+
Some(v) => format!("v{v}"),
75+
None => {
76+
let url = format!("{MIRROR_BASE_URL}/latest-version");
77+
client
78+
.get(&url)
79+
.send()
80+
.await?
81+
.error_for_status()?
82+
.text()
83+
.await?
84+
.trim()
85+
.to_owned()
86+
}
87+
};
88+
let ver_str = tag.strip_prefix('v').unwrap_or(&tag);
89+
let release_version =
90+
semver::Version::parse(ver_str).with_context(|| format!("Could not parse version from mirror: {tag}"))?;
91+
let release = Release {
92+
tag_name: tag.clone(),
93+
assets: vec![ReleaseAsset {
94+
name: download_name.to_owned(),
95+
browser_download_url: mirror_asset_url(&release_version, download_name),
96+
}],
97+
};
98+
Ok((release_version, release))
99+
}
100+
62101
pub(super) async fn download_and_install(
63102
client: &reqwest::Client,
64103
version: Option<semver::Version>,
@@ -72,29 +111,47 @@ pub(super) async fn download_and_install(
72111
let pb = make_progress_bar();
73112

74113
pb.set_message("Resolving version...");
75-
let releases_url = releases_url();
76-
let url = match &version {
77-
Some(version) => format!("{releases_url}/tags/v{version}"),
78-
None => [&*releases_url, "/latest"].concat(),
79-
};
80-
let release: Release = client
81-
.get(url)
82-
.send()
83-
.await?
84-
.error_for_status()
85-
.map_err(|e| {
86-
if e.status() == Some(reqwest::StatusCode::NOT_FOUND) {
87-
if let Some(version) = &version {
88-
return anyhow::anyhow!(e).context(format!("No release found for version {version}"));
114+
115+
// Try GitHub first, fall back to mirror if unavailable.
116+
let github_result = async {
117+
let releases_url = releases_url();
118+
let url = match &version {
119+
Some(version) => format!("{releases_url}/tags/v{version}"),
120+
None => [&*releases_url, "/latest"].concat(),
121+
};
122+
let release: Release = client
123+
.get(url)
124+
.send()
125+
.await?
126+
.error_for_status()
127+
.map_err(|e| {
128+
if e.status() == Some(reqwest::StatusCode::NOT_FOUND) {
129+
if let Some(version) = &version {
130+
return anyhow::anyhow!(e).context(format!("No release found for version {version}"));
131+
}
89132
}
90-
}
91-
anyhow::anyhow!(e).context("Could not fetch release info")
92-
})?
93-
.json()
94-
.await?;
95-
let release_version = match version {
96-
Some(version) => version,
97-
None => release.version().context("Could not parse version number")?,
133+
anyhow::anyhow!(e).context("Could not fetch release info")
134+
})?
135+
.json()
136+
.await?;
137+
let release_version = match &version {
138+
Some(version) => version.clone(),
139+
None => release.version().context("Could not parse version number")?,
140+
};
141+
anyhow::Ok((release_version, release))
142+
}
143+
.await;
144+
145+
let (release_version, release) = match github_result {
146+
Ok(result) => result,
147+
Err(github_err) => {
148+
pb.set_message("GitHub unavailable, trying mirror...");
149+
mirror_release(client, version.as_ref(), download_name)
150+
.await
151+
.map_err(|mirror_err| {
152+
anyhow::anyhow!("GitHub failed: {github_err:#}\nMirror also failed: {mirror_err:#}")
153+
})?
154+
}
98155
};
99156

100157
let asset = release
@@ -112,7 +169,21 @@ pub(super) async fn download_and_install(
112169

113170
pb.set_prefix(format!("Installing v{release_version}: "));
114171
pb.set_message("downloading...");
115-
let archive = download_with_progress(&pb, client, &asset.browser_download_url).await?;
172+
let mirror_url = mirror_asset_url(&release_version, download_name);
173+
let archive = match download_with_progress(&pb, client, &asset.browser_download_url).await {
174+
Ok(archive) => archive,
175+
Err(primary_err) => {
176+
if asset.browser_download_url == mirror_url {
177+
return Err(primary_err);
178+
}
179+
pb.set_message("download failed, trying mirror...");
180+
download_with_progress(&pb, client, &mirror_url)
181+
.await
182+
.map_err(|mirror_err| {
183+
anyhow::anyhow!("Primary download failed: {primary_err:#}\nMirror also failed: {mirror_err:#}")
184+
})?
185+
}
186+
};
116187

117188
pb.set_message("unpacking...");
118189

@@ -154,12 +225,34 @@ impl ArtifactType {
154225

155226
pub(super) async fn available_releases(client: &reqwest::Client) -> anyhow::Result<Vec<String>> {
156227
let url = releases_url();
157-
let releases: Vec<Release> = client.get(url).send().await?.json().await?;
158-
159-
releases
160-
.into_iter()
161-
.map(|release| Ok(release.version()?.to_string()))
162-
.collect()
228+
match async {
229+
anyhow::Ok(
230+
client
231+
.get(url)
232+
.send()
233+
.await?
234+
.error_for_status()?
235+
.json::<Vec<Release>>()
236+
.await?,
237+
)
238+
}
239+
.await
240+
{
241+
Ok(releases) => releases
242+
.into_iter()
243+
.map(|release| Ok(release.version()?.to_string()))
244+
.collect(),
245+
Err(_) => {
246+
eprintln!("GitHub unavailable, fetching latest version from mirror...");
247+
let url = format!("{MIRROR_BASE_URL}/latest-version");
248+
let tag = client.get(&url).send().await?.error_for_status()?.text().await?;
249+
let ver_str = tag.trim();
250+
let ver_str = ver_str.strip_prefix('v').unwrap_or(ver_str);
251+
semver::Version::parse(ver_str)
252+
.with_context(|| format!("Could not parse version from mirror: {ver_str}"))?;
253+
Ok(vec![ver_str.to_owned()])
254+
}
255+
}
163256
}
164257

165258
#[derive(Deserialize)]

crates/update/src/cli/upgrade.rs

Lines changed: 35 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::io::Write;
33
use anyhow::Context;
44
use spacetimedb_paths::SpacetimePaths;
55

6-
use super::install::{download_and_install, download_with_progress, make_progress_bar};
6+
use super::install::{download_and_install, download_with_progress, make_progress_bar, mirror_asset_url};
77

88
/// Upgrade and switch to the latest available version of SpacetimeDB.
99
#[derive(clap::Args)]
@@ -18,30 +18,40 @@ impl Upgrade {
1818

1919
let cur_version = semver::Version::parse(env!("CARGO_PKG_VERSION")).unwrap();
2020
if version > cur_version {
21-
if let Some(asset) = release.assets.iter().find(|asset| asset.name == UPDATE_BIN_NAME) {
22-
let pb = make_progress_bar().with_prefix("Self-updating `spacetime version`: ");
23-
pb.set_message("downloading...");
24-
let bin = download_with_progress(&pb, &client, &asset.browser_download_url).await?;
25-
pb.set_message("installing...");
26-
let cli_bin_file = paths.cli_bin_file.clone();
27-
tokio::task::spawn_blocking(move || {
28-
// TODO(noa): try and see if `self_replace` could support providing the binary
29-
// in a buffer, instead of an already existing file, since we're doing the same
30-
// work they are right now
31-
let mut file = tempfile::NamedTempFile::with_prefix_in(
32-
".spacetimedb-self-replace",
33-
cli_bin_file.0.parent().unwrap(),
34-
)?;
35-
file.write_all(&bin.to_bytes())?;
36-
self_replace::self_replace(file.path())
37-
.context("failed to overwrite the original spacetime binary")
38-
})
39-
.await??;
40-
41-
pb.finish_with_message("done!");
42-
} else {
43-
eprintln!("Tried to self-update `spacetime version`, but no release asset was found.");
44-
}
21+
let mirror_url = mirror_asset_url(&version, UPDATE_BIN_NAME);
22+
let download_url = match release.assets.iter().find(|asset| asset.name == UPDATE_BIN_NAME) {
23+
Some(asset) => asset.browser_download_url.clone(),
24+
None => mirror_url.clone(),
25+
};
26+
27+
let pb = make_progress_bar().with_prefix("Self-updating `spacetime version`: ");
28+
pb.set_message("downloading...");
29+
let bin = match download_with_progress(&pb, &client, &download_url).await {
30+
Ok(bin) => bin,
31+
Err(primary_err) => {
32+
if download_url == mirror_url {
33+
return Err(primary_err);
34+
}
35+
pb.set_message("download failed, trying mirror...");
36+
download_with_progress(&pb, &client, &mirror_url).await?
37+
}
38+
};
39+
pb.set_message("installing...");
40+
let cli_bin_file = paths.cli_bin_file.clone();
41+
tokio::task::spawn_blocking(move || {
42+
// TODO(noa): try and see if `self_replace` could support providing the binary
43+
// in a buffer, instead of an already existing file, since we're doing the same
44+
// work they are right now
45+
let mut file = tempfile::NamedTempFile::with_prefix_in(
46+
".spacetimedb-self-replace",
47+
cli_bin_file.0.parent().unwrap(),
48+
)?;
49+
file.write_all(&bin.to_bytes())?;
50+
self_replace::self_replace(file.path()).context("failed to overwrite the original spacetime binary")
51+
})
52+
.await??;
53+
54+
pb.finish_with_message("done!");
4555
}
4656

4757
Ok(())

0 commit comments

Comments
 (0)