1use crate::error::S3Error;
7use crate::request::{Request as _, ResponseData};
8use crate::{Bucket, command::Command};
9use http::{HeaderMap, HeaderName, HeaderValue};
10
11#[cfg(feature = "with-tokio")]
12use tokio::io::AsyncRead;
13
14#[cfg(feature = "with-async-std")]
15use async_std::io::Read as AsyncRead;
16
17#[cfg(feature = "with-async-std")]
18use crate::request::async_std_backend::SurfRequest as RequestImpl;
19#[cfg(feature = "sync")]
20use crate::request::blocking::AttoRequest as RequestImpl;
21#[cfg(feature = "with-tokio")]
22use crate::request::tokio_backend::ReqwestRequest as RequestImpl;
23
24#[derive(Debug, Clone)]
47pub struct PutObjectRequest<'a> {
48 bucket: &'a Bucket,
49 path: String,
50 content: Vec<u8>,
51 content_type: String,
52 custom_headers: HeaderMap,
53}
54
55impl<'a> PutObjectRequest<'a> {
56 pub(crate) fn new<S: AsRef<str>>(bucket: &'a Bucket, path: S, content: &[u8]) -> Self {
58 Self {
59 bucket,
60 path: path.as_ref().to_string(),
61 content: content.to_vec(),
62 content_type: "application/octet-stream".to_string(),
63 custom_headers: HeaderMap::new(),
64 }
65 }
66
67 pub fn with_content_type<S: AsRef<str>>(mut self, content_type: S) -> Self {
69 self.content_type = content_type.as_ref().to_string();
70 self
71 }
72
73 pub fn with_cache_control<S: AsRef<str>>(mut self, cache_control: S) -> Result<Self, S3Error> {
75 let value = cache_control
76 .as_ref()
77 .parse::<HeaderValue>()
78 .map_err(S3Error::InvalidHeaderValue)?;
79 self.custom_headers
80 .insert(http::header::CACHE_CONTROL, value);
81 Ok(self)
82 }
83
84 pub fn with_content_encoding<S: AsRef<str>>(mut self, encoding: S) -> Result<Self, S3Error> {
86 let value = encoding
87 .as_ref()
88 .parse::<HeaderValue>()
89 .map_err(S3Error::InvalidHeaderValue)?;
90 self.custom_headers
91 .insert(http::header::CONTENT_ENCODING, value);
92 Ok(self)
93 }
94
95 pub fn with_content_disposition<S: AsRef<str>>(
97 mut self,
98 disposition: S,
99 ) -> Result<Self, S3Error> {
100 let value = disposition
101 .as_ref()
102 .parse::<HeaderValue>()
103 .map_err(S3Error::InvalidHeaderValue)?;
104 self.custom_headers
105 .insert(http::header::CONTENT_DISPOSITION, value);
106 Ok(self)
107 }
108
109 pub fn with_expires<S: AsRef<str>>(mut self, expires: S) -> Result<Self, S3Error> {
111 let value = expires
112 .as_ref()
113 .parse::<HeaderValue>()
114 .map_err(S3Error::InvalidHeaderValue)?;
115 self.custom_headers.insert(http::header::EXPIRES, value);
116 Ok(self)
117 }
118
119 pub fn with_header<V>(mut self, key: &str, value: V) -> Result<Self, S3Error>
121 where
122 V: AsRef<str>,
123 {
124 let header_name = HeaderName::from_bytes(key.as_bytes())?;
125 let header_value = value
126 .as_ref()
127 .parse::<HeaderValue>()
128 .map_err(S3Error::InvalidHeaderValue)?;
129 self.custom_headers.insert(header_name, header_value);
130 Ok(self)
131 }
132
133 pub fn with_headers(mut self, headers: HeaderMap) -> Self {
135 self.custom_headers.extend(headers);
136 self
137 }
138
139 pub fn with_metadata<K: AsRef<str>, V: AsRef<str>>(
141 mut self,
142 key: K,
143 value: V,
144 ) -> Result<Self, S3Error> {
145 let header_name = format!("x-amz-meta-{}", key.as_ref());
146 let name = header_name.parse::<http::HeaderName>()?;
147 let value = value
148 .as_ref()
149 .parse::<HeaderValue>()
150 .map_err(S3Error::InvalidHeaderValue)?;
151 self.custom_headers.insert(name, value);
152 Ok(self)
153 }
154
155 pub fn with_storage_class<S: AsRef<str>>(mut self, storage_class: S) -> Result<Self, S3Error> {
157 let header_value = storage_class
158 .as_ref()
159 .parse::<HeaderValue>()
160 .map_err(S3Error::InvalidHeaderValue)?;
161 self.custom_headers.insert(
162 http::HeaderName::from_static("x-amz-storage-class"),
163 header_value,
164 );
165 Ok(self)
166 }
167
168 pub fn with_server_side_encryption<S: AsRef<str>>(
170 mut self,
171 encryption: S,
172 ) -> Result<Self, S3Error> {
173 let header_value = encryption
174 .as_ref()
175 .parse::<HeaderValue>()
176 .map_err(S3Error::InvalidHeaderValue)?;
177 self.custom_headers.insert(
178 http::HeaderName::from_static("x-amz-server-side-encryption"),
179 header_value,
180 );
181 Ok(self)
182 }
183
184 #[maybe_async::maybe_async]
186 pub async fn execute(self) -> Result<ResponseData, S3Error> {
187 let command = Command::PutObject {
188 content: &self.content,
189 content_type: &self.content_type,
190 custom_headers: if self.custom_headers.is_empty() {
191 None
192 } else {
193 Some(self.custom_headers)
194 },
195 multipart: None,
196 };
197
198 let request = RequestImpl::new(self.bucket, &self.path, command).await?;
199 request.response_data(true).await
200 }
201}
202
203#[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
205#[derive(Debug, Clone)]
206pub struct PutObjectStreamRequest<'a> {
207 bucket: &'a Bucket,
208 path: String,
209 content_type: String,
210 custom_headers: HeaderMap,
211}
212
213#[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
214impl<'a> PutObjectStreamRequest<'a> {
215 pub(crate) fn new<S: AsRef<str>>(bucket: &'a Bucket, path: S) -> Self {
217 Self {
218 bucket,
219 path: path.as_ref().to_string(),
220 content_type: "application/octet-stream".to_string(),
221 custom_headers: HeaderMap::new(),
222 }
223 }
224
225 pub fn with_content_type<S: AsRef<str>>(mut self, content_type: S) -> Self {
227 self.content_type = content_type.as_ref().to_string();
228 self
229 }
230
231 pub fn with_cache_control<S: AsRef<str>>(mut self, cache_control: S) -> Result<Self, S3Error> {
233 let value = cache_control
234 .as_ref()
235 .parse::<HeaderValue>()
236 .map_err(S3Error::InvalidHeaderValue)?;
237 self.custom_headers
238 .insert(http::header::CACHE_CONTROL, value);
239 Ok(self)
240 }
241
242 pub fn with_content_encoding<S: AsRef<str>>(mut self, encoding: S) -> Result<Self, S3Error> {
244 let value = encoding
245 .as_ref()
246 .parse::<HeaderValue>()
247 .map_err(S3Error::InvalidHeaderValue)?;
248 self.custom_headers
249 .insert(http::header::CONTENT_ENCODING, value);
250 Ok(self)
251 }
252
253 pub fn with_header<K, V>(mut self, key: K, value: V) -> Result<Self, S3Error>
255 where
256 K: Into<http::HeaderName>,
257 V: AsRef<str>,
258 {
259 let header_value = value
260 .as_ref()
261 .parse::<HeaderValue>()
262 .map_err(S3Error::InvalidHeaderValue)?;
263 self.custom_headers.insert(key.into(), header_value);
264 Ok(self)
265 }
266
267 pub fn with_headers(mut self, headers: HeaderMap) -> Self {
269 self.custom_headers.extend(headers);
270 self
271 }
272
273 pub fn with_metadata<K: AsRef<str>, V: AsRef<str>>(
275 mut self,
276 key: K,
277 value: V,
278 ) -> Result<Self, S3Error> {
279 let header_name = format!("x-amz-meta-{}", key.as_ref());
280 let name = header_name.parse::<http::HeaderName>()?;
281 let value = value
282 .as_ref()
283 .parse::<HeaderValue>()
284 .map_err(S3Error::InvalidHeaderValue)?;
285 self.custom_headers.insert(name, value);
286 Ok(self)
287 }
288
289 #[cfg(feature = "with-tokio")]
291 pub async fn execute_stream<R: AsyncRead + Unpin + ?Sized>(
292 self,
293 reader: &mut R,
294 ) -> Result<crate::utils::PutStreamResponse, S3Error> {
295 self.bucket
298 ._put_object_stream_with_content_type_and_headers(
299 reader,
300 &self.path,
301 &self.content_type,
302 if self.custom_headers.is_empty() {
303 None
304 } else {
305 Some(self.custom_headers)
306 },
307 )
308 .await
309 }
310
311 #[cfg(feature = "with-async-std")]
312 pub async fn execute_stream<R: AsyncRead + Unpin + ?Sized>(
313 self,
314 reader: &mut R,
315 ) -> Result<crate::utils::PutStreamResponse, S3Error> {
316 self.bucket
317 ._put_object_stream_with_content_type_and_headers(
318 reader,
319 &self.path,
320 &self.content_type,
321 if self.custom_headers.is_empty() {
322 None
323 } else {
324 Some(self.custom_headers)
325 },
326 )
327 .await
328 }
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334 use crate::Region;
335 use crate::creds::Credentials;
336
337 #[test]
338 fn test_builder_chain() {
339 let bucket =
340 Bucket::new("test", Region::UsEast1, Credentials::anonymous().unwrap()).unwrap();
341
342 let content = b"test content";
343 let request = PutObjectRequest::new(&bucket, "/test.txt", content)
344 .with_content_type("text/plain")
345 .with_cache_control("max-age=3600")
346 .unwrap()
347 .with_content_encoding("gzip")
348 .unwrap()
349 .with_metadata("author", "test-user")
350 .unwrap()
351 .with_header("x-custom", "value")
352 .unwrap()
353 .with_storage_class("STANDARD_IA")
354 .unwrap();
355
356 assert_eq!(request.content_type, "text/plain");
357 assert!(
358 request
359 .custom_headers
360 .contains_key(http::header::CACHE_CONTROL)
361 );
362 assert!(
363 request
364 .custom_headers
365 .contains_key(http::header::CONTENT_ENCODING)
366 );
367 assert!(request.custom_headers.contains_key("x-amz-meta-author"));
368 assert!(request.custom_headers.contains_key("x-custom"));
369 assert!(request.custom_headers.contains_key("x-amz-storage-class"));
370 }
371
372 #[test]
373 fn test_metadata_headers() {
374 let bucket =
375 Bucket::new("test", Region::UsEast1, Credentials::anonymous().unwrap()).unwrap();
376
377 let request = PutObjectRequest::new(&bucket, "/test.txt", b"test")
378 .with_metadata("key1", "value1")
379 .unwrap()
380 .with_metadata("key2", "value2")
381 .unwrap();
382
383 assert!(request.custom_headers.contains_key("x-amz-meta-key1"));
384 assert!(request.custom_headers.contains_key("x-amz-meta-key2"));
385 }
386}