parent
f000e9232f
commit
2237afb21a
4 changed files with 208 additions and 3 deletions
15
Cargo.lock
generated
15
Cargo.lock
generated
|
@ -1226,7 +1226,7 @@ dependencies = [
|
|||
"autocfg",
|
||||
"cfg-if",
|
||||
"crossbeam-utils",
|
||||
"memoffset",
|
||||
"memoffset 0.8.0",
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
|
@ -2536,6 +2536,15 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.8.0"
|
||||
|
@ -2675,6 +2684,8 @@ dependencies = [
|
|||
"bitflags",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"memoffset 0.7.1",
|
||||
"pin-utils",
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
|
@ -4277,6 +4288,7 @@ dependencies = [
|
|||
"ipnet",
|
||||
"jsonwebtoken",
|
||||
"log",
|
||||
"nix",
|
||||
"once_cell",
|
||||
"opentelemetry",
|
||||
"opentelemetry-otlp",
|
||||
|
@ -4291,6 +4303,7 @@ dependencies = [
|
|||
"serde_json",
|
||||
"surrealdb",
|
||||
"temp-env",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
|
|
|
@ -54,6 +54,7 @@ serde_cbor = "0.11.2"
|
|||
serde_pack = { version = "1.1.1", package = "rmp-serde" }
|
||||
serde_json = "1.0.96"
|
||||
surrealdb = { path = "lib", features = ["protocol-http", "protocol-ws", "rustls"] }
|
||||
tempfile = "3.5.0"
|
||||
thiserror = "1.0.40"
|
||||
tokio = { version = "1.28.1", features = ["macros", "signal"] }
|
||||
tokio-util = { version = "0.7.8", features = ["io"] }
|
||||
|
@ -65,6 +66,9 @@ tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
|
|||
urlencoding = "2.1.2"
|
||||
warp = { version = "0.3.5", features = ["compression", "tls", "websocket"] }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
nix = "0.26.2"
|
||||
|
||||
[dev-dependencies]
|
||||
rcgen = "0.10.0"
|
||||
tonic = "0.8.3"
|
||||
|
|
|
@ -6,14 +6,15 @@ mod import;
|
|||
mod isready;
|
||||
mod sql;
|
||||
mod start;
|
||||
mod upgrade;
|
||||
pub(crate) mod validator;
|
||||
mod version;
|
||||
|
||||
pub use config::CF;
|
||||
|
||||
use self::upgrade::UpgradeCommandArguments;
|
||||
use crate::cnf::LOGO;
|
||||
use backup::BackupCommandArguments;
|
||||
use clap::{Parser, Subcommand};
|
||||
pub use config::CF;
|
||||
use export::ExportCommandArguments;
|
||||
use import::ImportCommandArguments;
|
||||
use isready::IsReadyCommandArguments;
|
||||
|
@ -57,6 +58,8 @@ enum Commands {
|
|||
Export(ExportCommandArguments),
|
||||
#[command(about = "Output the command-line tool version information")]
|
||||
Version,
|
||||
#[command(about = "Upgrade to the latest stable version")]
|
||||
Upgrade(UpgradeCommandArguments),
|
||||
#[command(about = "Start an SQL REPL in your terminal with pipe support")]
|
||||
Sql(SqlCommandArguments),
|
||||
#[command(
|
||||
|
@ -74,6 +77,7 @@ pub async fn init() -> ExitCode {
|
|||
Commands::Import(args) => import::init(args).await,
|
||||
Commands::Export(args) => export::init(args).await,
|
||||
Commands::Version => version::init(),
|
||||
Commands::Upgrade(args) => upgrade::init(args).await,
|
||||
Commands::Sql(args) => sql::init(args).await,
|
||||
Commands::IsReady(args) => isready::init(args).await,
|
||||
};
|
||||
|
|
184
src/cli/upgrade.rs
Normal file
184
src/cli/upgrade.rs
Normal file
|
@ -0,0 +1,184 @@
|
|||
use crate::cnf::PKG_VERSION;
|
||||
use crate::err::Error;
|
||||
use clap::Args;
|
||||
use std::borrow::Cow;
|
||||
use std::fs;
|
||||
use std::io::{Error as IoError, ErrorKind};
|
||||
use std::ops::Deref;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use surrealdb::env::{arch, os};
|
||||
|
||||
const LATEST_STABLE_VERSION: &str = "https://version.surrealdb.com/";
|
||||
const ROOT: &str = "https://download.surrealdb.com";
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub struct UpgradeCommandArguments {
|
||||
/// Install the latest nightly version
|
||||
#[arg(long, conflicts_with = "version")]
|
||||
nightly: bool,
|
||||
/// Install a specific version
|
||||
#[arg(long, conflicts_with = "nightly")]
|
||||
version: Option<String>,
|
||||
/// Don't actually replace the executable
|
||||
#[arg(long)]
|
||||
dry_run: bool,
|
||||
}
|
||||
|
||||
impl UpgradeCommandArguments {
|
||||
/// Get the version string to download based on the user preference
|
||||
async fn version(&self) -> Result<Cow<'_, str>, Error> {
|
||||
Ok(if self.nightly {
|
||||
Cow::Borrowed("nightly")
|
||||
} else if let Some(version) = self.version.as_ref() {
|
||||
Cow::Borrowed(version)
|
||||
} else {
|
||||
let response = reqwest::get(LATEST_STABLE_VERSION).await?;
|
||||
if !response.status().is_success() {
|
||||
return Err(Error::Io(IoError::new(
|
||||
ErrorKind::Other,
|
||||
format!("received status {} when fetching version", response.status()),
|
||||
)));
|
||||
}
|
||||
Cow::Owned(response.text().await?.trim().to_owned())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn init(args: UpgradeCommandArguments) -> Result<(), Error> {
|
||||
// Initialize opentelemetry and logging
|
||||
crate::o11y::builder().with_log_level("error").init();
|
||||
|
||||
// Upgrading overwrites the existing executable
|
||||
let exe = std::env::current_exe()?;
|
||||
|
||||
// Check if we have write permissions
|
||||
let metadata = fs::metadata(&exe)?;
|
||||
let permissions = metadata.permissions();
|
||||
if permissions.readonly() {
|
||||
return Err(Error::Io(IoError::new(
|
||||
ErrorKind::PermissionDenied,
|
||||
"executable is read-only",
|
||||
)));
|
||||
}
|
||||
#[cfg(unix)]
|
||||
if std::os::unix::fs::MetadataExt::uid(&metadata) == 0
|
||||
&& !nix::unistd::Uid::effective().is_root()
|
||||
{
|
||||
return Err(Error::Io(IoError::new(
|
||||
ErrorKind::PermissionDenied,
|
||||
"executable is owned by root; try again with sudo",
|
||||
)));
|
||||
}
|
||||
|
||||
// Compare old and new versions
|
||||
let old_version = PKG_VERSION.deref().clone();
|
||||
let new_version = args.version().await?;
|
||||
|
||||
if old_version == new_version {
|
||||
println!("{old_version} is already installed");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let arch = arch();
|
||||
let os = os();
|
||||
|
||||
println!("current version is {old_version} for {os} on {arch}",);
|
||||
|
||||
let download_arch = match arch {
|
||||
"aarch64" => "arm64",
|
||||
"x86_64" => "amd64",
|
||||
_ => {
|
||||
return Err(Error::Io(IoError::new(
|
||||
ErrorKind::Unsupported,
|
||||
format!("unsupported arch {arch}"),
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
let (download_os, download_ext) = match os {
|
||||
"linux" => ("linux", "tgz"),
|
||||
"macos" => ("darwin", "tgz"),
|
||||
"windows" => ("windows", "exe"),
|
||||
_ => {
|
||||
return Err(Error::Io(IoError::new(
|
||||
ErrorKind::Unsupported,
|
||||
format!("unsupported OS {os}"),
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
println!("downloading {new_version} for {download_os} on {download_arch}");
|
||||
|
||||
let download_filename =
|
||||
format!("surreal-{new_version}.{download_os}-{download_arch}.{download_ext}");
|
||||
let url = format!("{ROOT}/{new_version}/{download_filename}");
|
||||
|
||||
let response = reqwest::get(&url).await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(Error::Io(IoError::new(
|
||||
ErrorKind::Other,
|
||||
format!("received status {} when downloading from {url}", response.status()),
|
||||
)));
|
||||
}
|
||||
|
||||
let binary = response.bytes().await?;
|
||||
|
||||
// Create a temporary file path
|
||||
let tmp_dir = tempfile::tempdir()?;
|
||||
let mut tmp_path = tmp_dir.path().join(download_filename);
|
||||
|
||||
// Download to a temp file to avoid writing to a running exe file
|
||||
fs::write(&tmp_path, &*binary)?;
|
||||
|
||||
// Preserve permissions
|
||||
fs::set_permissions(&tmp_path, permissions)?;
|
||||
|
||||
// Unarchive
|
||||
if download_ext == "tgz" {
|
||||
let output = Command::new("tar")
|
||||
.arg("-zxf")
|
||||
.arg(&tmp_path)
|
||||
.arg("-C")
|
||||
.arg(tmp_dir.path())
|
||||
.output()?;
|
||||
if !output.status.success() {
|
||||
return Err(Error::Io(IoError::new(
|
||||
ErrorKind::Other,
|
||||
format!("failed to unarchive: {}", output.status),
|
||||
)));
|
||||
}
|
||||
|
||||
// focus on the extracted path
|
||||
tmp_path = tmp_dir.path().join("surreal");
|
||||
}
|
||||
|
||||
println!("installing at {}", exe.display());
|
||||
|
||||
// Replace the running executable
|
||||
if args.dry_run {
|
||||
println!("Dry run successfully completed")
|
||||
} else {
|
||||
replace_exe(&tmp_path, &exe)?;
|
||||
println!("SurrealDB successfully upgraded");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Replace exe at `to` with contents of `from`
|
||||
fn replace_exe(from: &Path, to: &Path) -> Result<(), IoError> {
|
||||
if cfg!(windows) {
|
||||
fs::rename(to, to.with_extension("old.exe"))?;
|
||||
} else {
|
||||
fs::remove_file(to)?;
|
||||
}
|
||||
// Rename works when from and to are on the same file system/device, but
|
||||
// fall back to copy if they're not
|
||||
fs::rename(from, to).or_else(|_| {
|
||||
// Don't worry about deleting the file as the tmp directory will
|
||||
// be deleted automatically
|
||||
fs::copy(from, to).map(|_| ())
|
||||
})
|
||||
}
|
Loading…
Reference in a new issue