Skip to main content

wekan_cli/commands/
attachments.rs

1mod delete;
2mod download;
3mod export;
4mod get;
5mod list;
6mod recovery;
7#[cfg(test)]
8mod tests;
9mod transfer;
10mod upload;
11
12use crate::{
13    client::WekanClientFactory, command_result::CommandSuccess, credentials::CredentialStore,
14    error::AppError, input::ConfirmationProvider,
15};
16use clap::{Args, Subcommand};
17
18#[derive(Debug, Args)]
19pub struct AttachmentArgs {
20    #[command(subcommand)]
21    pub command: AttachmentCommand,
22}
23
24#[derive(Debug, Subcommand)]
25pub enum AttachmentCommand {
26    /// Export a native artifact to a file or validated raw stdout.
27    Export(export::ExportArgs),
28    /// List attachments. Wekan v12.57 ignores deeper selectors and returns all board attachments.
29    List(list::ListArgs),
30    /// Read attachment metadata and versions.
31    Get(get::GetArgs),
32    /// Upload a file or stdin (-) using JSON or native raw bytes.
33    Upload(upload::UploadArgs),
34    /// Download an attachment to a file or raw stdout (-).
35    Download(download::DownloadArgs),
36    /// Copy an attachment to a card. Pinned Wekan JSON copies return HTTP 408.
37    Copy(transfer::TransferArgs),
38    /// Move an attachment to a card. Pinned Wekan JSON moves return HTTP 408.
39    Move(transfer::TransferArgs),
40    /// Soft-delete an attachment; requires confirmation or --yes.
41    Delete(delete::DeleteArgs),
42    /// List recoverable deleted attachments on a board.
43    ListDeleted(recovery::DeletedArgs),
44    /// Restore a deleted attachment; its previous cover selection stays unset.
45    Restore(recovery::RestoreArgs),
46    /// Upload and select a board background using JSON or native raw bytes.
47    UploadBackground(upload::BackgroundArgs),
48    /// Download the current board background to a file or raw stdout (-).
49    DownloadBackground(download::BackgroundArgs),
50}
51
52impl AttachmentCommand {
53    pub(crate) fn validate_input(&self) -> Result<(), AppError> {
54        match self {
55            Self::Upload(a) => {
56                a.source.name()?;
57            }
58            Self::UploadBackground(a) => {
59                a.source.name()?;
60            }
61            _ => {}
62        }
63        Ok(())
64    }
65    pub(crate) fn download_destination(&self) -> Option<&download::DestinationArgs> {
66        match self {
67            Self::Download(a) => Some(&a.destination),
68            Self::DownloadBackground(a) => Some(&a.destination),
69            _ => None,
70        }
71    }
72}
73
74pub(crate) async fn dispatch(
75    command: AttachmentCommand,
76    factory: &WekanClientFactory,
77    store: &dyn CredentialStore,
78    confirmation: &dyn ConfirmationProvider,
79) -> Result<CommandSuccess, AppError> {
80    match command {
81        AttachmentCommand::ListDeleted(a) => recovery::list(a, factory, store).await,
82        AttachmentCommand::Restore(a) => recovery::restore(a, factory, store).await,
83        AttachmentCommand::Export(args) => export::execute(args, factory, store).await,
84        AttachmentCommand::List(a) => list::execute(a, factory, store).await,
85        AttachmentCommand::Get(a) => get::execute(a, factory, store).await,
86        AttachmentCommand::Upload(a) => upload::execute(a, factory, store).await,
87        AttachmentCommand::UploadBackground(a) => upload::background(a, factory, store).await,
88        AttachmentCommand::Download(a) => {
89            download::execute(Some(a.attachment_id), None, a.destination, factory, store).await
90        }
91        AttachmentCommand::DownloadBackground(a) => {
92            download::execute(None, Some(a.board_id), a.destination, factory, store).await
93        }
94        AttachmentCommand::Copy(a) => transfer::execute(a, false, factory, store).await,
95        AttachmentCommand::Move(a) => transfer::execute(a, true, factory, store).await,
96        AttachmentCommand::Delete(a) => delete::execute(a, factory, store, confirmation).await,
97    }
98}
99
100fn identifier(value: &str) -> Result<String, String> {
101    let value = crate::client::trim_javascript_whitespace(value);
102    if value.is_empty() || matches!(value, "." | "..") {
103        Err("ID must be nonblank and cannot be . or ..".to_owned())
104    } else {
105        Ok(value.to_owned())
106    }
107}
108
109fn file_error(operation: &str, path: &std::path::Path, error: std::io::Error) -> AppError {
110    AppError::new(
111        crate::error::ErrorCode::FileIoError,
112        format!(
113            "could not {operation} attachment file `{}`: {}",
114            crate::input::escape_terminal_text(&path.display().to_string()),
115            crate::input::escape_terminal_text(&error.to_string())
116        ),
117        crate::exit_code::StableExitCode::Transport,
118    )
119}