Skip to main content

ossify/ops/object/base/
head_object.rs

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/// Restore status information
15#[derive(Debug, Clone, Deserialize)]
16pub struct RestoreInfo {
17    /// Whether a restore request is ongoing
18    pub ongoing_request: bool,
19    /// restoreExpiration time
20    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        // Parse restore header information, format: ongoing-request="true", expiry-date="Sun, 16 Apr 2017 08:12:33 GMT"
31        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/// HeadObject response
74#[derive(Debug, Clone, Deserialize)]
75#[serde(rename_all = "kebab-case")]
76pub struct HeadObjectResponse {
77    /// File size
78    #[serde(deserialize_with = "deserialize_content_length")]
79    pub content_length: u64,
80    /// Content type
81    pub content_type: String,
82    /// Creation time
83    #[serde(deserialize_with = "deserialize_datetime")]
84    pub date: Timestamp,
85    /// Last modified time
86    #[serde(deserialize_with = "deserialize_datetime")]
87    pub last_modified: Timestamp,
88    /// ETag
89    #[serde(rename = "etag")]
90    pub etag: Option<String>,
91    /// Version ID
92    #[serde(rename = "x-oss-versionId")]
93    pub version_id: Option<String>,
94    /// Object type
95    #[serde(rename = "x-oss-object-type")]
96    pub object_type: Option<ObjectType>,
97    /// Storage class
98    #[serde(rename = "x-oss-storage-class")]
99    pub storage_class: Option<StorageClass>,
100    /// Server-side encryption
101    #[serde(rename = "x-oss-server-side-encryption")]
102    pub server_side_encryption: Option<ServerSideEncryption>,
103    /// Server-side encryption key ID
104    #[serde(rename = "x-oss-server-side-encryption-key-id")]
105    pub server_side_encryption_key_id: Option<String>,
106    /// Next append position
107    #[serde(rename = "x-oss-next-append-position")]
108    pub next_append_position: Option<u64>,
109    /// CRC64 value
110    #[serde(rename = "x-oss-hash-crc64ecma")]
111    pub hash_crc64ecma: Option<String>,
112    /// Tag count
113    #[serde(rename = "x-oss-tagging-count")]
114    pub tagging_count: Option<u32>,
115    /// Expiration time
116    #[serde(rename = "x-oss-expiration")]
117    pub expiration: Option<String>,
118    /// Restore information
119    #[serde(rename = "x-oss-restore")]
120    pub restore: Option<RestoreInfo>,
121    /// Source file
122    #[serde(rename = "x-oss-meta-source")]
123    pub source: Option<String>,
124}
125
126/// HeadObjectRequest parameters
127#[derive(Debug, Clone, Default, Serialize)]
128pub struct HeadObjectParams {
129    /// Version ID
130    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/// HeadObjectRequest builder
166#[derive(Debug, Clone)]
167pub struct HeadObjectRequestBuilder {
168    /// Return 200 OK and Object Meta if the time in the parameter is earlier than the actual modification time; otherwise return 304 Not Modified
169    pub if_modified_since: Option<String>,
170    /// Return 200 OK and Object Meta if the time in the parameter is equal to or later than the actual modification time; otherwise return 412 Precondition Failed
171    pub if_unmodified_since: Option<String>,
172    /// Return 200 OK and Object Meta if the expected ETag matches the Object's ETag; otherwise return 412 precondition failed
173    pub if_match: Option<String>,
174    /// Return 200 OK and Object Meta if the expected ETag value does not match the Object's ETag; otherwise return 304 Not Modified
175    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    /// Set the If-Modified-Since header
189    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    /// Set the If-Unmodified-Since header
195    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    /// Set the If-Match header
201    pub fn if_match(mut self, etag: impl Into<String>) -> Self {
202        self.if_match = Some(etag.into());
203        self
204    }
205
206    /// Set the If-None-Match header
207    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
228/// HeadObject operation
229pub 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
251/// HeadObject operations trait
252pub trait HeadObjectOperations {
253    /// Get metadata for an object (file)
254    ///
255    /// Official documentation: <https://www.alibabacloud.com/help/en/oss/developer-reference/headobject>
256    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}