feat(trello): add command 'trello attachement save'

This commit is contained in:
Paul Campbell 2024-12-10 17:47:14 +00:00
parent ea2b3086fb
commit 8bc8852528
9 changed files with 178 additions and 43 deletions

View file

@ -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)

View file

@ -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<PathBuf>) -> 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,

View file

@ -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
}

View file

@ -118,8 +118,8 @@ mod commands {
.body(include_str!("../tests/responses/nextcloud-board-list.json"))
.expect("mock request");
// let fs = given::a_filesystem();
let ctx = given::a_full_context(mock_net);
let fs = given::a_filesystem();
let ctx = given::a_full_context(mock_net, fs);
let prt = ctx.prt.clone();
let prt = prt.as_test().unwrap();
@ -151,8 +151,8 @@ mod commands {
.body(include_str!("../tests/responses/nextcloud-board-list.json"))
.expect("mock request");
// let fs = given::a_filesystem();
let ctx = given::a_full_context(mock_net);
let fs = given::a_filesystem();
let ctx = given::a_full_context(mock_net, fs);
let prt = ctx.prt.clone();
let prt = prt.as_test().unwrap();
@ -195,8 +195,8 @@ mod commands {
))
.expect("mock request");
// let fs = given::a_filesystem();
let ctx = given::a_full_context(mock_net);
let fs = given::a_filesystem();
let ctx = given::a_full_context(mock_net, fs);
//when
let result = ctx
@ -220,8 +220,8 @@ mod commands {
.body(include_str!("../tests/responses/nextcloud-stack-list.json"))
.expect("mock request");
// let fs = given::a_filesystem();
let ctx = given::a_full_context(mock_net);
let fs = given::a_filesystem();
let ctx = given::a_full_context(mock_net, fs);
//when
let result = ctx
@ -332,8 +332,8 @@ mod commands {
.body(include_str!("../tests/responses/nextcloud-card-list.json"))
.expect("mock request");
// let fs = given::a_filesystem();
let ctx = given::a_full_context(mock_net);
let fs = given::a_filesystem();
let ctx = given::a_full_context(mock_net, fs);
//when
let result = ctx
@ -378,8 +378,8 @@ mod commands {
.body(include_str!("../tests/responses/nextcloud-card-get.json"))
.expect("mock request");
// let fs = given::a_filesystem();
let ctx = given::a_full_context(mock_net);
let fs = given::a_filesystem();
let ctx = given::a_full_context(mock_net, fs);
//when
let result = ctx
@ -432,8 +432,8 @@ mod commands {
))
.expect("mock request");
// let fs = given::a_filesystem();
let ctx = given::a_full_context(mock_net);
let fs = given::a_filesystem();
let ctx = given::a_full_context(mock_net, fs);
//when
let result = ctx
@ -484,8 +484,8 @@ mod commands {
.mock()
.expect("mock request");
// let fs = given::a_filesystem();
let ctx = given::a_full_context(mock_net);
let fs = given::a_filesystem();
let ctx = given::a_full_context(mock_net, fs);
//when
let result = ctx

View file

@ -131,12 +131,9 @@ pub(crate) mod given {
Ctx { fs, net, prt }
}
pub(crate) fn a_full_context(
mock_net: MockNet,
// fs: TempFileSystem
) -> FullCtx {
pub(crate) fn a_full_context(mock_net: MockNet, fs: TempFileSystem) -> FullCtx {
FullCtx {
// fs: fs.as_real(),
fs: fs.as_real(),
net: mock_net.into(),
prt: a_printer(),
cfg: AppConfig {

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

View file

@ -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<PathBuf>, // 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(())
}
}
}
}

View file

@ -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<String, String> {
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<String, String> {
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<std::path::PathBuf>,
) -> color_eyre::Result<PathBuf> {
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> {

View file

@ -75,8 +75,8 @@ mod commands {
.body(include_str!("../tests/responses/trello-board-get.json"))
.expect("mock request");
// let fs = given::a_filesystem();
let ctx = given::a_full_context(mock_net);
let fs = given::a_filesystem();
let ctx = given::a_full_context(mock_net, fs);
//when
let result = ctx
@ -123,8 +123,8 @@ mod commands {
prep_mock_get(&mock_net);
// let fs = given::a_filesystem();
let ctx = given::a_full_context(mock_net);
let fs = given::a_filesystem();
let ctx = given::a_full_context(mock_net, fs);
let prt = ctx.prt.clone();
let prt = prt.as_test().unwrap();
let command = Command::Trello(TrelloCommand::Card(TrelloCardCommand::Get {
@ -147,8 +147,8 @@ mod commands {
prep_mock_get(&mock_net);
// let fs = given::a_filesystem();
let ctx = given::a_full_context(mock_net);
let fs = given::a_filesystem();
let ctx = given::a_full_context(mock_net, fs);
let prt = ctx.prt.clone();
let prt = prt.as_test().unwrap();
let command = Command::Trello(TrelloCommand::Card(TrelloCardCommand::Get {
@ -195,8 +195,8 @@ mod commands {
prep_mock_get(&mock_net);
// let fs = given::a_filesystem();
let ctx = given::a_full_context(mock_net);
let fs = given::a_filesystem();
let ctx = given::a_full_context(mock_net, fs);
let prt = ctx.prt.clone();
let prt = prt.as_test().unwrap();
let command =
@ -207,7 +207,7 @@ mod commands {
}));
//when
command.execute(ctx).await;
command.execute(ctx).await.expect("execute");
//then
let output = prt.output();
@ -221,8 +221,8 @@ mod commands {
prep_mock_get(&mock_net);
// let fs = given::a_filesystem();
let ctx = given::a_full_context(mock_net);
let fs = given::a_filesystem();
let ctx = given::a_full_context(mock_net, fs);
let prt = ctx.prt.clone();
let prt = prt.as_test().unwrap();
let command =
@ -233,7 +233,7 @@ mod commands {
}));
//when
command.execute(ctx).await;
command.execute(ctx).await.expect("execute");
//then
let output = prt.output();
@ -255,6 +255,85 @@ mod commands {
.expect("mock request");
}
}
mod save {
use super::*;
use bytes::Bytes;
#[tokio::test]
async fn save_writes_file() {
//given
let mock_net = kxio::net::mock();
prep_mock_get(&mock_net);
let fs = given::a_filesystem();
let test_fs = fs.clone();
let ctx = given::a_full_context(mock_net, fs);
let command =
Command::Trello(TrelloCommand::Attachment(TrelloAttachmentCommand::Save {
card_id: s!("65ad94865aed24f70ecdcebb"),
attachment_id: s!("65ad94875aed24f70ecdd037"),
file_name: None,
}));
//when
command.execute(ctx).await.expect("execute");
//then
let file = test_fs.base().join("Backlog.png");
assert!(file.exists());
}
#[tokio::test]
async fn save_log_output() {
//given
let mock_net = kxio::net::mock();
prep_mock_get(&mock_net);
let fs = given::a_filesystem();
let test_fs = fs.clone();
let ctx = given::a_full_context(mock_net, fs);
let prt = ctx.prt.clone();
let prt = prt.as_test().unwrap();
let command =
Command::Trello(TrelloCommand::Attachment(TrelloAttachmentCommand::Save {
card_id: s!("65ad94865aed24f70ecdcebb"),
attachment_id: s!("65ad94875aed24f70ecdd037"),
file_name: None,
}));
//when
command.execute(ctx).await.expect("execute");
//then
let file = test_fs.base().join("Backlog.png");
drop(test_fs);
let output = prt.output();
assert_eq!(output.trim(), crate::f!("Wrote: {}", file.display()));
}
fn prep_mock_get(mock_net: &MockNet) {
mock_net
.on()
.get("https://api.trello.com/1/cards/65ad94865aed24f70ecdcebb/attachments/65ad94875aed24f70ecdd037")
.header("authorization", "OAuth oauth_consumer_key=\"trello-api-key\", oauth_token=\"trello-api-secret\"")
.header("accept", "application/json")
.header("content-type", "application/json")
.respond(StatusCode::OK)
.body(include_str!("../tests/responses/trello-attachment-get.json"))
.expect("mock request 1");
mock_net
.on()
.get("https://trello.com/1/cards/65ad94865aed24f70ecdcebb/attachments/65ad94875aed24f70ecdd037/download/Backlog.png")
.header("authorization", "OAuth oauth_consumer_key=\"trello-api-key\", oauth_token=\"trello-api-secret\"")
.respond(StatusCode::OK)
.body(Bytes::from(include_bytes!("../tests/responses/trello-attachment-save.png").as_slice()))
.expect("mock request 2");
}
}
}
}