1use std::future::Future;
2use std::str::FromStr;
3
4use http::{HeaderMap, Method, header};
5use jiff::Timestamp;
6use serde::{Deserialize, Deserializer, Serialize};
7
8use crate::body::NoneBody;
9use crate::error::Result;
10use crate::ops::common::{ObjectType, ServerSideEncryption, StorageClass};
11use crate::response::HeaderResponseProcessor;
12use crate::{Client, Ops, Prepared, Request};
13
14#[derive(Debug, Clone, Deserialize)]
16pub struct RestoreInfo {
17 pub ongoing_request: bool,
19 pub expiry_date: Option<String>,
21}
22
23impl FromStr for RestoreInfo {
24 type Err = crate::error::Error;
25
26 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
27 let ongoing_request_regex = regex::Regex::new(r#"ongoing-request="([^"]+)""#).unwrap();
28 let expiry_date_regex = regex::Regex::new(r#"expiry-date="([^"]+)""#).unwrap();
29
30 let mut ongoing_request = false;
32 let mut expiry_date = None;
33
34 for part in s.split(',') {
35 let part = part.trim();
36 if let Some(captures) = ongoing_request_regex.captures(part) {
37 ongoing_request = captures.get(1).unwrap().as_str() == "true";
38 } else if let Some(captures) = expiry_date_regex.captures(part) {
39 expiry_date = Some(captures.get(1).unwrap().as_str().to_string());
40 }
41 }
42
43 Ok(RestoreInfo {
44 ongoing_request,
45 expiry_date,
46 })
47 }
48}
49
50fn deserialize_content_length<'de, D>(deserializer: D) -> std::result::Result<u64, D::Error>
51where
52 D: Deserializer<'de>,
53{
54 let s = String::deserialize(deserializer)?;
55 s.parse().map_err(serde::de::Error::custom)
56}
57
58fn deserialize_datetime<'de, D>(deserializer: D) -> std::result::Result<Timestamp, D::Error>
59where
60 D: Deserializer<'de>,
61{
62 use jiff::fmt::rfc2822;
63
64 const RFC2822_PARSER: rfc2822::DateTimeParser = rfc2822::DateTimeParser::new();
65
66 let s = String::deserialize(deserializer)?;
67 let timestamp = RFC2822_PARSER
68 .parse_timestamp(s)
69 .map_err(serde::de::Error::custom)?;
70 Ok(timestamp)
71}
72
73#[derive(Debug, Clone, Deserialize)]
75#[serde(rename_all = "kebab-case")]
76pub struct HeadObjectResponse {
77 #[serde(deserialize_with = "deserialize_content_length")]
79 pub content_length: u64,
80 pub content_type: String,
82 #[serde(deserialize_with = "deserialize_datetime")]
84 pub date: Timestamp,
85 #[serde(deserialize_with = "deserialize_datetime")]
87 pub last_modified: Timestamp,
88 #[serde(rename = "etag")]
90 pub etag: Option<String>,
91 #[serde(rename = "x-oss-versionId")]
93 pub version_id: Option<String>,
94 #[serde(rename = "x-oss-object-type")]
96 pub object_type: Option<ObjectType>,
97 #[serde(rename = "x-oss-storage-class")]
99 pub storage_class: Option<StorageClass>,
100 #[serde(rename = "x-oss-server-side-encryption")]
102 pub server_side_encryption: Option<ServerSideEncryption>,
103 #[serde(rename = "x-oss-server-side-encryption-key-id")]
105 pub server_side_encryption_key_id: Option<String>,
106 #[serde(rename = "x-oss-next-append-position")]
108 pub next_append_position: Option<u64>,
109 #[serde(rename = "x-oss-hash-crc64ecma")]
111 pub hash_crc64ecma: Option<String>,
112 #[serde(rename = "x-oss-tagging-count")]
114 pub tagging_count: Option<u32>,
115 #[serde(rename = "x-oss-expiration")]
117 pub expiration: Option<String>,
118 #[serde(rename = "x-oss-restore")]
120 pub restore: Option<RestoreInfo>,
121 #[serde(rename = "x-oss-meta-source")]
123 pub source: Option<String>,
124}
125
126#[derive(Debug, Clone, Default, Serialize)]
128pub struct HeadObjectParams {
129 pub version_id: Option<String>,
131}
132
133#[derive(Debug, Clone, Default)]
134pub struct HeadObjectOptions {
135 pub if_modified_since: Option<String>,
136 pub if_unmodified_since: Option<String>,
137 pub if_match: Option<String>,
138 pub if_none_match: Option<String>,
139}
140
141impl HeadObjectOptions {
142 fn into_headers(self) -> Result<HeaderMap> {
143 let mut headers = HeaderMap::new();
144
145 if let Some(if_modified_since) = &self.if_modified_since {
146 headers.insert(header::IF_MODIFIED_SINCE, if_modified_since.parse()?);
147 }
148
149 if let Some(if_unmodified_since) = &self.if_unmodified_since {
150 headers.insert(header::IF_UNMODIFIED_SINCE, if_unmodified_since.parse()?);
151 }
152
153 if let Some(if_match) = &self.if_match {
154 headers.insert(header::IF_MATCH, if_match.parse()?);
155 }
156
157 if let Some(if_none_match) = &self.if_none_match {
158 headers.insert(header::IF_NONE_MATCH, if_none_match.parse()?);
159 }
160
161 Ok(headers)
162 }
163}
164
165#[derive(Debug, Clone)]
167pub struct HeadObjectRequestBuilder {
168 pub if_modified_since: Option<String>,
170 pub if_unmodified_since: Option<String>,
172 pub if_match: Option<String>,
174 pub if_none_match: Option<String>,
176}
177
178impl HeadObjectRequestBuilder {
179 pub fn new() -> Self {
180 Self {
181 if_modified_since: None,
182 if_unmodified_since: None,
183 if_match: None,
184 if_none_match: None,
185 }
186 }
187
188 pub fn if_modified_since(mut self, time: impl Into<String>) -> Self {
190 self.if_modified_since = Some(time.into());
191 self
192 }
193
194 pub fn if_unmodified_since(mut self, time: impl Into<String>) -> Self {
196 self.if_unmodified_since = Some(time.into());
197 self
198 }
199
200 pub fn if_match(mut self, etag: impl Into<String>) -> Self {
202 self.if_match = Some(etag.into());
203 self
204 }
205
206 pub fn if_none_match(mut self, etag: impl Into<String>) -> Self {
208 self.if_none_match = Some(etag.into());
209 self
210 }
211
212 pub fn build(self) -> HeadObjectOptions {
213 HeadObjectOptions {
214 if_modified_since: self.if_modified_since,
215 if_unmodified_since: self.if_unmodified_since,
216 if_match: self.if_match,
217 if_none_match: self.if_none_match,
218 }
219 }
220}
221
222impl Default for HeadObjectRequestBuilder {
223 fn default() -> Self {
224 Self::new()
225 }
226}
227
228pub struct HeadObject {
230 pub object_name: String,
231 pub params: HeadObjectParams,
232 pub options: HeadObjectOptions,
233}
234
235impl Ops for HeadObject {
236 type Response = HeaderResponseProcessor<HeadObjectResponse>;
237 type Body = NoneBody;
238 type Query = HeadObjectParams;
239
240 fn prepare(self) -> Result<Prepared<HeadObjectParams>> {
241 Ok(Prepared {
242 method: Method::HEAD,
243 key: Some(self.object_name),
244 query: Some(self.params),
245 headers: Some(self.options.into_headers()?),
246 ..Default::default()
247 })
248 }
249}
250
251pub trait HeadObjectOperations {
253 fn head_object(
257 &self,
258 object_name: impl Into<String>,
259 params: HeadObjectParams,
260 options: Option<HeadObjectOptions>,
261 ) -> impl Future<Output = Result<HeadObjectResponse>>;
262}
263
264impl HeadObjectOperations for Client {
265 async fn head_object(
266 &self,
267 object_name: impl Into<String>,
268 params: HeadObjectParams,
269 options: Option<HeadObjectOptions>,
270 ) -> Result<HeadObjectResponse> {
271 let ops = HeadObject {
272 object_name: object_name.into(),
273 params,
274 options: options.unwrap_or_default(),
275 };
276
277 self.request(ops).await
278 }
279}