Skip to content

Commit

Permalink
newly installed binary is not executable (#13)
Browse files Browse the repository at this point in the history
* not working yet

* clippy lints

* pedantic lints

* remove unnecessary clones

* make destination an input arg

* solved exectuable problem

I was setting the permissions on the parent folder, not on the file itself
  • Loading branch information
natemcintosh committed May 11, 2024
1 parent 353bf85 commit c547cfa
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 40 deletions.
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ categories = ["command-line-utilities"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
anyhow = "1.0.83"
clap = { version = "4.5.4", features = ["derive"] }
home = "0.5.9"
os_info = "3.8.2"
Expand Down
114 changes: 74 additions & 40 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use std::{
fs::File,
os::unix::fs::PermissionsExt,
path::{Path, PathBuf},
};

use anyhow::{anyhow, Context, Result};
use clap::Parser;
use reqwest::{blocking::Client, header::USER_AGENT};
use tempfile::tempdir;
Expand Down Expand Up @@ -164,7 +166,7 @@ pub struct Reactions {
pub eyes: i64,
}

fn get_latest_release_zip_urls() -> Result<Vec<String>, Box<dyn std::error::Error>> {
fn get_latest_release_zip_urls() -> Result<Vec<String>> {
// Construct the URL for GitHub API to fetch the latest release information
let url = "https://api.github.com/repos/duckdb/duckdb/releases/latest";

Expand All @@ -175,7 +177,7 @@ fn get_latest_release_zip_urls() -> Result<Vec<String>, Box<dyn std::error::Erro
.header(USER_AGENT, "duckup")
.send()?
.text()?;
let release: Release = serde_json::from_str(&text)?;
let release: Release = serde_json::from_str(&text).context("Failed to parse release JSON")?;

// Extract the URL of the zip files from the release information
Ok(release
Expand All @@ -185,33 +187,35 @@ fn get_latest_release_zip_urls() -> Result<Vec<String>, Box<dyn std::error::Erro
.collect())
}

fn download_zip(url: &str, output_dir: &Path) -> Result<PathBuf, Box<dyn std::error::Error>> {
fn download_zip(url: &str, output_dir: &Path) -> Result<PathBuf> {
// Create a reqwest client
let client = Client::new();

// Send a GET request to the provided URL
let mut response = client.get(url).send()?;
let mut response = client.get(url).send().context("Failed to download file")?;

// Check if the request was successful
if !response.status().is_success() {
return Err("Failed to download file".into());
return Err(anyhow!("Failed to download file"));
}

// Open a file to write the downloaded content
let zip_file_path = output_dir.join("downloaded.zip");
let mut file = File::create(&zip_file_path)?;
let mut file = File::create(&zip_file_path).context("Failed to create file for writing to")?;

// Copy the content of the response to the file
let _ = response.copy_to(&mut file);
Ok(zip_file_path)
}

fn unzip_file(zip_file: &Path, output_dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
fn unzip_file(zip_file: &Path, output_dir: &Path) -> Result<()> {
let file = File::open(zip_file)?;
let mut archive = ZipArchive::new(file)?;
let mut archive = ZipArchive::new(file).context("Failed to open file as a zip file")?;

for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
let mut file = archive
.by_index(i)
.context("Failed to get file at zip archive index")?;
let outpath = Path::new(output_dir).join(file.name());

if let Some(parent_dir) = outpath.parent() {
Expand All @@ -231,10 +235,7 @@ fn unzip_file(zip_file: &Path, output_dir: &Path) -> Result<(), Box<dyn std::err
Ok(())
}

fn get_matching_url<'a>(
zip_urls: &'a [String],
info: &'a os_info::Info,
) -> Result<&'a str, Box<dyn std::error::Error>> {
fn get_matching_url<'a>(zip_urls: &'a [String], info: &'a os_info::Info) -> Result<&'a str> {
Ok(zip_urls
.iter()
// Filter to urls with this OS
Expand All @@ -244,21 +245,24 @@ fn get_matching_url<'a>(
// Filter to the correct architecture
// Can't seem to find a list of what all the possible architecture might be,
// so I'm mostly guessing here.
.filter(|url| match info.architecture() {
Some("arm64") | Some("aarch") => url.contains("aarch"),
Some("amd") | Some("x86_64") => url.contains("amd"),
.find(|url| match info.architecture() {
Some("arm64" | "aarch") => url.contains("aarch"),
Some("amd" | "x86_64") => url.contains("amd"),
Some(&_) | None => false,
})
.next()
.ok_or("Could not find any matching URLs")?)
.context("Could not find any matching URLs")?)
}

#[derive(clap::Parser)]
#[command(version, about, long_about = None, arg_required_else_help(true))]
struct CLI {
struct Cli {
/// Run the update, downloading the latest binary, and installing it
#[command(subcommand)]
command: Option<Commands>,

/// Set the folder to put the binary in. Default is `$HOME/.local/bin/`
#[arg(short, long)]
folder_path: Option<String>,
}

#[derive(clap::Subcommand)]
Expand All @@ -267,8 +271,48 @@ enum Commands {
Update,
}

/// Performs all of the actions to download the newest version, and store it in
/// `dest_folder`
fn update(dest_folder: &Path) -> Result<()> {
let zip_urls: Vec<String> = get_latest_release_zip_urls()?.into_iter().collect();
// Get the correct url for this architecture
let info = os_info::get();
let url = get_matching_url(&zip_urls, &info).expect("Could not find a URL for this computer");
println!("Going to download {url}");

// Create a temp directory for unzipping
let zip_dir = tempdir().expect("Could not make tempdir to put zip file in");
std::fs::create_dir_all(zip_dir.path()).expect("Could not create folder within tempdir");
// Download the file
let zip_file_path = download_zip(url, zip_dir.path()).expect("Failed to download the zip file");

println!("Downloaded successfully!");

std::fs::create_dir_all(dest_folder)?;
unzip_file(&zip_file_path, dest_folder)?;
println!("Unzipped successfully into {dest_folder:?}");

// Make file executable
let executable_file = dest_folder.join("duckdb");
let mut perms = std::fs::metadata(&executable_file)
.context("failed to stat file")?
.permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&executable_file, perms).context("failed to set permissions")?;
println!("Successfully set permissions");
// std::process::Command::new("chmod")
// .args([
// "+x",
// dest_folder.to_str().expect("Failed to convert to &str"),
// ])
// .status()
// .expect("Unable to set permissions");

Ok(())
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = CLI::parse();
let cli = Cli::parse();

match cli.command {
// All is good, just continue on with what is below
Expand All @@ -286,26 +330,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
panic!("Running on Mac. Will only work on Linux.")
}

let zip_urls: Vec<String> = get_latest_release_zip_urls()?.into_iter().collect();
// Get the correct url for this architecture
let url = get_matching_url(&zip_urls, &info)?;
println!("Going to download {}", url);

// Create a temp directory for unzipping
let zip_dir = tempdir()?;
std::fs::create_dir_all(zip_dir.path())?;
// Download the file
let zip_file_path = download_zip(url, zip_dir.path())?;

println!("Downloaded successfully!");

let final_path = home::home_dir()
.expect("Could not find home directory")
.join(".local")
.join("bin/");
std::fs::create_dir_all(final_path.clone())?;
unzip_file(&zip_file_path, &final_path.clone())?;
println!("Unzipped successfully into {:?}", final_path);
let dest_folder = match cli.folder_path {
Some(ref p) => PathBuf::from(p),
None => home::home_dir()
.expect("Could not get home directory")
.join(".local")
.join("bin"),
}
.canonicalize()
.with_context(|| format!("Could not canonicalize {:?} handed in", cli.folder_path))?;
update(&dest_folder)?;

Ok(())
}

0 comments on commit c547cfa

Please sign in to comment.