Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Combine CLI id/tree cmds, add find cmd #122

Merged
merged 1 commit into from
Feb 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions omnibor/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ async-walkdir = { version = "1.0.0", optional = true }
clap = { version = "4.5.1", features = ["derive"], optional = true }
futures-lite = { version = "2.2.0", optional = true }
serde_json = { version = "1.0.114", optional = true }
smart-default = { version = "0.7.1", optional = true }

[dev-dependencies]
tokio = { version = "1.36.0", features = ["io-util", "fs"] }
Expand All @@ -37,6 +38,7 @@ build-binary = [
"dep:clap",
"dep:futures-lite",
"dep:serde_json",
"dep:smart-default",
"tokio/fs",
"tokio/rt-multi-thread"
]
Expand Down
176 changes: 99 additions & 77 deletions omnibor/src/bin/omnibor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,25 @@ use futures_lite::stream::StreamExt as _;
use omnibor::ArtifactId;
use omnibor::Sha256;
use serde_json::json;
use smart_default::SmartDefault;
use std::default::Default;
use std::fmt::Display;
use std::fmt::Formatter;
use std::fmt::Result as FmtResult;
use std::fs::File;
use std::path::Path;
use std::path::PathBuf;
use std::process::ExitCode;
use std::str::FromStr;
use tokio::fs::File as AsyncFile;
use tokio::runtime::Runtime;
use url::Url;

