Skip to content

Commit 701d4d4

Browse files
committed
chore: initial implementation
chore: fix linting
1 parent 0ab98e1 commit 701d4d4

11 files changed

Lines changed: 260 additions & 2 deletions

File tree

Cargo.lock

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ serde = { version = "1.0.210", features = ["derive"] }
2828
serde_json = "1.0.128"
2929
snafu = { version = "0.8.5" }
3030
tokio = { version = "1", features = ["full"] }
31+
tokio-util = { version = "0.7", features = ["codec"] }
32+
walkdir = "2"
3133
futures = { version = "0.3" }
3234
regex = { version = "1.12.3" }
3335
iso8601-timestamp = { version = "0.2.17" }

src/cli.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ pub async fn execute_cmd(opts: MainOpts) -> Result<(), CmdError> {
2525

2626
#[cfg(feature = "user-doc")]
2727
SubCommand::UserDoc(input) => input.exec(ctx).await?,
28+
29+
SubCommand::Dataset(input) => input.exec(ctx).await?,
2830
};
2931
Ok(())
3032
}

src/cli/cmd.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
pub mod dataset;
12
pub mod login;
23
pub mod project;
34
pub mod update;
@@ -109,6 +110,9 @@ pub enum CmdError {
109110
#[cfg(feature = "user-doc")]
110111
#[snafu(display("UserDoc - {}", source))]
111112
UserDoc { source: userdoc::Error },
113+
114+
#[snafu(display("Dataset - {}", source))]
115+
Dataset { source: dataset::Error },
112116
}
113117

114118
impl From<version::Error> for CmdError {
@@ -140,3 +144,9 @@ impl From<login::Error> for CmdError {
140144
CmdError::Login { source }
141145
}
142146
}
147+
148+
impl From<dataset::Error> for CmdError {
149+
fn from(source: dataset::Error) -> Self {
150+
CmdError::Dataset { source }
151+
}
152+
}

src/cli/cmd/dataset.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
pub mod deposit;
2+
pub mod zenodo;
3+
4+
use super::Context;
5+
use clap::Parser;
6+
use snafu::Snafu;
7+
8+
#[derive(Debug, Snafu)]
9+
pub enum Error {
10+
#[snafu(display("Error with deposit: {}", source))]
11+
Deposit { source: deposit::Error },
12+
}
13+
14+
/// Sub command for managing datasets
15+
#[derive(Parser, Debug)]
16+
pub struct Input {
17+
#[command(subcommand)]
18+
pub subcmd: DatasetCommand,
19+
}
20+
21+
#[derive(Parser, Debug)]
22+
pub enum DatasetCommand {
23+
Deposit {
24+
#[command(subcommand)]
25+
cmd: DepositCommand,
26+
},
27+
}
28+
29+
#[derive(Parser, Debug)]
30+
pub enum DepositCommand {
31+
#[command()]
32+
CopyFiles(deposit::CopyInput),
33+
}
34+
35+
impl Input {
36+
pub async fn exec(&self, _ctx: Context) -> Result<(), Error> {
37+
match self.subcmd {
38+
DatasetCommand::Deposit { cmd: _ } => {
39+
print!("Hi");
40+
Ok(())
41+
}
42+
}
43+
}
44+
}

src/cli/cmd/dataset/deposit.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
use super::zenodo;
2+
use super::Context;
3+
use clap::{Parser, ValueEnum};
4+
use snafu::{ResultExt, Snafu};
5+
use std::env::VarError;
6+
use std::path::PathBuf;
7+
8+
#[derive(Debug, Clone, ValueEnum)]
9+
pub enum Provider {
10+
Zenodo,
11+
}
12+
13+
#[derive(Debug, Snafu)]
14+
pub enum Error {
15+
#[snafu(display("A dataset deposit error occured: {}", source))]
16+
Zenodo { source: zenodo::Error },
17+
18+
#[snafu(display("Env variable error: {}", source))]
19+
EnvVarMissing { source: VarError },
20+
}
21+
22+
/// Copies the data from a location into a data deposit
23+
#[derive(Parser, Debug)]
24+
pub struct CopyInput {
25+
/// The id of the deposit the data should be copied to.
26+
#[arg()]
27+
pub deposit_id: String,
28+
29+
/// The provider for the dataset
30+
#[arg()]
31+
pub provider: Provider,
32+
33+
/// The source directory where the files to be copied can be found.
34+
#[arg()]
35+
pub source_dir: PathBuf,
36+
}
37+
38+
impl CopyInput {
39+
pub async fn exec(&self, _ctx: Context) -> Result<(), Error> {
40+
match self.provider {
41+
Provider::Zenodo => {
42+
let token = std::env::var("ZENODO_API_KEY").context(EnvVarMissingSnafu)?;
43+
let clnt = zenodo::ZenodoClient::new(token);
44+
clnt.upload_files(&self.deposit_id, &self.source_dir)
45+
.await
46+
.context(ZenodoSnafu)
47+
}
48+
}
49+
}
50+
}

