Skip to content

Commit

Permalink
Add blob related commands (#17)
Browse files Browse the repository at this point in the history
* Create blob download command

* Add file upload to blobs
  • Loading branch information
nikola-jokic authored Feb 28, 2025
1 parent 1caa48a commit 6c2590f
Show file tree
Hide file tree
Showing 2 changed files with 163 additions and 10 deletions.
105 changes: 97 additions & 8 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
use crate::client::{Client, HTTPClient};
use crate::validation;
use clap::Args;
use clap::{CommandFactory, Parser, Subcommand, ValueHint};
use clap_complete::generate;
use clap_complete::Shell;
use clap::{Args, CommandFactory, Parser, Subcommand, ValueHint};
use clap_complete::{generate, Shell};
use error_stack::{Context, Report, Result, ResultExt};
use serde_json::Value;
use std::collections::BTreeMap;
use std::env;
use std::fmt;
use std::io;
use std::path::PathBuf;
use std::{env, fmt, fs, io};
use uuid::Uuid;

#[derive(Parser, Debug)]
Expand Down Expand Up @@ -50,6 +46,9 @@ enum Commands {
#[command(subcommand)]
Scan(Scan),

#[command(subcommand)]
Blob(Blob),

/// Shell completion commands
#[command(arg_required_else_help = true)]
Completion(Completion),
Expand All @@ -67,6 +66,7 @@ impl Commands {
Commands::Completion(_) => unreachable!(),
Commands::Job(job) => job.run(client)?,
Commands::Scan(scan) => scan.run(client)?,
Commands::Blob(blob) => blob.run(client)?,
}

Ok(())
Expand Down Expand Up @@ -204,7 +204,7 @@ impl Job {
.change_context(CliError)
.attach_printable("Failed to download file")?;

let mut fwriter = std::fs::File::create(output)
let mut fwriter = fs::File::create(output)
.change_context(CliError)
.attach_printable("Failed to create file")?;

Expand Down Expand Up @@ -330,6 +330,78 @@ impl Scan {
}
}

#[derive(Subcommand, Debug, Clone)]
enum Blob {
Download {
#[clap(short, long, required = true)]
path: String,
#[clap(short, long, env = "BOUNTYHUB_OUTPUT")]
#[arg(value_hint = ValueHint::DirPath)]
output: Option<String>,
},
Upload {
/// src is the source file on the local filesystem
#[clap(short, long, required = true)]
#[arg(value_hint = ValueHint::DirPath)]
src: String,

/// dst is the destination path on bountyhub.org blobs
#[clap(long, required = true)]
dst: String,
},
}

impl Blob {
fn run<C>(self, client: C) -> Result<(), CliError>
where
C: Client,
{
match self {
Blob::Download { path, output } => {
let output = match output {
Some(output) => {
let output = PathBuf::from(output);
if output.is_dir() {
output.join(&path)
} else {
output
}
}
None => env::current_dir()
.change_context(CliError)
.attach_printable("Failed to get current directory")?
.join(&path),
};

let mut freader = client
.download_blob_file(&path)
.change_context(CliError)
.attach_printable("Failed to download file")?;

let mut fwriter = fs::File::create(output)
.change_context(CliError)
.attach_printable("Failed to create file")?;

std::io::copy(&mut *freader, &mut fwriter)
.change_context(CliError)
.attach_printable("failed to write file")?;
}
Blob::Upload { src, dst } => {
let freader = fs::File::open(&src)
.change_context(CliError)
.attach_printable(format!("failed to open file '{src}'"))?;

client
.upload_blob_file(freader, dst.as_str())
.change_context(CliError)
.attach_printable("failed to call upload blob file")?;
}
}

Ok(())
}
}

#[cfg(test)]
mod job_tests {
use super::*;
Expand Down Expand Up @@ -479,6 +551,23 @@ mod job_tests {
assert_eq!(result.0, "k");
assert_eq!(result.1, "v=a");
}

#[test]
fn test_download_blob_file() {
let cmd = Blob::Download {
path: "file.txt".to_string(),
output: None,
};
let mut client = MockClient::new();
client
.expect_download_blob_file()
.with(function(|v| v == "file.txt"))
.times(1)
.returning(|_| Err(Report::new(ClientError)));

let result = cmd.run(client);
assert!(result.is_err(), "expected error, got ok");
}
}

#[derive(Args, Debug)]
Expand Down
68 changes: 66 additions & 2 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeMap;
use std::fmt;
use std::fs::File;
use std::io::Read;
use std::time::Duration;
use ureq::Agent;
Expand All @@ -17,6 +18,12 @@ pub struct DispatchScanRequest {
pub inputs: Option<BTreeMap<String, Value>>,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct UploadBlobFileRequest {
pub path: String,
}

#[cfg_attr(test, automock)]
pub trait Client {
fn download_job_result_file(
Expand All @@ -43,6 +50,13 @@ pub trait Client {
scan_name: String,
inputs: Option<BTreeMap<String, Value>>,
) -> Result<(), ClientError>;

fn download_blob_file(
&self,
path: &str,
) -> Result<Box<dyn Read + Send + Sync + 'static>, ClientError>;

fn upload_blob_file(&self, file: File, dst: &str) -> Result<(), ClientError>;
}

pub struct HTTPClient {
Expand Down Expand Up @@ -98,7 +112,7 @@ impl Client for HTTPClient {
job_id: Uuid,
) -> Result<Box<dyn Read + Send + Sync + 'static>, ClientError> {
let url = format!("{0}/api/v0/projects/{project_id}/workflows/{workflow_id}/revisions/{revision_id}/jobs/{job_id}/result", self.bountyhub_domain);
let FileResult { url } = self
let UrlResponse { url } = self
.bountyhub_agent
.get(url.as_str())
.set("Authorization", self.authorization.as_str())
Expand Down Expand Up @@ -166,6 +180,56 @@ impl Client for HTTPClient {
Err(e) => Err(Report::new(ClientError).attach_printable(e.to_string())),
}
}

fn download_blob_file(
&self,
path: &str,
) -> Result<Box<dyn Read + Send + Sync + 'static>, ClientError> {
let url = format!("{0}/api/v0/blobs/{path}", self.bountyhub_domain);
let UrlResponse { url } = self
.bountyhub_agent
.get(url.as_str())
.set("Authorization", self.authorization.as_str())
.call()
.change_context(ClientError)
.attach_printable("Failed to request download")?
.into_json()
.change_context(ClientError)
.attach_printable("Failed to parse response")?;

let res = self
.file_agent
.get(url.as_str())
.call()
.change_context(ClientError)
.attach_printable("Failed to download file")?;

Ok(res.into_reader())
}

fn upload_blob_file(&self, file: File, dst: &str) -> Result<(), ClientError> {
let url = format!("{0}/api/v0/blobs/files", self.bountyhub_domain);
let UrlResponse { url } = self
.bountyhub_agent
.post(url.as_str())
.set("Authorization", self.authorization.as_str())
.send_json(UploadBlobFileRequest {
path: dst.to_string(),
})
.change_context(ClientError)
.attach_printable("failed to create upload link")?
.into_json()
.change_context(ClientError)
.attach_printable("failed to parse response")?;

self.file_agent
.put(&url)
.send(file)
.change_context(ClientError)
.attach_printable("failed to send file")?;

Ok(())
}
}

#[derive(Debug)]
Expand All @@ -180,6 +244,6 @@ impl fmt::Display for ClientError {
impl Context for ClientError {}

#[derive(Deserialize, Debug)]
struct FileResult {
struct UrlResponse {
url: String,
}

0 comments on commit 6c2590f

Please sign in to comment.