fn main() -> ExitCode {
let args = Cli::parse();

let result = match args.command {
Command::Id(ref args) => run_id(args),
Command::Tree(ref args) => run_tree(args),
Command::Find(ref args) => run_find(args),
};

if let Err(e) = result {
Expand Down Expand Up @@ -59,17 +60,18 @@ impl Cli {
fn format(&self) -> Option<Format> {
match &self.command {
Command::Id(args) => Some(args.format),
Command::Tree(args) => Some(args.format),
Command::Find(args) => Some(args.format),
}
}
}

#[derive(Debug, Subcommand)]
enum Command {
/// Print the Artifact ID of the path given.
/// For files, prints their Artifact ID. For directories, recursively prints IDs for all files under it.
Id(IdArgs),
/// Print the Artifact IDs of a directory tree.
Tree(TreeArgs),

/// Find file matching an Artifact ID.
Find(FindArgs),
}

#[derive(Debug, Args)]
Expand All @@ -87,31 +89,25 @@ struct IdArgs {
}

#[derive(Debug, Args)]
struct TreeArgs {
/// Root of the tree to identify.
struct FindArgs {
/// `gitoid` URL to match
url: Url,

/// The root path to search under
path: PathBuf,

/// Output format (can be "plain" or "json")
#[arg(short = 'f', long = "format", default_value_t)]
format: Format,

/// Hash algorithm (can be "sha256")
#[arg(short = 'H', long = "hash", default_value_t)]
hash: SelectedHash,
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, SmartDefault)]
enum Format {
#[default]
Plain,
Json,
}

impl Default for Format {
fn default() -> Self {
Format::Plain
}
}

impl Display for Format {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self {
Expand All @@ -133,17 +129,12 @@ impl FromStr for Format {
}
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, SmartDefault)]
enum SelectedHash {
#[default]
Sha256,
}

impl Default for SelectedHash {
fn default() -> Self {
SelectedHash::Sha256
}
}

impl Display for SelectedHash {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self {
Expand All @@ -168,35 +159,31 @@ impl FromStr for SelectedHash {
*-------------------------------------------------------------------------*/

/// Run the `id` subcommand.
///
/// This command just produces the `gitoid` URL for the given file.
fn run_id(args: &IdArgs) -> Result<()> {
let file = open_file(&args.path)?;
let url = match args.hash {
SelectedHash::Sha256 => sha256_id_file(&file, &args.path)?.url(),
};

match args.format {
Format::Plain => println!("{}", url),
Format::Json => {
let output = json!({ "id": url.to_string() });
println!("{}", output);
}
}
Runtime::new()
.context("failed to initialize the async runtime")?
.block_on(async move {
let mut file = open_async_file(&args.path).await?;

Ok(())
if file_is_dir(&file).await? {
id_directory(&args.path, args.format, args.hash).await
} else {
id_file(&mut file, &args.path, args.format, args.hash).await
}
})
}

/// Run the `tree` subcommand.
///
/// This command produces the `gitoid` URL for all files in a directory tree.
fn run_tree(args: &TreeArgs) -> Result<()> {
let TreeArgs { path, format, hash } = args;
/// Run the `find` subcommand.
fn run_find(args: &FindArgs) -> Result<()> {
let FindArgs { url, path, format } = args;

Runtime::new()
.context("failed to initialize the async runtime")?
.block_on(async move {
let mut entries = WalkDir::new(path);
let id = ArtifactId::<Sha256>::id_url(url.clone())?;
let url = id.url();

let mut entries = WalkDir::new(&path);

loop {
match entries.next().await {
Expand All @@ -209,25 +196,12 @@ fn run_tree(args: &TreeArgs) -> Result<()> {
continue;
}

let mut file = open_async_file(path).await?;

// This 'match' is included to ensure this gets updated
// if we ever add a new hash algorithm.
let url = match *hash {
SelectedHash::Sha256 => {
sha256_id_async_file(&mut file, path).await?.url()
}
};

match *format {
Format::Plain => println!("{} => {}", path.display(), url),
Format::Json => println!(
"{}",
json!({
"path": path.display().to_string(),
"id": url.to_string()
})
),
let mut file = open_async_file(&path).await?;
let file_url = hash_file(SelectedHash::Sha256, &mut file, &path).await?;

if url == file_url {
print_id(&path, &url, *format);
return Ok(());
}
}
}
Expand All @@ -241,6 +215,60 @@ fn run_tree(args: &TreeArgs) -> Result<()> {
* Helper Functions
*-------------------------------------------------------------------------*/

// Identify, recursively, all the files under a directory.
async fn id_directory(path: &Path, format: Format, hash: SelectedHash) -> Result<()> {
let mut entries = WalkDir::new(path);

loop {
match entries.next().await {
None => break,
Some(Err(e)) => print_error(e, format),
Some(Ok(entry)) => {
let path = &entry.path();

if entry_is_dir(&entry).await? {
continue;
}

let mut file = open_async_file(&path).await?;
id_file(&mut file, &path, format, hash).await?;
}
}
}

Ok(())
}

/// Identify a single file.
async fn id_file(
file: &mut AsyncFile,
path: &Path,
format: Format,
hash: SelectedHash,
) -> Result<()> {
let url = hash_file(hash, file, &path).await?;
print_id(path, &url, format);
Ok(())
}

/// Hash the file and produce a `gitoid`-scheme URL.
async fn hash_file(hash: SelectedHash, file: &mut AsyncFile, path: &Path) -> Result<Url> {
match hash {
SelectedHash::Sha256 => sha256_id_async_file(file, &path).await.map(|id| id.url()),
}
}

/// Print IDs for path and file in the chosen format.
fn print_id(path: &Path, url: &Url, format: Format) {
let path = path.display().to_string();
let url = url.to_string();

match format {
Format::Plain => println!("path: {}, id: {}", path, url),
Format::Json => println!("{}", json!({ "path": path, "id": url })),
}
}

/// Print an error, respecting formatting.
fn print_error<E: Into<Error>>(error: E, format: Format) {
fn _print_error(error: Error, format: Format) {
Expand All @@ -264,6 +292,11 @@ fn print_plain_error(error: Error) {
eprintln!("error: {}", error);
}

/// Check if the file is for a directory.
async fn file_is_dir(file: &AsyncFile) -> Result<bool> {
Ok(file.metadata().await.map(|meta| meta.is_dir())?)
}

/// Check if the entry is for a directory.
async fn entry_is_dir(entry: &AsyncDirEntry) -> Result<bool> {
entry
Expand All @@ -278,24 +311,13 @@ async fn entry_is_dir(entry: &AsyncDirEntry) -> Result<bool> {
.map(|file_type| file_type.is_dir())
}

/// Open a file.
fn open_file(path: &Path) -> Result<File> {
File::open(path).with_context(|| format!("failed to open '{}'", path.display()))
}

/// Open an asynchronous file.
async fn open_async_file(path: &Path) -> Result<AsyncFile> {
AsyncFile::open(path)
.await
.with_context(|| format!("failed to open file '{}'", path.display()))
}

/// Identify a file using a SHA-256 hash.
fn sha256_id_file(file: &File, path: &Path) -> Result<ArtifactId<Sha256>> {
ArtifactId::id_reader(file)
.with_context(|| format!("failed to produce Artifact ID for '{}'", path.display()))
}

/// Identify a file using a SHA-256 hash.
async fn sha256_id_async_file(file: &mut AsyncFile, path: &Path) -> Result<ArtifactId<Sha256>> {
ArtifactId::id_async_reader(file)
Expand Down