src/cli/cmd/dataset/zenodo.rs

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
use mime_guess::from_path;
2+
use reqwest;
3+
use serde::de::DeserializeOwned;
4+
use snafu::{ResultExt, Snafu};
5+
use std::path::{Path, PathBuf};
6+
use tokio::fs::File;
7+
use tokio_util::codec::{BytesCodec, FramedRead};
8+
use url::{ParseError, Url};
9+
use walkdir::WalkDir;
10+
11+
use std::io;
12+
use std::sync::LazyLock;
13+
14+
pub struct ZenodoClient {
15+
http_client: reqwest::Client,
16+
base_url: Url,
17+
token: String,
18+
}
19+
20+
static BASE_URL: LazyLock<Url> =
21+
LazyLock::new(|| Url::parse("https://zenodo.org").expect("Invalid Base URL config"));
22+
23+
#[derive(Debug, Snafu)]
24+
pub enum Error {
25+
#[snafu(display("A zendoo client error occured: {}", source))]
26+
Reqwest { source: reqwest::Error },
27+
#[snafu(display("A directory listing error occured: {}", source))]
28+
DirWalk { source: walkdir::Error },
29+
#[snafu(display("An error occured reading the response: {}", source))]
30+
DeserializeResp { source: reqwest::Error },
31+
#[snafu(display("An error occured reading the response: {}", source))]
32+
FileReading { source: io::Error },
33+
#[snafu(display("An error occured parsing the file path: {}", fp.to_string_lossy()))]
34+
FileParsing { fp: PathBuf },
35+
#[snafu(display("An error occured parsing the url {}", source))]
36+
UrlParse { source: ParseError },
37+
}
38+
39+
impl ZenodoClient {
40+
pub fn new(token: String) -> ZenodoClient {
41+
let http_client = reqwest::Client::new();
42+
ZenodoClient {
43+
http_client,
44+
base_url: BASE_URL.clone(),
45+
token,
46+
}
47+
}
48+
49+
fn make_url(&self, path: &str) -> Result<Url, Error> {
50+
self.base_url.join(path).context(UrlParseSnafu)
51+
}
52+
53+
pub async fn get_deposition<R: DeserializeOwned>(
54+
&self,
55+
deposition_id: &str,
56+
) -> Result<R, Error> {
57+
let endpoint = self.make_url(&format!("/api/deposit/depositions/{deposition_id}"))?;
58+
let res = self
59+
.http_client
60+
.get(endpoint)
61+
.bearer_auth(&self.token)
62+
.send()
63+
.await
64+
.context(ReqwestSnafu)?;
65+
// res.error_for_status().context(ReqwestSnafu)?;
66+
res.json::<R>().await.context(DeserializeRespSnafu)
67+
}
68+
69+
pub async fn upload_files(&self, deposition_id: &str, source_path: &Path) -> Result<(), Error> {
70+
for f in WalkDir::new(source_path) {
71+
self.upload_file::<()>(deposition_id, f.context(DirWalkSnafu)?.path())
72+
.await?;
73+
}
74+
Ok(())
75+
}
76+
77+
pub async fn list_files<R: DeserializeOwned>(&self, deposition_id: &str) -> Result<R, Error> {
78+
let endpoint = self.make_url(&format!("/api/deposit/depositions/{deposition_id}/files"))?;
79+
let res = self
80+
.http_client
81+
.get(endpoint)
82+
.bearer_auth(&self.token)
83+
.send()
84+
.await
85+
.context(ReqwestSnafu)?;
86+
res.json::<R>().await.context(DeserializeRespSnafu)
87+
}
88+
89+
async fn upload_file<R: DeserializeOwned>(
90+
&self,
91+
deposition_id: &str,
92+
file_path: &Path,
93+
) -> Result<R, Error> {
94+
let file = File::open(file_path).await.context(FileReadingSnafu)?;
95+
let endpoint = self.make_url(&format!("/api/deposit/depositions/{deposition_id}/files"))?;
96+
let stream = FramedRead::new(file, BytesCodec::new());
97+
let file_name = file_path
98+
.file_name()
99+
.ok_or(Error::FileParsing {
100+
fp: file_path.to_path_buf(),
101+
})?
102+
.to_str()
103+
.ok_or(Error::FileParsing {
104+
fp: file_path.to_path_buf(),
105+
})?;
106+
let mime_type = from_path(file_path).first_or_octet_stream();
107+
let form = reqwest::multipart::Form::new()
108+
.text("name", file_name.to_owned())
109+
.part(
110+
"file",
111+
reqwest::multipart::Part::stream(reqwest::Body::wrap_stream(stream))
112+
.file_name(file_name.to_owned())
113+
.mime_str(mime_type.as_ref())
114+
.context(ReqwestSnafu)?,
115+
);
116+
let res = self
117+
.http_client
118+
.post(endpoint)
119+
.bearer_auth(&self.token)
120+
.multipart(form)
121+
.send()
122+
.await
123+
.context(ReqwestSnafu)?;
124+
res.json::<R>().await.context(DeserializeRespSnafu)
125+
}
126+
}

src/cli/cmd/login.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ enum Steps<'a> {
6868
Complete,
6969
}
7070
impl Input {
71-
fn get_steps(&self) -> Steps<'_> {
71+
fn get_steps(&'_ self) -> Steps<'_> {
7272
if let Some(p) = &self.continue_from {
7373
Steps::Continue(p)
7474
} else if self.user_code_only {

src/cli/cmd/version.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ pub struct Versions<'a> {
5858
pub renku_url: &'a str,
5959
}
6060
impl Versions<'_> {
61-
pub fn create(server: VersionInfo, renku_url: &str) -> Versions<'_> {
61+
pub fn create(server: VersionInfo, renku_url: &'_ str) -> Versions<'_> {
6262
Versions {
6363
client: BuildInfo::default(),
6464
server,

src/cli/opts.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ pub enum SubCommand {
6363

6464
#[cfg(feature = "user-doc")]
6565
UserDoc(userdoc::Input),
66+
67+
#[command()]
68+
Dataset(dataset::Input),
6669
}
6770

6871
/// This is the command line interface to the Renku platform. Main

0 commit comments

Comments
 (0)