Skip to main content

wekan_cli/client/boards/
trello_import.rs

1//! The application HTTP importer is distinct from the /api import engines.
2use std::time::Duration;
3
4use reqwest::{
5    RequestBuilder,
6    header::{ACCEPT, CONTENT_LENGTH, CONTENT_TYPE, COOKIE},
7};
8use secrecy::{ExposeSecret, SecretString};
9use serde::{Deserialize, Serialize};
10use tokio::io::AsyncRead;
11use tokio_util::io::ReaderStream;
12
13use super::import::BoardImportRequest;
14use crate::client::{
15    ClientError, WekanClient, imports::PreparedImportRequest, transport::decode_success,
16};
17
18#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
19#[serde(rename_all = "lowercase")]
20pub enum TrelloImportFormat {
21    Json,
22    Zip,
23}
24
25#[derive(Clone, Copy, Debug)]
26pub enum TrelloImportAuth {
27    Bearer,
28    XAuthToken,
29    MeteorCookie,
30    WekanCookie,
31}
32
33/// Wekan echoes an unvalidated name on failure. Compound names are deliberately
34/// rejected rather than admitting unmapped nested response objects.
35#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
36#[serde(untagged)]
37pub enum TrelloImportBoardName {
38    String(String),
39    Number(serde_json::Number),
40    Boolean(bool),
41}
42
43#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
44#[serde(deny_unknown_fields)]
45pub struct TrelloImportBoardError {
46    pub board: TrelloImportBoardName,
47    pub error: String,
48}
49
50#[derive(Debug, Deserialize)]
51#[serde(deny_unknown_fields)]
52struct JsonResponse {
53    #[serde(rename = "boardIds")]
54    board_ids: Vec<String>,
55}
56
57#[derive(Debug, Deserialize)]
58#[serde(deny_unknown_fields)]
59struct ZipResponse {
60    #[serde(rename = "boardIds")]
61    board_ids: Vec<String>,
62    errors: Vec<TrelloImportBoardError>,
63}
64
65#[derive(Deserialize)]
66#[serde(deny_unknown_fields)]
67struct ErrorResponse {
68    #[serde(rename = "error")]
69    _error: String,
70}
71
72#[derive(Debug)]
73pub struct TrelloImportResponse {
74    pub board_ids: Vec<String>,
75    pub errors: Option<Vec<TrelloImportBoardError>>,
76}
77
78impl WekanClient {
79    pub async fn import_trello_json(
80        &self,
81        body: PreparedImportRequest<BoardImportRequest>,
82        token: &SecretString,
83        auth: TrelloImportAuth,
84        timeout: Option<Duration>,
85    ) -> Result<TrelloImportResponse, ClientError> {
86        let request = self
87            .http
88            .post(self.boards_api_endpoint("import-trello", "Trello import")?)
89            .header(CONTENT_TYPE, "application/json")
90            .body(body.into_body());
91        self.send_trello_import(request, TrelloImportFormat::Json, token, auth, timeout)
92            .await
93    }
94
95    pub async fn import_trello_zip(
96        &self,
97        reader: impl AsyncRead + Send + Unpin + 'static,
98        length: u64,
99        token: &SecretString,
100        auth: TrelloImportAuth,
101        timeout: Option<Duration>,
102    ) -> Result<TrelloImportResponse, ClientError> {
103        let request = self
104            .http
105            .post(self.boards_api_endpoint("import-trello", "Trello import")?)
106            .header(CONTENT_TYPE, "application/zip")
107            .header(CONTENT_LENGTH, length)
108            .body(reqwest::Body::wrap_stream(ReaderStream::new(reader)));
109        self.send_trello_import(request, TrelloImportFormat::Zip, token, auth, timeout)
110            .await
111    }
112
113    async fn send_trello_import(
114        &self,
115        request: RequestBuilder,
116        format: TrelloImportFormat,
117        token: &SecretString,
118        auth: TrelloImportAuth,
119        timeout: Option<Duration>,
120    ) -> Result<TrelloImportResponse, ClientError> {
121        let mut request = request.header(ACCEPT, "application/json");
122        request = match auth {
123            TrelloImportAuth::Bearer => request.bearer_auth(token.expose_secret()),
124            TrelloImportAuth::XAuthToken => request.header("X-Auth-Token", token.expose_secret()),
125            TrelloImportAuth::MeteorCookie | TrelloImportAuth::WekanCookie => {
126                let name = if matches!(auth, TrelloImportAuth::MeteorCookie) {
127                    "meteor_login_token"
128                } else {
129                    "wekan_login_token"
130                };
131                let encoded =
132                    url::form_urlencoded::byte_serialize(token.expose_secret().as_bytes())
133                        .collect::<String>()
134                        .replace('+', "%20");
135                request.header(COOKIE, format!("{name}={encoded}"))
136            }
137        };
138        if let Some(timeout) = timeout {
139            request = request.timeout(timeout);
140        }
141        let body = self
142            .execute_success_request_with_error::<ClientError>(request, |_, body| {
143                // Middleware may return HTML. JSON errors belong to this route's
144                // stricter {error: string} contract; retain shared HTTP metadata.
145                if let Ok(value) = serde_json::from_slice(body) {
146                    let _: ErrorResponse =
147                        crate::client::decoding::decode_value(value, "Trello import error", false)?;
148                }
149                Ok(None)
150            })
151            .await?;
152        match format {
153            TrelloImportFormat::Json => {
154                let response: JsonResponse = decode_success(&body, "Trello JSON import", true)?;
155                Ok(TrelloImportResponse {
156                    board_ids: response.board_ids,
157                    errors: None,
158                })
159            }
160            TrelloImportFormat::Zip => {
161                let response: ZipResponse = decode_success(&body, "Trello ZIP import", true)?;
162                Ok(TrelloImportResponse {
163                    board_ids: response.board_ids,
164                    errors: Some(response.errors),
165                })
166            }
167        }
168    }
169}