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