diff --git a/README.md b/README.md index 7abe42f..9a03ccf 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ As part of building the import server, the following commands exercise each oper - [x] trello stack get - includes list of cards - [x] trello card get - includes list of attachments - [x] trello attachment get - includes download url +- [x] trello attachment save - saves to disk - [ ] nextcloud deck get (was board list) - [ ] nextcloud board get (was stack list) - [ ] nextcloud stack get (was card list) diff --git a/src/lib.rs b/src/lib.rs index b49aac8..8aec10c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -67,10 +67,10 @@ pub struct Ctx { pub net: Net, pub prt: Printer, } -impl Default for Ctx { - fn default() -> Self { +impl Ctx { + pub fn new(base: impl Into) -> Self { Self { - fs: kxio::fs::new(PathBuf::default()), + fs: kxio::fs::new(base), net: kxio::net::new(), prt: kxio::print::standard(), } @@ -79,7 +79,7 @@ impl Default for Ctx { #[derive(Clone)] pub(crate) struct FullCtx { - // pub fs: FileSystem, + pub fs: FileSystem, pub net: Net, pub prt: Printer, pub cfg: AppConfig, @@ -121,7 +121,7 @@ pub async fn run(ctx: Ctx) -> color_eyre::Result<()> { commands .command .execute(FullCtx { - // fs: ctx.fs, + fs: ctx.fs, net: ctx.net, prt: ctx.prt, cfg, diff --git a/src/main.rs b/src/main.rs index d253395..7b8210e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,5 +6,5 @@ use trello_to_deck::{run, Ctx}; #[tokio::main] #[cfg_attr(test, mutants::skip)] async fn main() -> Result<()> { - run(Ctx::default()).await + run(Ctx::new(std::env::current_dir()?)).await } diff --git a/src/tests/mod.rs b/src/tests/mod.rs index f8b9fbf..3e34bd0 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -132,6 +132,7 @@ pub(crate) mod given { pub(crate) fn a_context(fs: FileSystem, net: Net, prt: Printer) -> Ctx { Ctx { fs, net, prt } } + pub(crate) fn a_filesystem() -> TempFileSystem { kxio::fs::temp().expect("temp fs") } @@ -165,10 +166,10 @@ pub(crate) mod given { } } - pub(crate) fn a_full_context(fs: TempFileSystem, net: MockNet) -> FullCtx { + pub(crate) fn a_full_context(fs: TempFileSystem, mock_net: MockNet) -> FullCtx { FullCtx { - // fs: a_filesystem(), - net: net.into(), + fs: fs.as_real(), + net: mock_net.into(), prt: a_printer(), cfg: AppConfig { trello: a_trello_config(), diff --git a/src/tests/responses/trello-attachment-save.png b/src/tests/responses/trello-attachment-save.png new file mode 100644 index 0000000..2f10201 Binary files /dev/null and b/src/tests/responses/trello-attachment-save.png differ diff --git a/src/trello/attachment.rs b/src/trello/attachment.rs index cbd661b..4ec8644 100644 --- a/src/trello/attachment.rs +++ b/src/trello/attachment.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + // use clap::Parser; use color_eyre::Result; @@ -15,6 +17,11 @@ pub(crate) enum TrelloAttachmentCommand { card_id: String, attachment_id: String, }, + Save { + card_id: String, + attachment_id: String, + file_name: Option, // will use file name from attachment if not provided. + }, } impl Execute for TrelloAttachmentCommand { @@ -25,12 +32,11 @@ impl Execute for TrelloAttachmentCommand { card_id, attachment_id, } => { + let trello_card_id = TrelloCardId::new(card_id); + let trello_attachment_id = TrelloAttachmentId::new(attachment_id); let api_result = ctx .trello_client() - .card_attachment( - &TrelloCardId::new(card_id), - &TrelloAttachmentId::new(attachment_id), - ) + .card_attachment(&trello_card_id, &trello_attachment_id) .await; if dump { p!(ctx.prt, "{}", api_result.text); @@ -40,6 +46,20 @@ impl Execute for TrelloAttachmentCommand { } Ok(()) } + Self::Save { + card_id, + attachment_id, + file_name, + } => { + let trello_card_id = TrelloCardId::new(card_id); + let trello_attachment_id = TrelloAttachmentId::new(attachment_id); + let file_name = ctx + .trello_client() + .save_attachment(&trello_card_id, &trello_attachment_id, file_name) + .await?; + p!(ctx.prt, "Wrote: {}", file_name.display()); + Ok(()) + } } } } diff --git a/src/trello/client.rs b/src/trello/client.rs index d6f5573..f0d2739 100644 --- a/src/trello/client.rs +++ b/src/trello/client.rs @@ -1,6 +1,8 @@ // use std::collections::HashMap; +use std::path::PathBuf; +use color_eyre::eyre::Context; use kxio::net::{Net, ReqBuilder}; use crate::trello::model::{TrelloAttachment, TrelloAttachmentId}; @@ -26,6 +28,14 @@ impl<'ctx> TrelloClient<'ctx> { f!("https://api.trello.com/1{path}") } + fn auth_headers(&self) -> HashMap { + let api_key = &self.ctx.cfg.trello.api_key; + let api_secret = &self.ctx.cfg.trello.api_secret; + HashMap::from([( + s!("Authorization"), + f!(r#"OAuth oauth_consumer_key="{api_key}", oauth_token="{api_secret}""#,), + )]) + } fn common_headers(&self) -> HashMap { let api_key = &self.ctx.cfg.trello.api_key; let api_secret = &self.ctx.cfg.trello.api_secret; @@ -53,6 +63,34 @@ impl<'ctx> TrelloClient<'ctx> { ) .await } + + pub(crate) async fn save_attachment( + &self, + card_id: &TrelloCardId, + attachment_id: &TrelloAttachmentId, + file_name: Option, + ) -> color_eyre::Result { + let attachment = self.card_attachment(card_id, attachment_id).await.result?; + let url = attachment.url; + let file_name = file_name.unwrap_or_else(|| attachment.file_name.into()); + crate::e!(self.ctx.prt, "file_name: {}", file_name.display()); + crate::e!(self.ctx.prt, "base: {}", self.ctx.fs.base().display()); + let file_name = self.ctx.fs.base().join(file_name); + crate::e!(self.ctx.prt, "file_name: {}", file_name.display()); + let resp = self + .ctx + .net + .get(url) + .headers(self.auth_headers()) + .header("accept", "application/octet") + .send() + .await? + .bytes() + .await?; + let file = self.ctx.fs.file(&file_name); + file.write(resp).context("writing to disk")?; + Ok(file_name) + } } impl<'ctx> TrelloClient<'ctx> {