From 4a03ee68244df03d3fb25c103102b47c4258766b Mon Sep 17 00:00:00 2001 From: Ryan Bernstein Date: Thu, 16 Nov 2023 15:20:54 -0500 Subject: [PATCH 1/4] Add get_url command --- src/cli/cmd/get_url.rs | 199 +++++++++++++++++++++++++++++++++++++++++ src/cli/cmd/mod.rs | 2 + src/main.rs | 1 + 3 files changed, 202 insertions(+) create mode 100644 src/cli/cmd/get_url.rs diff --git a/src/cli/cmd/get_url.rs b/src/cli/cmd/get_url.rs new file mode 100644 index 00000000..27149d38 --- /dev/null +++ b/src/cli/cmd/get_url.rs @@ -0,0 +1,199 @@ +use std::process::ExitCode; + +use clap::Parser; +use color_eyre::eyre::WrapErr; +use reqwest::header::{HeaderValue, ACCEPT, AUTHORIZATION}; +use serde::Deserialize; + +use super::CommandExecute; + +/// Prints the URL of the given flake +#[derive(Parser, Debug)] +pub(crate) struct GetURLSubcommand { + /// The name of the flake input. + /// + /// If not provided, it will be inferred from the provided input URL (if possible). + #[clap(long)] + pub(crate) input_name: Option, + /// The flake reference to add as an input. + /// + /// A reference in the form of `NixOS/nixpkgs` or `NixOS/nixpkgs/0.2305.*` (without a URL + /// scheme) will be inferred as a FlakeHub input. + pub(crate) input_ref: String, + + #[clap(from_global)] + api_addr: url::Url, +} + +#[async_trait::async_trait] +impl CommandExecute for GetURLSubcommand { + async fn execute(self) -> color_eyre::Result { + let (_, flake_input_url) = + infer_flake_input_name_url(self.api_addr, self.input_ref, self.input_name).await?; + println!("{}", flake_input_url); + + // let input_url_attr_path: VecDeque = [ + // String::from("inputs"), + // flake_input_name.clone(), + // String::from("url"), + // ] + // .into(); + + // let new_flake_contents = flake::upsert_flake_input( + // &parsed.expression, + // flake_input_name, + // flake_input_url, + // flake_contents, + // input_url_attr_path, + // self.insertion_location, + // )?; + + // if self.dry_run { + // println!("{new_flake_contents}"); + // } else { + // tokio::fs::write(self.flake_path, new_flake_contents).await?; + // } + + Ok(ExitCode::SUCCESS) + } +} + +#[tracing::instrument(skip_all)] +async fn infer_flake_input_name_url( + api_addr: url::Url, + flake_ref: String, + input_name: Option, +) -> color_eyre::Result<(String, url::Url)> { + let flake_ref = flake_ref.trim_end_matches('/'); + let url_result = flake_ref.parse::(); + + match url_result { + // A URL like `github:nixos/nixpkgs` + Ok(parsed_url) if parsed_url.host().is_none() => { + // TODO: validate that the format of all Nix-supported schemes allows us to do this; + // else, have an allowlist of schemes + let mut path_parts = parsed_url.path().split('/'); + path_parts.next(); // e.g. in `fh:` or `github:`, the org name + + match (input_name, path_parts.next()) { + (Some(input_name), _) => Ok((input_name, parsed_url)), + (None, Some(input_name)) => Ok((input_name.to_string(), parsed_url)), + (None, _) => Err(color_eyre::eyre::eyre!( + "cannot infer an input name for {parsed_url}; please specify one with the `--input-name` flag" + )) + } + } + // A URL like `nixos/nixpkgs` or `nixos/nixpkgs/0.2305` + Err(url::ParseError::RelativeUrlWithoutBase) => { + let (org, project, version) = match flake_ref.split('/').collect::>()[..] { + // `nixos/nixpkgs/0.2305` + [org, project, version] => { + let version = version.strip_suffix(".tar.gz").unwrap_or(version); + let version = version.strip_prefix('v').unwrap_or(version); + semver::VersionReq::parse(version).map_err(|_| { + color_eyre::eyre::eyre!( + "version '{version}' was not a valid SemVer version requirement" + ) + })?; + + (org, project, Some(version)) + } + // `nixos/nixpkgs` + [org, project] => (org, project, None), + _ => Err(color_eyre::eyre::eyre!( + "flakehub input did not match the expected format of \ + `org/project` or `org/project/version`" + ))?, + }; + + let (flakehub_input, url) = + get_flakehub_project_and_url(&api_addr, org, project, version).await?; + + if let Some(input_name) = input_name { + Ok((input_name, url)) + } else { + Ok((flakehub_input, url)) + } + } + // A URL like `https://flakehub.com/f/NixOS/nixpkgs/*.tar.gz` + Ok(parsed_url) => { + if let Some(input_name) = input_name { + Ok((input_name, parsed_url)) + } else { + Err(color_eyre::eyre::eyre!( + "cannot infer an input name for `{flake_ref}`; please specify one with the `--input-name` flag" + ))? + } + } + Err(e) => Err(e)?, + } +} + +#[tracing::instrument(skip_all)] +pub(crate) async fn get_flakehub_project_and_url( + api_addr: &url::Url, + org: &str, + project: &str, + version: Option<&str>, +) -> color_eyre::Result<(String, url::Url)> { + let mut headers = reqwest::header::HeaderMap::new(); + headers.insert(ACCEPT, HeaderValue::from_static("application/json")); + + let xdg = xdg::BaseDirectories::new()?; + // $XDG_CONFIG_HOME/fh/auth; basically ~/.config/fh/auth + let token_path = xdg.get_config_file("flakehub/auth"); + + if token_path.exists() { + let token = tokio::fs::read_to_string(&token_path) + .await + .wrap_err_with(|| format!("Could not open {}", token_path.display()))?; + + headers.insert( + AUTHORIZATION, + HeaderValue::from_str(&format!("Bearer {token}"))?, + ); + } + + let client = reqwest::Client::builder() + .user_agent(crate::APP_USER_AGENT) + .default_headers(headers) + .build()?; + + let mut flakehub_json_url = api_addr.clone(); + { + let mut path_segments_mut = flakehub_json_url + .path_segments_mut() + .expect("flakehub url cannot be base (this should never happen)"); + + match version { + Some(version) => { + path_segments_mut + .push("version") + .push(org) + .push(project) + .push(version); + } + None => { + path_segments_mut.push("f").push(org).push(project); + } + } + } + + #[derive(Debug, Deserialize)] + struct ProjectCanonicalNames { + project: String, + // FIXME: detect Nix version and strip .tar.gz if it supports it + pretty_download_url: url::Url, + } + + let res = client.get(&flakehub_json_url.to_string()).send().await?; + + if let Err(e) = res.error_for_status_ref() { + let err_text = res.text().await?; + return Err(e).wrap_err(err_text)?; + }; + + let res = res.json::().await?; + + Ok((res.project, res.pretty_download_url)) +} diff --git a/src/cli/cmd/mod.rs b/src/cli/cmd/mod.rs index 6be19dc9..70e4e0f8 100644 --- a/src/cli/cmd/mod.rs +++ b/src/cli/cmd/mod.rs @@ -2,6 +2,7 @@ pub(crate) mod add; pub(crate) mod completion; pub(crate) mod convert; pub(crate) mod eject; +pub(crate) mod get_url; pub(crate) mod init; pub(crate) mod list; pub(crate) mod login; @@ -50,6 +51,7 @@ pub trait CommandExecute { #[derive(clap::Subcommand)] pub(crate) enum FhSubcommands { Add(add::AddSubcommand), + GetURL(get_url::GetURLSubcommand), Completion(completion::CompletionSubcommand), Init(init::InitSubcommand), List(list::ListSubcommand), diff --git a/src/main.rs b/src/main.rs index 558565f5..2f6086b0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -30,6 +30,7 @@ async fn main() -> color_eyre::Result { match cli.subcommand { FhSubcommands::Add(add) => add.execute().await, + FhSubcommands::GetURL(get_url) => get_url.execute().await, FhSubcommands::Init(init) => init.execute().await, FhSubcommands::List(list) => list.execute().await, FhSubcommands::Search(search) => search.execute().await, From adb1ff29d0d81fda8ef032e085bd7c699500478a Mon Sep 17 00:00:00 2001 From: Ryan Bernstein Date: Thu, 16 Nov 2023 15:38:19 -0500 Subject: [PATCH 2/4] remove unnecessary code from get_url command --- src/cli/cmd/add/mod.rs | 2 +- src/cli/cmd/get_url.rs | 168 +---------------------------------------- 2 files changed, 3 insertions(+), 167 deletions(-) diff --git a/src/cli/cmd/add/mod.rs b/src/cli/cmd/add/mod.rs index c493c8bd..3bb118c4 100644 --- a/src/cli/cmd/add/mod.rs +++ b/src/cli/cmd/add/mod.rs @@ -114,7 +114,7 @@ pub(crate) async fn load_flake( } #[tracing::instrument(skip_all)] -async fn infer_flake_input_name_url( +pub(crate) async fn infer_flake_input_name_url( api_addr: url::Url, flake_ref: String, input_name: Option, diff --git a/src/cli/cmd/get_url.rs b/src/cli/cmd/get_url.rs index 27149d38..73e1735f 100644 --- a/src/cli/cmd/get_url.rs +++ b/src/cli/cmd/get_url.rs @@ -1,9 +1,6 @@ use std::process::ExitCode; use clap::Parser; -use color_eyre::eyre::WrapErr; -use reqwest::header::{HeaderValue, ACCEPT, AUTHORIZATION}; -use serde::Deserialize; use super::CommandExecute; @@ -29,171 +26,10 @@ pub(crate) struct GetURLSubcommand { impl CommandExecute for GetURLSubcommand { async fn execute(self) -> color_eyre::Result { let (_, flake_input_url) = - infer_flake_input_name_url(self.api_addr, self.input_ref, self.input_name).await?; - println!("{}", flake_input_url); - - // let input_url_attr_path: VecDeque = [ - // String::from("inputs"), - // flake_input_name.clone(), - // String::from("url"), - // ] - // .into(); + crate::cli::cmd::add::infer_flake_input_name_url(self.api_addr, self.input_ref, self.input_name).await?; - // let new_flake_contents = flake::upsert_flake_input( - // &parsed.expression, - // flake_input_name, - // flake_input_url, - // flake_contents, - // input_url_attr_path, - // self.insertion_location, - // )?; - - // if self.dry_run { - // println!("{new_flake_contents}"); - // } else { - // tokio::fs::write(self.flake_path, new_flake_contents).await?; - // } + println!("{}", flake_input_url); Ok(ExitCode::SUCCESS) } } - -#[tracing::instrument(skip_all)] -async fn infer_flake_input_name_url( - api_addr: url::Url, - flake_ref: String, - input_name: Option, -) -> color_eyre::Result<(String, url::Url)> { - let flake_ref = flake_ref.trim_end_matches('/'); - let url_result = flake_ref.parse::(); - - match url_result { - // A URL like `github:nixos/nixpkgs` - Ok(parsed_url) if parsed_url.host().is_none() => { - // TODO: validate that the format of all Nix-supported schemes allows us to do this; - // else, have an allowlist of schemes - let mut path_parts = parsed_url.path().split('/'); - path_parts.next(); // e.g. in `fh:` or `github:`, the org name - - match (input_name, path_parts.next()) { - (Some(input_name), _) => Ok((input_name, parsed_url)), - (None, Some(input_name)) => Ok((input_name.to_string(), parsed_url)), - (None, _) => Err(color_eyre::eyre::eyre!( - "cannot infer an input name for {parsed_url}; please specify one with the `--input-name` flag" - )) - } - } - // A URL like `nixos/nixpkgs` or `nixos/nixpkgs/0.2305` - Err(url::ParseError::RelativeUrlWithoutBase) => { - let (org, project, version) = match flake_ref.split('/').collect::>()[..] { - // `nixos/nixpkgs/0.2305` - [org, project, version] => { - let version = version.strip_suffix(".tar.gz").unwrap_or(version); - let version = version.strip_prefix('v').unwrap_or(version); - semver::VersionReq::parse(version).map_err(|_| { - color_eyre::eyre::eyre!( - "version '{version}' was not a valid SemVer version requirement" - ) - })?; - - (org, project, Some(version)) - } - // `nixos/nixpkgs` - [org, project] => (org, project, None), - _ => Err(color_eyre::eyre::eyre!( - "flakehub input did not match the expected format of \ - `org/project` or `org/project/version`" - ))?, - }; - - let (flakehub_input, url) = - get_flakehub_project_and_url(&api_addr, org, project, version).await?; - - if let Some(input_name) = input_name { - Ok((input_name, url)) - } else { - Ok((flakehub_input, url)) - } - } - // A URL like `https://flakehub.com/f/NixOS/nixpkgs/*.tar.gz` - Ok(parsed_url) => { - if let Some(input_name) = input_name { - Ok((input_name, parsed_url)) - } else { - Err(color_eyre::eyre::eyre!( - "cannot infer an input name for `{flake_ref}`; please specify one with the `--input-name` flag" - ))? - } - } - Err(e) => Err(e)?, - } -} - -#[tracing::instrument(skip_all)] -pub(crate) async fn get_flakehub_project_and_url( - api_addr: &url::Url, - org: &str, - project: &str, - version: Option<&str>, -) -> color_eyre::Result<(String, url::Url)> { - let mut headers = reqwest::header::HeaderMap::new(); - headers.insert(ACCEPT, HeaderValue::from_static("application/json")); - - let xdg = xdg::BaseDirectories::new()?; - // $XDG_CONFIG_HOME/fh/auth; basically ~/.config/fh/auth - let token_path = xdg.get_config_file("flakehub/auth"); - - if token_path.exists() { - let token = tokio::fs::read_to_string(&token_path) - .await - .wrap_err_with(|| format!("Could not open {}", token_path.display()))?; - - headers.insert( - AUTHORIZATION, - HeaderValue::from_str(&format!("Bearer {token}"))?, - ); - } - - let client = reqwest::Client::builder() - .user_agent(crate::APP_USER_AGENT) - .default_headers(headers) - .build()?; - - let mut flakehub_json_url = api_addr.clone(); - { - let mut path_segments_mut = flakehub_json_url - .path_segments_mut() - .expect("flakehub url cannot be base (this should never happen)"); - - match version { - Some(version) => { - path_segments_mut - .push("version") - .push(org) - .push(project) - .push(version); - } - None => { - path_segments_mut.push("f").push(org).push(project); - } - } - } - - #[derive(Debug, Deserialize)] - struct ProjectCanonicalNames { - project: String, - // FIXME: detect Nix version and strip .tar.gz if it supports it - pretty_download_url: url::Url, - } - - let res = client.get(&flakehub_json_url.to_string()).send().await?; - - if let Err(e) = res.error_for_status_ref() { - let err_text = res.text().await?; - return Err(e).wrap_err(err_text)?; - }; - - let res = res.json::().await?; - - Ok((res.project, res.pretty_download_url)) -} From e6fcfc2d2911efd1827fe0f473180f67913c704d Mon Sep 17 00:00:00 2001 From: Ryan Bernstein Date: Thu, 16 Nov 2023 15:51:55 -0500 Subject: [PATCH 3/4] simplify get-url interface --- src/cli/cmd/get_url.rs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/cli/cmd/get_url.rs b/src/cli/cmd/get_url.rs index 73e1735f..3f728540 100644 --- a/src/cli/cmd/get_url.rs +++ b/src/cli/cmd/get_url.rs @@ -4,18 +4,12 @@ use clap::Parser; use super::CommandExecute; -/// Prints the URL of the given flake +/// Prints the URL of a given FlakeHub flake #[derive(Parser, Debug)] pub(crate) struct GetURLSubcommand { - /// The name of the flake input. + /// The FlakeHub reference to print as a URL /// - /// If not provided, it will be inferred from the provided input URL (if possible). - #[clap(long)] - pub(crate) input_name: Option, - /// The flake reference to add as an input. - /// - /// A reference in the form of `NixOS/nixpkgs` or `NixOS/nixpkgs/0.2305.*` (without a URL - /// scheme) will be inferred as a FlakeHub input. + /// A FlakeHub reference is of a form like `NixOS/nixpkgs` or `NixOS/nixpkgs/0.2305.*` pub(crate) input_ref: String, #[clap(from_global)] @@ -26,7 +20,7 @@ pub(crate) struct GetURLSubcommand { impl CommandExecute for GetURLSubcommand { async fn execute(self) -> color_eyre::Result { let (_, flake_input_url) = - crate::cli::cmd::add::infer_flake_input_name_url(self.api_addr, self.input_ref, self.input_name).await?; + crate::cli::cmd::add::infer_flake_input_name_url(self.api_addr, self.input_ref, None).await?; println!("{}", flake_input_url); From d3a339aa4f81b07aab071521b1be88651ea33532 Mon Sep 17 00:00:00 2001 From: Ryan Bernstein Date: Thu, 16 Nov 2023 18:06:04 -0500 Subject: [PATCH 4/4] fix formatting on get-url --- src/cli/cmd/get_url.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/cli/cmd/get_url.rs b/src/cli/cmd/get_url.rs index 3f728540..917150da 100644 --- a/src/cli/cmd/get_url.rs +++ b/src/cli/cmd/get_url.rs @@ -20,7 +20,8 @@ pub(crate) struct GetURLSubcommand { impl CommandExecute for GetURLSubcommand { async fn execute(self) -> color_eyre::Result { let (_, flake_input_url) = - crate::cli::cmd::add::infer_flake_input_name_url(self.api_addr, self.input_ref, None).await?; + crate::cli::cmd::add::infer_flake_input_name_url(self.api_addr, self.input_ref, None) + .await?; println!("{}", flake_input_url);