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