redmine_api/api/
uploads.rs1use derive_builder::Builder;
15use reqwest::Method;
16use std::borrow::Cow;
17use std::io::Read;
18use std::path::PathBuf;
19
20use crate::api::{Endpoint, NoPagination, QueryParams, ReturnsJsonResponse};
21
22#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
26pub struct FileUploadToken {
27 token: String,
29}
30
31#[derive(Debug, Clone, Builder)]
36#[builder(setter(strip_option))]
37pub struct UploadFile<'a> {
38 #[builder(setter(into))]
40 file: PathBuf,
41 #[builder(default, setter(into))]
44 filename: Option<Cow<'a, str>>,
45}
46
47impl ReturnsJsonResponse for UploadFile<'_> {}
48impl NoPagination for UploadFile<'_> {}
49
50impl<'a> UploadFile<'a> {
51 #[must_use]
53 pub fn builder() -> UploadFileBuilder<'a> {
54 UploadFileBuilder::default()
55 }
56}
57
58impl Endpoint for UploadFile<'_> {
59 fn method(&self) -> Method {
60 Method::POST
61 }
62
63 fn endpoint(&self) -> Cow<'static, str> {
64 "uploads.json".into()
65 }
66
67 fn parameters(&self) -> QueryParams {
68 let mut params = QueryParams::default();
69 if let Some(ref filename) = self.filename {
70 params.push("filename", filename);
71 } else {
72 let filename = self.file.file_name();
73 if let Some(filename) = filename {
74 params.push_opt("filename", filename.to_str());
75 }
76 }
77 params
78 }
79
80 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
81 let mut file_content: Vec<u8> = Vec::new();
82 let mut f = std::fs::File::open(&self.file)
83 .map_err(|e| crate::Error::UploadFileError(self.file.clone(), e))?;
84 f.read_to_end(&mut file_content)
85 .map_err(|e| crate::Error::UploadFileError(self.file.clone(), e))?;
86 Ok(Some(("application/octet-stream", file_content)))
87 }
88}
89
90#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
93pub struct UploadWrapper<T> {
94 pub upload: T,
96}
97
98#[cfg(test)]
99pub(crate) mod test {
100 use super::*;
101 use crate::api::issues::{
102 test::ISSUES_LOCK, CreateIssue, Issue, IssueWrapper, UpdateIssue, UploadedAttachment,
103 };
104 use crate::api::test_helpers::with_project;
105 use std::error::Error;
106 use tracing_test::traced_test;
107
108 #[function_name::named]
109 #[traced_test]
110 #[test]
111 fn test_create_issue_with_attachment() -> Result<(), Box<dyn Error>> {
112 let _w_issues = ISSUES_LOCK.write();
113 let name = format!("unittest_{}", function_name!());
114 with_project(&name, |redmine, project_id, _| {
115 let upload_endpoint = UploadFile::builder().file("README.md").build()?;
116 let UploadWrapper {
117 upload: FileUploadToken { token },
118 } = redmine
119 .json_response_body::<_, UploadWrapper<FileUploadToken>>(&upload_endpoint)?;
120 let create_endpoint = CreateIssue::builder()
121 .project_id(project_id)
122 .subject("Attachment Test Issue")
123 .uploads(vec![UploadedAttachment {
124 token: token.into(),
125 filename: "README.md".into(),
126 description: Some("Uploaded as part of unit test for redmine-api".into()),
127 content_type: "text/markdown".into(),
128 }])
129 .build()?;
130 redmine.json_response_body::<_, IssueWrapper<Issue>>(&create_endpoint)?;
131 Ok(())
132 })?;
133 Ok(())
134 }
135
136 #[function_name::named]
137 #[traced_test]
138 #[test]
139 fn test_update_issue_with_attachment() -> Result<(), Box<dyn Error>> {
140 let _w_issues = ISSUES_LOCK.write();
141 let name = format!("unittest_{}", function_name!());
142 with_project(&name, |redmine, project_id, _| {
143 let upload_endpoint = UploadFile::builder().file("README.md").build()?;
144 let UploadWrapper {
145 upload: FileUploadToken { token },
146 } = redmine
147 .json_response_body::<_, UploadWrapper<FileUploadToken>>(&upload_endpoint)?;
148 let create_endpoint = CreateIssue::builder()
149 .project_id(project_id)
150 .subject("Attachment Test Issue")
151 .build()?;
152 let IssueWrapper { issue }: IssueWrapper<Issue> =
153 redmine.json_response_body::<_, _>(&create_endpoint)?;
154 let update_endpoint = UpdateIssue::builder()
155 .id(issue.id)
156 .subject("New test subject")
157 .uploads(vec![UploadedAttachment {
158 token: token.into(),
159 filename: "README.md".into(),
160 description: Some("Uploaded as part of unit test for redmine-api".into()),
161 content_type: "text/markdown".into(),
162 }])
163 .build()?;
164 redmine.ignore_response_body::<_>(&update_endpoint)?;
165 Ok(())
166 })?;
167 Ok(())
168 }
169}