Skip to main content

ali_oss_rs/
object_common.rs

1use std::{collections::HashMap, fmt::Display};
2
3use base64::prelude::{Engine, BASE64_STANDARD};
4use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event};
5
6use crate::{
7    common::{self, build_tag_string, MetadataDirective, ObjectType, ServerSideEncryptionAlgorithm, StorageClass, TagDirective, MIME_TYPE_XML},
8    error::Error,
9    request::{OssRequest, RequestMethod},
10    util::{sanitize_etag, validate_bucket_name, validate_meta_key, validate_object_key, validate_tag_key, validate_tag_value},
11    RequestBody, Result,
12};
13
14///
15/// Represents the access control list (ACL) for an object in Aliyun OSS.
16///
17#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
18#[cfg_attr(feature = "serde-support", derive(serde::Serialize, serde::Deserialize))]
19pub enum ObjectAcl {
20    #[default]
21    Default,
22
23    #[cfg_attr(feature = "serde-support", serde(rename = "public-read-write"))]
24    PublicReadWrite,
25
26    #[cfg_attr(feature = "serde-support", serde(rename = "public-read"))]
27    PublicRead,
28
29    #[cfg_attr(feature = "serde-support", serde(rename = "private"))]
30    Private,
31}
32
33impl ObjectAcl {
34    pub fn as_str(&self) -> &str {
35        match self {
36            ObjectAcl::PublicReadWrite => "public-read-write",
37            ObjectAcl::PublicRead => "public-read",
38            ObjectAcl::Private => "private",
39            ObjectAcl::Default => "default",
40        }
41    }
42}
43
44impl Display for ObjectAcl {
45    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        match self {
47            ObjectAcl::PublicReadWrite => write!(f, "public-read-write"),
48            ObjectAcl::PublicRead => write!(f, "public-read"),
49            ObjectAcl::Private => write!(f, "private"),
50            ObjectAcl::Default => write!(f, "default"),
51        }
52    }
53}
54
55impl AsRef<str> for ObjectAcl {
56    fn as_ref(&self) -> &str {
57        self.as_str()
58    }
59}
60
61impl TryFrom<&str> for ObjectAcl {
62    type Error = crate::error::Error;
63
64    fn try_from(s: &str) -> std::result::Result<Self, Self::Error> {
65        match s {
66            "public-read-write" => Ok(ObjectAcl::PublicReadWrite),
67            "public-read" => Ok(ObjectAcl::PublicRead),
68            "private" => Ok(ObjectAcl::Private),
69            "default" | "" => Ok(ObjectAcl::Default),
70            _ => Err(Error::Other(format!("Invalid ACL value: {}", s))),
71        }
72    }
73}
74
75impl TryFrom<String> for ObjectAcl {
76    type Error = crate::error::Error;
77
78    fn try_from(s: String) -> std::result::Result<Self, Self::Error> {
79        Self::try_from(s.as_str())
80    }
81}
82
83impl TryFrom<&String> for ObjectAcl {
84    type Error = crate::error::Error;
85
86    fn try_from(s: &String) -> std::result::Result<Self, Self::Error> {
87        Self::try_from(s.as_str())
88    }
89}
90
91#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
92#[cfg_attr(feature = "serde-support", derive(serde::Serialize, serde::Deserialize))]
93pub enum ContentEncoding {
94    /// 表示 Object 未经过压缩或编码
95    #[default]
96    #[cfg_attr(feature = "serde-support", serde(rename = "identity"))]
97    Identity,
98
99    /// 表示 Object 采用 Lempel-Ziv(LZ77) 压缩算法以及 32 位 CRC 校验的编码方式。
100    #[cfg_attr(feature = "serde-support", serde(rename = "gzip"))]
101    Gzip,
102
103    /// 表示 Object 采用 zlib 结构和 deflate 压缩算法的编码方式。
104    #[cfg_attr(feature = "serde-support", serde(rename = "deflate"))]
105    Deflate,
106
107    /// 表示 Object 采用 Lempel-Ziv-Welch(LZW) 压缩算法的编码方式。
108    #[cfg_attr(feature = "serde-support", serde(rename = "compress"))]
109    Compress,
110
111    /// 表示 Object 采用 Brotli 压缩算法的编码方式。
112    #[cfg_attr(feature = "serde-support", serde(rename = "br"))]
113    Brotli,
114}
115
116impl ContentEncoding {
117    pub fn as_str(&self) -> &str {
118        match self {
119            ContentEncoding::Identity => "identity",
120            ContentEncoding::Gzip => "gzip",
121            ContentEncoding::Deflate => "deflate",
122            ContentEncoding::Compress => "compress",
123            ContentEncoding::Brotli => "br",
124        }
125    }
126}
127
128impl TryFrom<&str> for ContentEncoding {
129    type Error = Error;
130
131    /// Try to create a ContentEncoding from a string.
132    /// Acceptable values are "identity", "gzip", "deflate", "compress", and "br".
133    fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
134        match value.to_lowercase().as_str() {
135            "identity" => Ok(ContentEncoding::Identity),
136            "gzip" => Ok(ContentEncoding::Gzip),
137            "deflate" => Ok(ContentEncoding::Deflate),
138            "compress" => Ok(ContentEncoding::Compress),
139            "br" => Ok(ContentEncoding::Brotli),
140            _ => Err(Error::Other(format!("invalid content encoding: {}", value))),
141        }
142    }
143}
144
145impl TryFrom<&String> for ContentEncoding {
146    type Error = Error;
147
148    /// See [`try_from(value: &str)`] for more details.
149    fn try_from(value: &String) -> std::result::Result<Self, Self::Error> {
150        Self::try_from(value.as_str())
151    }
152}
153
154impl TryFrom<String> for ContentEncoding {
155    type Error = Error;
156
157    /// See [`try_from(value: &str)`] for more details.
158    fn try_from(value: String) -> std::result::Result<Self, Self::Error> {
159        Self::try_from(value.as_str())
160    }
161}
162
163/// Options for putting object
164///
165/// Official document: <https://help.aliyun.com/zh/oss/developer-reference/putobject>
166#[derive(Debug, Clone, Default)]
167#[cfg_attr(feature = "serde-support", derive(serde::Serialize, serde::Deserialize))]
168#[cfg_attr(feature = "serde-camelcase", serde(rename_all = "camelCase"))]
169pub struct PutObjectOptions {
170    /// 文件的 mime_type。如果不指定,则从文件名猜测。如果猜测不到,则使用 application/octet-stream
171    /// 如果是直接从字节数组创建 Object 的,则不会猜测这个值,建议显式指定
172    pub mime_type: Option<String>,
173
174    /// 指定该 Object 被下载时网页的缓存行为。取值如下:
175    ///
176    /// - `no-cache`:不可直接使用缓存,而是先到服务端验证 Object 是否已更新。如果 Object 已更新,表明缓存已过期,需从服务端重新下载 Object;如果 Object 未更新,表明缓存未过期,此时将使用本地缓存。
177    /// - `no-store`:所有内容都不会被缓存。
178    /// - `public`:所有内容都将被缓存。
179    /// - `private`:所有内容只在客户端缓存。
180    /// - `max-age=<seconds>`:缓存内容的相对过期时间,单位为秒。此选项仅在 HTTP 1.1 中可用。示例:`max-age=3600`
181    pub cache_control: Option<String>,
182
183    /// 指定Object的展示形式。取值如下:
184    ///
185    /// - `inline`: 直接预览文件内容。
186    /// - `attachment`: 以原文件名的形式下载到浏览器指定路径。
187    /// - `attachment; filename="yourFileName"`: 以自定义文件名的形式下载到浏览器指定路径。 `yourFileName` 用于自定义下载后的文件名称,例如 `example.jpg`。
188    ///
189    /// 注意:
190    ///
191    /// - 如果 Object 名称包含星号(`*`)、正斜线(`/`)等特殊字符时,可能会出现特殊字符转义的情况。例如,下载 `example*.jpg` 到本地时,`example*.jpg` 可能会转义为`example_.jpg`。
192    /// - 如需确保下载名称中包含中文字符的 Object 到本地指定路径后,文件名称不出现乱码的现象,您需要将名称中包含的中文字符进行 URL 编码。例如,将`测试.txt` 从 OSS 下载到本地后,需要保留文件名为 `测试.txt`,需按照 `"attachment;filename="+URLEncoder.encode("测试","UTF-8")+".txt;filename*=UTF-8''"+URLEncoder.encode("测试","UTF-8")+".txt"` 的格式设置 `Content-Disposition`,即 `attachment;filename=%E6%B5%8B%E8%AF%95.txt;filename*=UTF-8''%E6%B5%8B%E8%AF%95.txt`
193    pub content_disposition: Option<String>,
194
195    pub content_encoding: Option<ContentEncoding>,
196
197    /// 上传内容的 MD5 摘要算法结果的 base64 字符串。用于检查消息内容是否与发送时一致。Content-MD5 是由 MD5 算法生成的值。上传了 Content-MD5 请求头后,OSS 会计算消息体的 Content-MD5 并检查一致性。
198    pub content_md5: Option<String>,
199
200    /// 过期时间。例如:`Wed, 08 Jul 2015 16:57:01 GMT`
201    pub expires: Option<String>,
202
203    /// 指定 PutObject 操作时是否覆盖同名 Object。
204    ///
205    /// 当目标 Bucket 处于已开启或已暂停的版本控制状态时,
206    /// `x-oss-forbid-overwrite` 请求 Header 设置无效,即允许覆盖同名 Object。
207    ///
208    /// - 不指定 `x-oss-forbid-overwrite` 或者指定 `x-oss-forbid-overwrite` 为 `false` 时,表示允许覆盖同名 Object。
209    /// - 指定 `x-oss-forbid-overwrite` 为 `true` 时,表示禁止覆盖同名 Object。
210    ///
211    /// **设置 `x-oss-forbid-overwrite` 请求 Header 导致 QPS 处理性能下降,如果您有大量的操作需要使用 `x-oss-forbid-overwrite` 请求 Header(QPS > 1000),请联系技术支持,避免影响您的业务。**
212    pub forbid_overwrite: Option<bool>,
213
214    /// 创建 Object 时,指定服务器端加密方式。
215    /// 指定此选项后,在响应头中会返回此选项,OSS 会对上传的 Object 进行加密编码存储。当下载该 Object 时,响应头中会包含 `x-oss-server-side-encryption`,且该值会被设置成此 Object 的加密算法。
216    pub server_side_encryption: Option<ServerSideEncryptionAlgorithm>,
217
218    /// 指定Object的加密算法。如果未指定此选项,表明 Object 使用 AES256 加密算法。此选项仅当 `x-oss-server-side-encryption` 为 KMS 时有效。
219    pub server_side_data_encryption: Option<ServerSideEncryptionAlgorithm>,
220
221    /// KMS托管的用户主密钥。此选项仅在 `x-oss-server-side-encryption` 为 KMS 时有效。
222    pub server_side_encryption_key_id: Option<String>,
223
224    /// 如果不指定,则默认采用 Bucket 的 ACL。
225    pub object_acl: Option<ObjectAcl>,
226
227    /// 如果不指定,则默认采用 Bucket 的存储类型。
228    pub storage_class: Option<StorageClass>,
229
230    /// 使用 PutObject 接口时,如果配置以 `x-oss-meta-` 为前缀的参数,则该参数视为元数据,例如 `x-oss-meta-location`。
231    /// 一个 Object 可以有多个类似的参数,但所有的元数据总大小不能超过 8 KB。
232    /// 元数据支持短划线(`-`)、数字、英文字母(`a~z`)。英文字符的大写字母会被转成小写字母,不支持下划线(`_`)在内的其他字符。
233    ///
234    /// **注意:Key 必须是 `x-oss-meta-` 开头的**
235    pub metadata: HashMap<String, String>,
236
237    /// Object 标签
238    /// 签合法字符集包括大小写字母、数字、空格和下列符号:`+ - = . _ : /`。
239    pub tags: HashMap<String, String>,
240
241    /// For `put_object` only.
242    pub callback: Option<Callback>,
243}
244
245pub struct PutObjectOptionsBuilder {
246    mime_type: Option<String>,
247    cache_control: Option<String>,
248    content_disposition: Option<String>,
249    content_encoding: Option<ContentEncoding>,
250    content_md5: Option<String>,
251    expires: Option<String>,
252    forbid_overwrite: Option<bool>,
253    server_side_encryption: Option<ServerSideEncryptionAlgorithm>,
254    server_side_data_encryption: Option<ServerSideEncryptionAlgorithm>,
255    server_side_encryption_key_id: Option<String>,
256    object_acl: Option<ObjectAcl>,
257    storage_class: Option<StorageClass>,
258    metadata: HashMap<String, String>,
259    tags: HashMap<String, String>,
260    callback: Option<Callback>,
261}
262
263impl PutObjectOptionsBuilder {
264    pub fn new() -> Self {
265        Self {
266            mime_type: None,
267            cache_control: None,
268            content_disposition: None,
269            content_encoding: None,
270            content_md5: None,
271            expires: None,
272            forbid_overwrite: None,
273            server_side_encryption: None,
274            server_side_data_encryption: None,
275            server_side_encryption_key_id: None,
276            object_acl: None,
277            storage_class: None,
278            metadata: HashMap::new(),
279            tags: HashMap::new(),
280            callback: None,
281        }
282    }
283
284    pub fn mime_type(mut self, mime_type: impl Into<String>) -> Self {
285        self.mime_type = Some(mime_type.into());
286        self
287    }
288
289    pub fn cache_control(mut self, cache_control: impl Into<String>) -> Self {
290        self.cache_control = Some(cache_control.into());
291        self
292    }
293
294    pub fn content_disposition(mut self, content_disposition: impl Into<String>) -> Self {
295        self.content_disposition = Some(content_disposition.into());
296        self
297    }
298
299    pub fn content_encoding(mut self, content_encoding: ContentEncoding) -> Self {
300        self.content_encoding = Some(content_encoding);
301        self
302    }
303
304    pub fn content_md5(mut self, content_md5: impl Into<String>) -> Self {
305        self.content_md5 = Some(content_md5.into());
306        self
307    }
308
309    pub fn expires(mut self, expires: impl Into<String>) -> Self {
310        self.expires = Some(expires.into());
311        self
312    }
313
314    pub fn forbid_overwrite(mut self, forbid_overwrite: bool) -> Self {
315        self.forbid_overwrite = Some(forbid_overwrite);
316        self
317    }
318
319    pub fn server_side_encryption(mut self, algorithm: ServerSideEncryptionAlgorithm) -> Self {
320        self.server_side_encryption = Some(algorithm);
321        self
322    }
323
324    pub fn server_side_data_encryption(mut self, algorithm: ServerSideEncryptionAlgorithm) -> Self {
325        self.server_side_data_encryption = Some(algorithm);
326        self
327    }
328
329    pub fn server_side_encryption_key_id(mut self, key_id: impl Into<String>) -> Self {
330        self.server_side_encryption_key_id = Some(key_id.into());
331        self
332    }
333
334    pub fn object_acl(mut self, acl: ObjectAcl) -> Self {
335        self.object_acl = Some(acl);
336        self
337    }
338
339    pub fn storage_class(mut self, storage_class: StorageClass) -> Self {
340        self.storage_class = Some(storage_class);
341        self
342    }
343
344    pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
345        self.metadata.insert(key.into(), value.into());
346        self
347    }
348
349    pub fn tag(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
350        self.tags.insert(key.into(), value.into());
351        self
352    }
353
354    pub fn callback(mut self, cb: Callback) -> Self {
355        self.callback = Some(cb);
356        self
357    }
358
359    pub fn build(self) -> PutObjectOptions {
360        PutObjectOptions {
361            mime_type: self.mime_type,
362            cache_control: self.cache_control,
363            content_disposition: self.content_disposition,
364            content_encoding: self.content_encoding,
365            content_md5: self.content_md5,
366            expires: self.expires,
367            forbid_overwrite: self.forbid_overwrite,
368            server_side_encryption: self.server_side_encryption,
369            server_side_data_encryption: self.server_side_data_encryption,
370            server_side_encryption_key_id: self.server_side_encryption_key_id,
371            object_acl: self.object_acl,
372            storage_class: self.storage_class,
373            metadata: self.metadata,
374            tags: self.tags,
375            callback: self.callback,
376        }
377    }
378}
379
380impl Default for PutObjectOptionsBuilder {
381    fn default() -> Self {
382        Self::new()
383    }
384}
385
386/// Options for getting object
387///
388/// Official document: <https://help.aliyun.com/zh/oss/developer-reference/getobject>
389pub struct GetObjectOptions {
390    // The following fields are header items
391    /// 指定文件传输的范围。
392    ///
393    /// - 如果指定的范围符合规范,返回消息中会包含整个 Object 的大小和此次返回 Object 的范围。例如:`Content-Range: bytes 0~9/44`,表示整个 Object 大小为 `44`,此次返回的范围为 `0~9`。
394    /// - 如果指定的范围不符合规范,则传送整个 Object,并且结果中不包含 `Content-Range`。
395    pub range: Option<String>,
396
397    /// GMT 日期时间字符串,例如:`Fri, 13 Nov 2015 14:47:53 GMT`
398    ///
399    /// 如果指定的时间早于实际修改时间或指定的时间不符合规范,则直接返回 Object,并返回 `200 OK`;
400    /// 如果指定的时间等于或者晚于实际修改时间,则返回 `304 Not Modified`。
401    pub if_modified_since: Option<String>,
402
403    /// GMT 日期时间字符串,例如:`Fri, 13 Nov 2015 14:47:53 GMT`
404    ///
405    /// 如果指定的时间等于或者晚于 Object 实际修改时间,则正常传输 Object,并返回 `200 OK`;
406    /// 如果指定的时间早于实际修改时间,则返回 `412 Precondition Failed`。
407    /// `If-Modified-Since` 和 `If-Unmodified-Since` 可以同时使用。
408    pub if_unmodified_since: Option<String>,
409
410    /// ETag 值
411    ///
412    /// 如果传入的 `ETag` 和 Object 的 `ETag` 匹配,则正常传输 Object,并返回 `200 OK`;
413    /// 如果传入的 `ETag` 和 Object 的 `ETag` 不匹配,则返回 `412 Precondition Failed`。
414    pub if_match: Option<String>,
415
416    /// ETag 值
417    ///
418    /// 如果传入的 `ETag` 值和 `Object` 的 `ETag` 不匹配,则正常传输 Object,并返回 `200 OK`;
419    /// 如果传入的 `ETag` 和 `Object` 的 `ETag` 匹配,则返回 `304 Not Modified`。
420    ///
421    /// `If-Match` 和 `If-None-Match` 可以同时使用。
422    pub if_none_match: Option<String>,
423
424    /// 指定客户端的编码类型。例如:gzip
425    ///
426    /// 如果要对返回内容进行 Gzip 压缩传输,您需要在请求头中以显示方式加入 `Accept-Encoding:gzip`。
427    /// OSS 会根据 Object 的 Content-Type 和 Object 大小(不小于1KB),
428    /// 判断传输过程中是否对数据进行 Gzip 压缩。满足条件时,数据以压缩形式传输,否则,数据以原始形式传输。
429    ///
430    /// 注意:
431    ///
432    /// - 如果采用了 Gzip 压缩且压缩生效,则不会附带 `ETag` 和 `Content-Length` 信息。
433    /// - 目前 OSS 支持对以下 `Content-Type` 类型的数据进行 Gzip 压缩:
434    ///   - text/cache-manifest
435    ///   - text/xml
436    ///   - text/css
437    ///   - text/html
438    ///   - text/plain
439    ///   - application/javascript
440    ///   - application/x-javascript
441    ///   - application/rss+xml
442    ///   - application/json
443    ///   - text/json
444    pub accept_encoding: Option<String>,
445
446    // The following fields are query parameters
447    /// Add `Content-Language` to response header
448    pub response_content_language: Option<String>,
449
450    /// Add `Expires` to response header
451    pub response_expires: Option<String>,
452
453    /// Add `Cache-Control` to response header
454    pub response_cache_control: Option<String>,
455
456    /// Add `Content-Disposition` to response header
457    pub response_content_disposition: Option<String>,
458
459    /// Add `Content-Encoding` to response header
460    pub response_content_encoding: Option<ContentEncoding>,
461
462    /// The version to retreive
463    pub version_id: Option<String>,
464}
465
466pub struct GetObjectOptionsBuilder {
467    range: Option<String>,
468    if_modified_since: Option<String>,
469    if_unmodified_since: Option<String>,
470    if_match: Option<String>,
471    if_non_match: Option<String>,
472    accept_encoding: Option<String>,
473    response_content_language: Option<String>,
474    response_expires: Option<String>,
475    response_cache_control: Option<String>,
476    response_content_disposition: Option<String>,
477    response_content_encoding: Option<ContentEncoding>,
478    version_id: Option<String>,
479}
480
481impl GetObjectOptionsBuilder {
482    pub fn new() -> Self {
483        Self {
484            range: None,
485            if_modified_since: None,
486            if_unmodified_since: None,
487            if_match: None,
488            if_non_match: None,
489            accept_encoding: None,
490            response_content_language: None,
491            response_expires: None,
492            response_cache_control: None,
493            response_content_disposition: None,
494            response_content_encoding: None,
495            version_id: None,
496        }
497    }
498
499    pub fn range(mut self, range: impl Into<String>) -> Self {
500        self.range = Some(range.into());
501        self
502    }
503
504    pub fn if_modified_since(mut self, if_modified_since: impl Into<String>) -> Self {
505        self.if_modified_since = Some(if_modified_since.into());
506        self
507    }
508
509    pub fn if_unmodified_since(mut self, if_unmodified_since: impl Into<String>) -> Self {
510        self.if_unmodified_since = Some(if_unmodified_since.into());
511        self
512    }
513
514    pub fn if_match(mut self, if_match: impl Into<String>) -> Self {
515        self.if_match = Some(if_match.into());
516        self
517    }
518
519    pub fn if_non_match(mut self, if_non_match: impl Into<String>) -> Self {
520        self.if_non_match = Some(if_non_match.into());
521        self
522    }
523
524    pub fn accept_encoding(mut self, accept_encoding: impl Into<String>) -> Self {
525        self.accept_encoding = Some(accept_encoding.into());
526        self
527    }
528
529    pub fn response_content_language(mut self, content_language: impl Into<String>) -> Self {
530        self.response_content_language = Some(content_language.into());
531        self
532    }
533
534    pub fn response_expires(mut self, expires: impl Into<String>) -> Self {
535        self.response_expires = Some(expires.into());
536        self
537    }
538
539    pub fn response_cache_control(mut self, cache_control: impl Into<String>) -> Self {
540        self.response_cache_control = Some(cache_control.into());
541        self
542    }
543
544    pub fn response_content_disposition(mut self, content_disposition: impl Into<String>) -> Self {
545        self.response_content_disposition = Some(content_disposition.into());
546        self
547    }
548
549    pub fn response_content_encoding(mut self, content_encoding: ContentEncoding) -> Self {
550        self.response_content_encoding = Some(content_encoding);
551        self
552    }
553
554    pub fn version_id(mut self, version_id: impl Into<String>) -> Self {
555        self.version_id = Some(version_id.into());
556        self
557    }
558
559    pub fn build(self) -> GetObjectOptions {
560        GetObjectOptions {
561            range: self.range,
562            if_modified_since: self.if_modified_since,
563            if_unmodified_since: self.if_unmodified_since,
564            if_match: self.if_match,
565            if_none_match: self.if_non_match,
566            accept_encoding: self.accept_encoding,
567            response_content_language: self.response_content_language,
568            response_expires: self.response_expires,
569            response_cache_control: self.response_cache_control,
570            response_content_disposition: self.response_content_disposition,
571            response_content_encoding: self.response_content_encoding,
572            version_id: self.version_id,
573        }
574    }
575}
576
577impl Default for GetObjectOptionsBuilder {
578    fn default() -> Self {
579        Self::new()
580    }
581}
582
583/// A "placeholder" struct for adding more fields in the future
584#[derive(Debug)]
585pub struct GetObjectResult;
586
587pub(crate) fn build_put_object_request(
588    bucket_name: &str,
589    object_key: &str,
590    request_body: RequestBody,
591    options: &Option<PutObjectOptions>,
592) -> Result<OssRequest> {
593    if !validate_bucket_name(bucket_name) {
594        return Err(Error::Other(format!("invalid bucket name: {}", bucket_name)));
595    }
596
597    if !validate_object_key(object_key) {
598        return Err(Error::Other(format!("invalid object key: {}", object_key)));
599    }
600
601    // check for metadata and tags
602    if let Some(options) = &options {
603        for (k, v) in &options.metadata {
604            if k.is_empty() || !validate_meta_key(k) || v.is_empty() {
605                return Err(Error::Other(format!("invalid meta data: \"{}: {}\". the key must starts with `x-oss-meta-`, and only `[0-9a-z\\-]` are allowed; the key and value must not be empty", k, v)));
606            }
607        }
608
609        for (k, v) in &options.tags {
610            if k.is_empty() || !validate_tag_key(k) || (!v.is_empty() && !validate_tag_value(v)) {
611                return Err(Error::Other(format!(
612                    "invalid tagging data: \"{}={}\". only `[0-9a-zA-Z\\+\\-=\\.:/]` and space character are allowed",
613                    k, v
614                )));
615            }
616        }
617    }
618
619    let mut request = OssRequest::new().method(RequestMethod::Put).bucket(bucket_name).object(object_key);
620
621    let content_length = match &request_body {
622        RequestBody::Empty => 0u64,
623        RequestBody::Text(s) => s.len() as u64,
624        RequestBody::Bytes(bytes) => bytes.len() as u64,
625        RequestBody::File(file_path, range) => {
626            if let Some(r) = range {
627                r.end - r.start
628            } else {
629                if !file_path.exists() || !file_path.is_file() {
630                    return Err(Error::Other(format!(
631                        "{} does not exist or is not a regular file",
632                        file_path.as_os_str().to_str().unwrap_or("UNKNOWN")
633                    )));
634                }
635
636                let file_meta = std::fs::metadata(file_path)?;
637
638                file_meta.len()
639            }
640        }
641    };
642
643    // max file size for putting object is 5GB
644    if content_length > 5_368_709_120 {
645        return Err(Error::Other(format!("length {} exceeds limitation. max allowed is 5GB", content_length)));
646    }
647
648    request = request.content_length(content_length);
649
650    // if no `content-type` specified, try to guess from file
651    if let RequestBody::File(file_path, _) = &request_body {
652        request = request.content_type(mime_guess::from_path(file_path).first_or_octet_stream().as_ref());
653    }
654
655    // move the body to request
656    request = request.body(request_body);
657
658    if let Some(options) = options {
659        // if `mime_type` is specified, overwrite it's value which guess from file (maybe)
660        if let Some(s) = &options.mime_type {
661            request = request.add_header("content-type", s);
662        }
663
664        if let Some(s) = &options.cache_control {
665            request = request.add_header("cache-control", s);
666        }
667
668        if let Some(s) = &options.content_disposition {
669            request = request.add_header("content-disposition", s);
670        }
671
672        if let Some(enc) = &options.content_encoding {
673            request = request.add_header("content-encoding", enc.as_str());
674        }
675
676        if let Some(s) = &options.expires {
677            request = request.add_header("expires", s);
678        }
679
680        if let Some(b) = &options.forbid_overwrite {
681            if *b {
682                request = request.add_header("x-oss-forbid-overwrite", "true");
683            }
684        }
685
686        if let Some(a) = &options.server_side_encryption {
687            request = request.add_header("x-oss-server-side-encryption", a.as_str());
688        }
689
690        if let Some(a) = &options.server_side_data_encryption {
691            request = request.add_header("x-oss-server-side-data-encryption", a.as_str());
692        }
693
694        if let Some(s) = &options.server_side_encryption_key_id {
695            request = request.add_header("x-oss-server-side-encryption-key-id", s);
696        }
697
698        if let Some(acl) = &options.object_acl {
699            request = request.add_header("x-oss-object-acl", acl.as_str());
700        }
701
702        if let Some(store) = &options.storage_class {
703            request = request.add_header("x-oss-storage-class", store.as_str());
704        }
705
706        for (k, v) in &options.metadata {
707            request = request.add_header(k, v);
708        }
709
710        if !options.tags.is_empty() {
711            request = request.add_header("x-oss-tagging", build_tag_string(&options.tags));
712        }
713
714        if let Some(cb) = &options.callback {
715            // custom variable values are not serialized
716            let callback_json = serde_json::to_string(cb)?;
717            let callback_base64 = BASE64_STANDARD.encode(&callback_json);
718            request = request.add_header("x-oss-callback", callback_base64);
719
720            if !cb.custom_variables.is_empty() {
721                let callback_vars_json = serde_json::to_string(&cb.custom_variables)?;
722                let callback_vars_base64 = BASE64_STANDARD.encode(&callback_vars_json);
723                request = request.add_header("x-oss-callback-var", callback_vars_base64);
724            }
725        }
726    }
727
728    Ok(request)
729}
730
731/// Options for getting object metadata
732///
733/// Official document: <https://help.aliyun.com/zh/oss/developer-reference/getobjectmeta>
734#[derive(Debug, Clone, Default)]
735#[cfg_attr(feature = "serde-support", derive(serde::Serialize, serde::Deserialize))]
736#[cfg_attr(feature = "serde-camelcase", serde(rename_all = "camelCase"))]
737pub struct GetObjectMetadataOptions {
738    pub version_id: Option<String>,
739}
740
741/// Options for heading object
742///
743/// Official document: <https://help.aliyun.com/zh/oss/developer-reference/headobject>
744#[derive(Debug, Clone, Default)]
745#[cfg_attr(feature = "serde-support", derive(serde::Serialize, serde::Deserialize))]
746#[cfg_attr(feature = "serde-camelcase", serde(rename_all = "camelCase"))]
747pub struct HeadObjectOptions {
748    pub version_id: Option<String>,
749    pub if_modified_since: Option<String>,
750    pub if_unmodified_since: Option<String>,
751    pub if_match: Option<String>,
752    pub if_none_match: Option<String>,
753}
754
755pub struct HeadObjectOptionsBuilder {
756    version_id: Option<String>,
757    if_modified_since: Option<String>,
758    if_unmodified_since: Option<String>,
759    if_match: Option<String>,
760    if_none_match: Option<String>,
761}
762
763impl HeadObjectOptionsBuilder {
764    pub fn new() -> Self {
765        Self {
766            version_id: None,
767            if_modified_since: None,
768            if_unmodified_since: None,
769            if_match: None,
770            if_none_match: None,
771        }
772    }
773
774    pub fn version_id(mut self, version_id: impl Into<String>) -> Self {
775        self.version_id = Some(version_id.into());
776        self
777    }
778
779    pub fn if_modified_since(mut self, if_modified_since: impl Into<String>) -> Self {
780        self.if_modified_since = Some(if_modified_since.into());
781        self
782    }
783
784    pub fn if_unmodified_since(mut self, if_unmodified_since: impl Into<String>) -> Self {
785        self.if_unmodified_since = Some(if_unmodified_since.into());
786        self
787    }
788
789    pub fn if_match(mut self, if_match: impl Into<String>) -> Self {
790        self.if_match = Some(if_match.into());
791        self
792    }
793
794    pub fn if_none_match(mut self, if_none_match: impl Into<String>) -> Self {
795        self.if_none_match = Some(if_none_match.into());
796        self
797    }
798
799    pub fn build(self) -> HeadObjectOptions {
800        HeadObjectOptions {
801            version_id: self.version_id,
802            if_modified_since: self.if_modified_since,
803            if_unmodified_since: self.if_unmodified_since,
804            if_match: self.if_match,
805            if_none_match: self.if_none_match,
806        }
807    }
808}
809
810impl Default for HeadObjectOptionsBuilder {
811    fn default() -> Self {
812        Self::new()
813    }
814}
815
816#[derive(Debug, Clone, Default)]
817#[cfg_attr(feature = "serde-support", derive(serde::Serialize, serde::Deserialize))]
818#[cfg_attr(feature = "serde-camelcase", serde(rename_all = "camelCase"))]
819pub struct ObjectMetadata {
820    pub request_id: String,
821    pub content_length: u64,
822
823    /// 已经移除了首尾双引号(`"`)之后的字符串
824    pub etag: String,
825    pub hash_crc64ecma: Option<u64>,
826
827    /// Object 通过生命周期规则转储为冷归档或者深度冷归档存储类型的时间。
828    pub transition_time: Option<String>,
829
830    /// Object 的最后一次访问时间。时间格式为 HTTP 1.1 协议中规定的 GMT 时间。
831    /// 开启访问跟踪时,该字段的值会随着文件被访问的时间持续更新。
832    /// 如果开启后关闭了访问跟踪,该字段的值保留为上一次最后更新的值。
833    /// 示例: `Tue, 30 Mar 2021 06:07:48 GMT`
834    pub last_access_time: Option<String>,
835
836    /// 时间格式为 HTTP 1.1 协议中规定的 GMT 时间。
837    pub last_modified: Option<String>,
838
839    pub version_id: Option<String>,
840
841    pub server_side_encryption: Option<ServerSideEncryptionAlgorithm>,
842    pub server_side_encryption_key_id: Option<String>,
843    pub storage_class: Option<StorageClass>,
844    pub object_type: Option<ObjectType>,
845
846    /// 对于 `Appendable` 类型的 Object 会返回此 Header,指明下一次请求应当提供的 `position`。
847    pub next_append_position: Option<u64>,
848
849    /// 配置了生命周期规则的Bucket中Object的过期时间。
850    pub expiration: Option<String>,
851
852    /// 如果 Object 存储类型为 `Archive`、`ColdArchive` 或者 `DeepColdArchive`,
853    /// 且您已提交 Restore 请求,则响应头中会以 `x-oss-restore` 返回该 Object 的 Restore 状态,分如下几种情况:
854    ///
855    /// - 如果没有提交 Restore 或者 Restore 已经超时,则不返回该字段。
856    /// - 如果已经提交 Restore,且 Restore 没有完成,则返回的 `x-oss-restore` 值为 `ongoing-request="true"`。
857    /// - 如果已经提交 Restore,且 Restore 已经完成,则返回的 `x-oss-restore` 值为 `ongoing-request="false", expiry-date="Sun, 16 Apr 2017 08:12:33 GMT"`,其中 `expiry-date` 是 Restore 完成后 Object 进入可读状态的过期时间。
858    pub restore: Option<String>,
859
860    /// 当用户通过轻量消息队列 SMQ 创建 OSS 事件通知后,
861    /// 在进行请求 OSS 相关操作时如果有匹配的事件通知规则,
862    /// 则响应中会携带这个 Header,值为经过 Base64 编码 JSON 格式的事件通知结果。
863    pub process_status: Option<String>,
864
865    /// 当 Object 所属的 Bucket 被设置为请求者付费模式,
866    /// 且请求者不是 Bucket 的拥有者时,响应中将携带此 Header,值为 `requester`。
867    pub request_charged: Option<String>,
868
869    /// - 对于 `Normal` 类型的 Object,根据 RFC 1864 标准对消息内容(不包括Header)计算 Md5 值获得 128 比特位数字,对该数字进行 Base64 编码作为一个消息的 Content-Md5 值。
870    /// - `Multipart` 和 `Appendable` 类型的文件不会返回这个 Header。
871    pub content_md5: Option<String>,
872
873    /// 当 Object 所在的 Bucket 配置了 CORS 规则,且请求的 Origin 满足指定的 CORS 规则时会在响应中包含这个 Origin。
874    pub access_control_allow_origin: Option<String>,
875
876    /// 当 Object 所在的 Bucket 配置了 CORS 规则,且请求的 `Access-Control-Request-Method` 满足指定的CORS规则时会在响应中包含允许的 Methods。
877    pub access_control_allow_methods: Option<String>,
878
879    /// 当 Object 所在的 Bucket 配置了 CORS 规则,且请求满足 Bucket 配置的 CORS 规则时会在响应中包含 `MaxAgeSeconds`。
880    pub access_control_allow_max_age: Option<String>,
881
882    /// 当 Object 所在的 Bucket 配置了 CORS 规则,且请求满足指定的 CORS 规则时会在响应中包含这些 Headers。
883    pub access_control_allow_headers: Option<String>,
884
885    /// 表示允许访问客户端 JavaScript 程序的 headers 列表。当 Object 所在的 Bucket 配置了 CORS 规则,且请求满足指定的CORS规则时会在响应中包含 ExposeHeader。
886    pub access_control_expose_headers: Option<String>,
887
888    /// 对象关联的标签个数。仅当用户有读取标签权限时返回。
889    pub tag_count: Option<u32>,
890
891    /// `x-oss-meta-` 开头的用户自定义属性
892    pub metadata: HashMap<String, String>,
893}
894
895impl From<HashMap<String, String>> for ObjectMetadata {
896    /// Consumes the headers map and return ObjectMetadata
897    fn from(mut headers: HashMap<String, String>) -> Self {
898        Self {
899            request_id: headers.remove("x-oss-request-id").unwrap_or("".to_string()),
900            content_length: headers.remove("content-length").unwrap_or("0".to_string()).parse().unwrap_or(0),
901            etag: sanitize_etag(headers.remove("etag").unwrap_or_default()),
902            hash_crc64ecma: headers.remove("x-oss-hash-crc64ecma").map(|s| s.parse::<u64>().unwrap_or(0)),
903            transition_time: headers.remove("x-oss-transition-time"),
904            last_access_time: headers.remove("x-oss-last-access-time"),
905            last_modified: headers.remove("last-modified"),
906            version_id: headers.remove("x-oss-version-id"),
907            server_side_encryption: if let Some(s) = headers.remove("x-oss-server-side-encryption") {
908                // Not good...
909                if let Ok(v) = s.try_into() {
910                    Some(v)
911                } else {
912                    None
913                }
914            } else {
915                None
916            },
917            server_side_encryption_key_id: headers.remove("x-oss-server-side-encryption-key-id"),
918            storage_class: if let Some(s) = headers.remove("x-oss-storage-class") {
919                if let Ok(v) = s.try_into() {
920                    Some(v)
921                } else {
922                    None
923                }
924            } else {
925                None
926            },
927            object_type: if let Some(s) = headers.remove("x-oss-object-type") {
928                if let Ok(v) = s.try_into() {
929                    Some(v)
930                } else {
931                    None
932                }
933            } else {
934                None
935            },
936            next_append_position: headers.remove("x-oss-next-append-position").map(|s| s.parse().unwrap_or(0)),
937            expiration: headers.remove("x-oss-expiration"),
938            restore: headers.remove("x-oss-restore"),
939            process_status: headers.remove("x-oss-process-status"),
940            request_charged: headers.remove("x-oss-request-charged"),
941            content_md5: headers.remove("content-md5"),
942            access_control_allow_origin: headers.remove("access-control-allow-origin"),
943            access_control_allow_methods: headers.remove("access-control-allow-methods"),
944            access_control_allow_headers: headers.remove("access-control-allow-headers"),
945            access_control_allow_max_age: headers.remove("access-control-max-age"),
946            access_control_expose_headers: headers.remove("access-control-expose-headers"),
947            tag_count: headers.remove("x-oss-tagging-count").map(|s| s.parse().unwrap_or(0)),
948
949            // CAUTION!! must be the last field to handle because `drain` consumes all the entries left in the map
950            metadata: headers.drain().filter(|(k, _)| k.starts_with("x-oss-meta-")).collect(),
951        }
952    }
953}
954
955/// Options for copying objects
956///
957/// Official document: <https://help.aliyun.com/zh/oss/developer-reference/copyobject>
958#[derive(Debug, Clone, Default)]
959#[cfg_attr(feature = "serde-support", derive(serde::Serialize, serde::Deserialize))]
960#[cfg_attr(feature = "serde-camelcase", serde(rename_all = "camelCase"))]
961pub struct CopyObjectOptions {
962    /// 指定 CopyObject 操作时是否覆盖同名目标 Object。
963    /// 当目标 Bucket 处于已开启或已暂停版本控制状态时,`x-oss-forbid-overwrite` 请求 Header 设置无效,即允许覆盖同名Object。
964    ///
965    /// - 未指定 `x-oss-forbid-overwrite` 或者指定 `x-oss-forbid-overwrite` 为 `false` 时,表示允许覆盖同名目标 Object。
966    /// - 指定 `x-oss-forbid-overwrite` 为 `true` 时,表示禁止覆盖同名 Object。
967    ///
968    /// 设置`x-oss-forbid-overwrite` 请求 Header 会导致 QPS 处理性能下降,
969    /// 如果您有大量的操作需要使用 `x-oss-forbid-overwrite` 请求 Header(QPS>1000),请联系技术支持,避免影响您的业务。
970    pub forbid_overwrite: Option<bool>,
971
972    /// 默认复制源 Object 的当前版本。如果需要复制指定的版本,请设置此参数
973    pub source_version_id: Option<String>,
974
975    /// 如果源 Object 的 ETag 值和您提供的 ETag 相等,则执行拷贝操作,并返回 200 OK。
976    pub copy_source_if_match: Option<String>,
977
978    /// 如果源 Object 的 ETag 值和您提供的 ETag 不相等,则执行拷贝操作,并返回 200 OK。
979    pub copy_source_if_none_match: Option<String>,
980
981    /// 如果指定的时间等于或者晚于文件实际修改时间,则正常拷贝文件,并返回 200 OK。
982    /// e.g. `Mon, 11 May 2020 08:16:23 GMT`
983    pub copy_source_if_unmodified_since: Option<String>,
984
985    /// 如果指定的时间早于文件实际修改时间,则正常拷贝文件,并返回200 OK。
986    /// e.g. `Mon, 11 May 2020 08:16:23 GMT`
987    pub copy_source_if_modified_since: Option<String>,
988
989    /// 指定如何设置目标 Object 的元数据。
990    pub metadata_directive: Option<MetadataDirective>,
991
992    /// Key 以 `x-oss-meta-` 开头
993    pub metadata: HashMap<String, String>,
994
995    pub server_side_encryption: Option<ServerSideEncryptionAlgorithm>,
996    pub server_side_encryption_key_id: Option<String>,
997
998    /// 指定 OSS 创建目标 Object 时的访问权限。
999    pub object_acl: Option<ObjectAcl>,
1000
1001    /// 指定 OSS 创建目标 Object 时的存储类型
1002    pub storage_class: Option<StorageClass>,
1003
1004    pub tags: HashMap<String, String>,
1005    pub tag_directive: Option<TagDirective>,
1006}
1007
1008pub struct CopyObjectOptionsBuilder {
1009    forbid_overwrite: Option<bool>,
1010    source_version_id: Option<String>,
1011    copy_source_if_match: Option<String>,
1012    copy_source_if_none_match: Option<String>,
1013    copy_source_if_unmodified_since: Option<String>,
1014    copy_source_if_modified_since: Option<String>,
1015    metadata_directive: Option<MetadataDirective>,
1016    metadata: HashMap<String, String>,
1017    server_side_encryption: Option<ServerSideEncryptionAlgorithm>,
1018    server_side_encryption_key_id: Option<String>,
1019    object_acl: Option<ObjectAcl>,
1020    storage_class: Option<StorageClass>,
1021    tags: HashMap<String, String>,
1022    tag_directive: Option<TagDirective>,
1023}
1024
1025impl CopyObjectOptionsBuilder {
1026    pub fn new() -> Self {
1027        Self {
1028            forbid_overwrite: None,
1029            source_version_id: None,
1030            copy_source_if_match: None,
1031            copy_source_if_none_match: None,
1032            copy_source_if_unmodified_since: None,
1033            copy_source_if_modified_since: None,
1034            metadata_directive: None,
1035            metadata: HashMap::new(),
1036            server_side_encryption: None,
1037            server_side_encryption_key_id: None,
1038            object_acl: None,
1039            storage_class: None,
1040            tags: HashMap::new(),
1041            tag_directive: None,
1042        }
1043    }
1044
1045    pub fn forbid_overwrite(mut self, forbid_overwrite: bool) -> Self {
1046        self.forbid_overwrite = Some(forbid_overwrite);
1047        self
1048    }
1049
1050    pub fn source_version_id(mut self, version_id: impl Into<String>) -> Self {
1051        self.source_version_id = Some(version_id.into());
1052        self
1053    }
1054
1055    pub fn copy_source_if_match(mut self, copy_source_if_match: impl Into<String>) -> Self {
1056        self.copy_source_if_match = Some(copy_source_if_match.into());
1057        self
1058    }
1059
1060    pub fn copy_source_if_none_match(mut self, copy_source_if_none_match: impl Into<String>) -> Self {
1061        self.copy_source_if_none_match = Some(copy_source_if_none_match.into());
1062        self
1063    }
1064
1065    pub fn copy_source_if_unmodified_since(mut self, copy_source_if_unmodified_since: impl Into<String>) -> Self {
1066        self.copy_source_if_unmodified_since = Some(copy_source_if_unmodified_since.into());
1067        self
1068    }
1069
1070    pub fn copy_source_if_modified_since(mut self, copy_source_if_modified_since: impl Into<String>) -> Self {
1071        self.copy_source_if_modified_since = Some(copy_source_if_modified_since.into());
1072        self
1073    }
1074
1075    pub fn metadata_directive(mut self, metadata_directive: MetadataDirective) -> Self {
1076        self.metadata_directive = Some(metadata_directive);
1077        self
1078    }
1079
1080    pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
1081        self.metadata.insert(key.into(), value.into());
1082        self
1083    }
1084
1085    pub fn server_side_encryption(mut self, algorithm: ServerSideEncryptionAlgorithm) -> Self {
1086        self.server_side_encryption = Some(algorithm);
1087        self
1088    }
1089
1090    pub fn server_side_encryption_key_id(mut self, key_id: impl Into<String>) -> Self {
1091        self.server_side_encryption_key_id = Some(key_id.into());
1092        self
1093    }
1094
1095    pub fn object_acl(mut self, acl: ObjectAcl) -> Self {
1096        self.object_acl = Some(acl);
1097        self
1098    }
1099
1100    pub fn storage_class(mut self, storage_class: StorageClass) -> Self {
1101        self.storage_class = Some(storage_class);
1102        self
1103    }
1104
1105    pub fn tag(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
1106        self.tags.insert(key.into(), value.into());
1107        self
1108    }
1109
1110    pub fn tag_directive(mut self, tag_directive: TagDirective) -> Self {
1111        self.tag_directive = Some(tag_directive);
1112        self
1113    }
1114
1115    pub fn build(self) -> CopyObjectOptions {
1116        CopyObjectOptions {
1117            forbid_overwrite: self.forbid_overwrite,
1118            source_version_id: self.source_version_id,
1119            copy_source_if_match: self.copy_source_if_match,
1120            copy_source_if_none_match: self.copy_source_if_none_match,
1121            copy_source_if_unmodified_since: self.copy_source_if_unmodified_since,
1122            copy_source_if_modified_since: self.copy_source_if_modified_since,
1123            metadata_directive: self.metadata_directive,
1124            metadata: self.metadata,
1125            server_side_encryption: self.server_side_encryption,
1126            server_side_encryption_key_id: self.server_side_encryption_key_id,
1127            object_acl: self.object_acl,
1128            storage_class: self.storage_class,
1129            tags: self.tags,
1130            tag_directive: self.tag_directive,
1131        }
1132    }
1133}
1134
1135impl Default for CopyObjectOptionsBuilder {
1136    fn default() -> Self {
1137        Self::new()
1138    }
1139}
1140
1141/// Options for deleting object
1142#[derive(Debug, Clone, Default)]
1143#[cfg_attr(feature = "serde-support", derive(serde::Serialize, serde::Deserialize))]
1144#[cfg_attr(feature = "serde-camelcase", serde(rename_all = "camelCase"))]
1145pub struct DeleteObjectOptions {
1146    pub version_id: Option<String>,
1147}
1148
1149/// A "placeholder" struct for adding more fields in the future
1150pub struct DeleteObjectResult;
1151
1152/// A "placeholder" struct for adding more fields in the future
1153pub struct CopyObjectResult;
1154
1155pub(crate) fn build_copy_object_request(
1156    source_bucket_name: &str,
1157    source_object_key: &str,
1158    dest_bucket_name: &str,
1159    dest_object_key: &str,
1160    options: &Option<CopyObjectOptions>,
1161) -> Result<OssRequest> {
1162    if !validate_bucket_name(source_bucket_name) {
1163        return Err(Error::Other(format!("invalid source bucket name: {}", source_bucket_name)));
1164    }
1165
1166    if !validate_object_key(source_object_key) {
1167        return Err(Error::Other(format!("invalid source object key: {}", source_object_key)));
1168    }
1169
1170    if !validate_bucket_name(dest_bucket_name) {
1171        return Err(Error::Other(format!("invalid destination bucket name: {}", dest_bucket_name)));
1172    }
1173
1174    if !validate_object_key(dest_object_key) {
1175        return Err(Error::Other(format!("invalid destination object key: {}", dest_object_key)));
1176    }
1177
1178    let mut request = OssRequest::new()
1179        .method(RequestMethod::Put)
1180        .bucket(dest_bucket_name)
1181        .object(dest_object_key)
1182        .add_header(
1183            "x-oss-copy-source",
1184            format!("/{}/{}", urlencoding::encode(source_bucket_name), urlencoding::encode(source_object_key)),
1185        );
1186
1187    if let Some(options) = options {
1188        // validate metadata key and taggings
1189        for (k, _) in options.metadata.iter() {
1190            if !validate_meta_key(k) {
1191                return Err(Error::Other(format!("invalid metadata key: {}", k)));
1192            }
1193        }
1194
1195        for (k, v) in options.tags.iter() {
1196            if !validate_tag_key(k) || !validate_tag_value(v) {
1197                return Err(Error::Other(format!("invalid tagging data: {}={}", k, v)));
1198            }
1199        }
1200
1201        if let Some(s) = &options.source_version_id {
1202            request = request.add_query("versionId", s);
1203        }
1204
1205        if let Some(b) = options.forbid_overwrite {
1206            request = request.add_header("x-oss-forbid-overwrite", b.to_string())
1207        }
1208
1209        if let Some(s) = &options.copy_source_if_match {
1210            request = request.add_header("x-oss-copy-source-if-match", s);
1211        }
1212
1213        if let Some(s) = &options.copy_source_if_none_match {
1214            request = request.add_header("x-oss-copy-source-if-none-match", s);
1215        }
1216
1217        if let Some(s) = &options.copy_source_if_modified_since {
1218            request = request.add_header("x-oss-copy-source-if-modified-since", s);
1219        }
1220
1221        if let Some(s) = &options.copy_source_if_unmodified_since {
1222            request = request.add_header("x-oss-copy-source-if-unmodified-since", s);
1223        }
1224
1225        if let Some(md) = options.metadata_directive {
1226            request = request.add_header("x-oss-metadata-directive", md);
1227        }
1228
1229        if let Some(a) = &options.server_side_encryption {
1230            request = request.add_header("x-oss-server-side-encryption", a);
1231        }
1232
1233        if let Some(s) = &options.server_side_encryption_key_id {
1234            request = request.add_header("x-oss-server-side-encryption-key-id", s);
1235        }
1236
1237        if let Some(acl) = options.object_acl {
1238            request = request.add_header("x-oss-object-acl", acl);
1239        }
1240
1241        if let Some(sc) = options.storage_class {
1242            request = request.add_header("x-oss-storage-class", sc);
1243        }
1244
1245        if let Some(td) = options.tag_directive {
1246            request = request.add_header("x-oss-tag-directive", td);
1247        }
1248
1249        if !options.tags.is_empty() {
1250            request = request.add_header("x-oss-tagging", build_tag_string(&options.tags));
1251        }
1252
1253        for (key, value) in options.metadata.iter() {
1254            request = request.add_header(key, value);
1255        }
1256    }
1257
1258    Ok(request)
1259}
1260
1261pub(crate) fn build_get_object_request(bucket_name: &str, object_key: &str, options: &Option<GetObjectOptions>) -> Result<OssRequest> {
1262    if !validate_bucket_name(bucket_name) {
1263        return Err(Error::Other(format!("invalid bucket name: {}", bucket_name)));
1264    }
1265
1266    if !validate_object_key(object_key) {
1267        return Err(Error::Other(format!("invalid object key: {}", object_key)));
1268    }
1269
1270    let mut request = OssRequest::new().method(RequestMethod::Get).bucket(bucket_name).object(object_key);
1271
1272    if let Some(options) = options {
1273        if let Some(s) = &options.range {
1274            request = request.add_header("range", s);
1275        }
1276
1277        if let Some(s) = &options.if_modified_since {
1278            request = request.add_header("if-modified-since", s);
1279        }
1280
1281        if let Some(s) = &options.if_unmodified_since {
1282            request = request.add_header("if-unmodified-since", s);
1283        }
1284
1285        if let Some(s) = &options.if_match {
1286            request = request.add_header("if-match", s);
1287        }
1288
1289        if let Some(s) = &options.if_none_match {
1290            request = request.add_header("if-none-match", s);
1291        }
1292
1293        if let Some(s) = &options.accept_encoding {
1294            request = request.add_header("accept-encoding", s);
1295        }
1296
1297        if let Some(s) = &options.response_content_language {
1298            request = request.add_query("response-content-language", s);
1299        }
1300
1301        if let Some(s) = &options.response_expires {
1302            request = request.add_query("response-expires", s);
1303        }
1304
1305        if let Some(s) = &options.response_cache_control {
1306            request = request.add_query("response-cache-control", s);
1307        }
1308
1309        if let Some(s) = &options.response_content_disposition {
1310            request = request.add_query("response-content-disposition", s);
1311        }
1312
1313        if let Some(ce) = options.response_content_encoding {
1314            request = request.add_query("response-content-encoding", ce.as_str());
1315        }
1316
1317        if let Some(s) = &options.version_id {
1318            request = request.add_query("versionId", s);
1319        }
1320    }
1321
1322    Ok(request)
1323}
1324
1325pub(crate) fn build_head_object_request(bucket_name: &str, object_key: &str, options: &Option<HeadObjectOptions>) -> Result<OssRequest> {
1326    if !validate_bucket_name(bucket_name) {
1327        return Err(Error::Other(format!("invalid bucket name: {}", bucket_name)));
1328    }
1329
1330    if !validate_object_key(object_key) {
1331        return Err(Error::Other(format!("invalid object key: {}", object_key)));
1332    }
1333
1334    let mut request = OssRequest::new().method(RequestMethod::Head).bucket(bucket_name).object(object_key);
1335
1336    if let Some(options) = options {
1337        if let Some(s) = &options.if_modified_since {
1338            request = request.add_header("if-modified-since", s);
1339        }
1340
1341        if let Some(s) = &options.if_unmodified_since {
1342            request = request.add_header("if-unmodified-since", s);
1343        }
1344
1345        if let Some(s) = &options.if_match {
1346            request = request.add_header("if-match", s);
1347        }
1348
1349        if let Some(s) = &options.if_none_match {
1350            request = request.add_header("if-none-match", s);
1351        }
1352
1353        if let Some(s) = &options.version_id {
1354            request = request.add_query("versionId", s);
1355        }
1356    }
1357
1358    Ok(request)
1359}
1360
1361/// Put object result enumeration
1362#[derive(Debug, Clone)]
1363#[cfg_attr(feature = "serde-support", derive(serde::Serialize, serde::Deserialize))]
1364#[cfg_attr(feature = "serde-camelcase", serde(rename_all = "camelCase"))]
1365pub enum PutObjectResult {
1366    /// This is response headers from aliyun oss api when you put object with no callback specified
1367    #[cfg_attr(feature = "serde-camelcase", serde(rename = "apiResponse", rename_all = "camelCase"))]
1368    ApiResponse(PutObjectApiResponse),
1369
1370    /// This is your callback response content string when you put object with callback specified.
1371    /// `.0` should be a valid JSON string.
1372    #[cfg_attr(feature = "serde-camelcase", serde(rename = "callbackResponse"))]
1373    CallbackResponse(String),
1374}
1375
1376/// The response headers from aliyun oss put object api
1377#[derive(Debug, Clone)]
1378#[cfg_attr(feature = "serde-support", derive(serde::Serialize, serde::Deserialize))]
1379#[cfg_attr(feature = "serde-camelcase", serde(rename_all = "camelCase"))]
1380pub struct PutObjectApiResponse {
1381    pub request_id: String,
1382
1383    /// 已经移除了首尾双引号(`"`)之后的字符串
1384    pub etag: String,
1385
1386    /// 文件 MD5 值,Base64 编码的字符串
1387    pub content_md5: String,
1388
1389    /// 文件 CRC64 值
1390    pub hash_crc64ecma: u64,
1391
1392    /// 表示文件的版本 ID。仅当您将文件上传至已开启版本控制状态的 Bucket 时,会返回该响应头。
1393    pub version_id: Option<String>,
1394}
1395
1396/// If you put object without callback, parse aliyun oss api headers into `PutObjectResult::WithoutCallback` enum variant
1397impl From<HashMap<String, String>> for PutObjectApiResponse {
1398    fn from(mut headers: HashMap<String, String>) -> Self {
1399        Self {
1400            request_id: headers.remove("x-oss-request-id").unwrap_or_default(),
1401            etag: sanitize_etag(headers.remove("etag").unwrap_or_default()),
1402            content_md5: headers.remove("content-md5").unwrap_or_default(),
1403            hash_crc64ecma: headers.remove("x-oss-hash-crc64ecma").unwrap_or("0".to_string()).parse().unwrap_or(0),
1404            version_id: headers.remove("x-oss-version-id"),
1405        }
1406    }
1407}
1408
1409/// Options for appending object
1410pub type AppendObjectOptions = PutObjectOptions;
1411pub type AppendObjectOptionsBuilder = PutObjectOptionsBuilder;
1412
1413pub struct AppendObjectResult {
1414    pub request_id: String,
1415    pub next_append_position: u64,
1416}
1417
1418impl From<HashMap<String, String>> for AppendObjectResult {
1419    fn from(mut headers: HashMap<String, String>) -> Self {
1420        Self {
1421            request_id: headers.remove("x-oss-request-id").unwrap_or_default(),
1422            next_append_position: headers.remove("x-oss-next-append-position").unwrap_or("0".to_string()).parse().unwrap_or(0),
1423        }
1424    }
1425}
1426
1427#[derive(Debug, Clone, Default)]
1428#[cfg_attr(feature = "serde-support", derive(serde::Serialize, serde::Deserialize))]
1429#[cfg_attr(feature = "serde-camelcase", serde(rename_all = "camelCase"))]
1430pub struct DeleteMultipleObjectsItem {
1431    pub key: String,
1432    pub version_id: Option<String>,
1433}
1434
1435/// Payload while call delete multiple objects in one request
1436///
1437/// Official document: <https://help.aliyun.com/zh/oss/developer-reference/deletemultipleobjects>
1438#[derive(Debug, Clone, Default)]
1439#[cfg_attr(feature = "serde-support", derive(serde::Serialize, serde::Deserialize))]
1440#[cfg_attr(feature = "serde-camelcase", serde(rename_all = "camelCase"))]
1441pub struct DeleteMultipleObjectsRequest {
1442    pub quiet: Option<bool>,
1443
1444    /// Object keys to delete
1445    pub objects: Vec<DeleteMultipleObjectsItem>,
1446}
1447
1448impl DeleteMultipleObjectsRequest {
1449    /// Consumes data and generate xml content
1450    pub(crate) fn into_xml(self) -> Result<String> {
1451        let mut writer = quick_xml::Writer::new(Vec::new());
1452        writer.write_event(Event::Decl(BytesDecl::new("1.0", Some("UTF-8"), None)))?;
1453
1454        writer.write_event(Event::Start(BytesStart::new("Delete")))?;
1455
1456        if let Some(b) = self.quiet {
1457            writer.write_event(Event::Start(BytesStart::new("Quiet")))?;
1458            writer.write_event(Event::Text(BytesText::new(&b.to_string())))?;
1459            writer.write_event(Event::End(BytesEnd::new("Quiet")))?;
1460        }
1461
1462        for item in self.objects {
1463            writer.write_event(Event::Start(BytesStart::new("Object")))?;
1464
1465            writer.write_event(Event::Start(BytesStart::new("Key")))?;
1466            writer.write_event(Event::Text(BytesText::new(&item.key)))?;
1467            writer.write_event(Event::End(BytesEnd::new("Key")))?;
1468
1469            if let Some(s) = item.version_id {
1470                writer.write_event(Event::Start(BytesStart::new("VersionId")))?;
1471                writer.write_event(Event::Text(BytesText::new(&s)))?;
1472                writer.write_event(Event::End(BytesEnd::new("VersionId")))?;
1473            }
1474
1475            writer.write_event(Event::End(BytesEnd::new("Object")))?;
1476        }
1477
1478        writer.write_event(Event::End(BytesEnd::new("Delete")))?;
1479
1480        Ok(String::from_utf8(writer.into_inner())?)
1481    }
1482}
1483
1484impl<T> From<&[T]> for DeleteMultipleObjectsRequest
1485where
1486    T: AsRef<str>,
1487{
1488    fn from(object_keys: &[T]) -> Self {
1489        Self {
1490            objects: object_keys
1491                .iter()
1492                .map(|s| DeleteMultipleObjectsItem {
1493                    key: s.as_ref().to_string(),
1494                    ..Default::default()
1495                })
1496                .collect::<Vec<_>>(),
1497            ..Default::default()
1498        }
1499    }
1500}
1501
1502#[derive(Debug)]
1503pub enum DeleteMultipleObjectsConfig<'a, T: AsRef<str> + 'a> {
1504    /// A simpler mode: only keys to delete are specified
1505    FromKeys(&'a [T]),
1506
1507    /// User custom full request
1508    FullRequest(DeleteMultipleObjectsRequest),
1509}
1510
1511#[derive(Debug, Clone, Default)]
1512#[cfg_attr(feature = "serde-support", derive(serde::Serialize, serde::Deserialize))]
1513#[cfg_attr(feature = "serde-camelcase", serde(rename_all = "camelCase"))]
1514pub struct DeleteMultipleObjectsResultItem {
1515    pub key: String,
1516    pub version_id: Option<String>,
1517    pub delete_marker: Option<String>,
1518    pub delete_marker_version_id: Option<String>,
1519}
1520
1521/// Result of deleting multiple objects ()
1522///
1523/// Official document: <https://help.aliyun.com/zh/oss/developer-reference/deletemultipleobjects>
1524#[derive(Debug, Clone, Default)]
1525#[cfg_attr(feature = "serde-support", derive(serde::Serialize, serde::Deserialize))]
1526#[cfg_attr(feature = "serde-camelcase", serde(rename_all = "camelCase"))]
1527pub struct DeleteMultipleObjectsResult {
1528    pub items: Vec<DeleteMultipleObjectsResultItem>,
1529}
1530
1531impl DeleteMultipleObjectsResult {
1532    pub fn from_xml(xml_content: &str) -> Result<Self> {
1533        let mut reader = quick_xml::Reader::from_str(xml_content);
1534        let mut tag = String::new();
1535        let mut items = Vec::new();
1536
1537        let mut current_item = DeleteMultipleObjectsResultItem::default();
1538
1539        loop {
1540            match reader.read_event()? {
1541                Event::Eof => break,
1542                Event::Start(t) => {
1543                    tag = String::from_utf8_lossy(t.local_name().as_ref()).to_string();
1544                    if tag == "Deleted" {
1545                        current_item = DeleteMultipleObjectsResultItem::default();
1546                    }
1547                }
1548
1549                Event::Text(e) => {
1550                    let s = e.unescape()?.trim().to_string();
1551                    match tag.as_str() {
1552                        "Key" => current_item.key = s,
1553                        "VersionId" => current_item.version_id = Some(s),
1554                        "DeleteMarker" => current_item.delete_marker = Some(s),
1555                        "DeleteMarkerVersionId" => current_item.delete_marker_version_id = Some(s),
1556                        _ => {}
1557                    }
1558                }
1559
1560                Event::End(t) => {
1561                    if t.local_name().as_ref() == b"Deleted" {
1562                        items.push(current_item.clone());
1563                    }
1564                    tag.clear();
1565                }
1566
1567                _ => {}
1568            }
1569        }
1570
1571        Ok(Self { items })
1572    }
1573}
1574
1575pub(crate) fn build_delete_multiple_objects_request<S>(bucket_name: &str, config: DeleteMultipleObjectsConfig<S>) -> Result<OssRequest>
1576where
1577    S: AsRef<str>,
1578{
1579    if !validate_bucket_name(bucket_name) {
1580        return Err(Error::Other(format!("invalid bucket name: {}", bucket_name)));
1581    }
1582
1583    let mut request = OssRequest::new()
1584        .method(RequestMethod::Post)
1585        .bucket(bucket_name)
1586        .add_query("delete", "")
1587        .content_type(MIME_TYPE_XML);
1588
1589    let items_len = match &config {
1590        DeleteMultipleObjectsConfig::FromKeys(items) => items.len(),
1591        DeleteMultipleObjectsConfig::FullRequest(cfg) => cfg.objects.len(),
1592    };
1593
1594    if items_len > common::DELETE_MULTIPLE_OBJECTS_LIMIT {
1595        return Err(Error::Other(format!(
1596            "{} exceeds the items count limits while deleting multiple objects",
1597            items_len
1598        )));
1599    }
1600
1601    let payload = match config {
1602        DeleteMultipleObjectsConfig::FromKeys(items) => DeleteMultipleObjectsRequest::from(items),
1603        DeleteMultipleObjectsConfig::FullRequest(delete_multiple_objects_request) => delete_multiple_objects_request,
1604    };
1605
1606    let xml_content = payload.into_xml()?;
1607    let content_md5 = BASE64_STANDARD.encode(*md5::compute(xml_content.as_bytes()));
1608
1609    request = request
1610        .content_length(xml_content.len() as u64)
1611        .add_header("content-md5", content_md5)
1612        .text_body(xml_content);
1613
1614    Ok(request)
1615}
1616
1617/// 发起回调请求的 `Content-Type`
1618#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1619pub enum CallbackBodyType {
1620    #[default]
1621    #[serde(rename = "application/x-www-form-urlencoded")]
1622    FormUrlEncoded,
1623
1624    #[serde(rename = "application/json")]
1625    Json,
1626}
1627
1628/// The callback while call
1629///
1630/// - `put_object_from_file`
1631/// - `put_object_from_buffer`
1632/// - `put_object_from_base64`
1633/// - `complete_multipart_upload`
1634///
1635/// to create an object, if create object successfully,
1636/// the OSS server will call your server according to this callback config with `POST` request method.
1637///
1638/// Official document: <https://help.aliyun.com/zh/oss/developer-reference/callback>
1639#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1640pub struct Callback {
1641    /// 创建 Object 成功之后,OSS 服务器会 `POST` 到这个 URL。
1642    /// 该 URL 调用后的要求有:
1643    ///
1644    /// - 响应 `HTTP/1.1 200 OK`
1645    /// - 响应头 `Content-Length` 必须是合法的值
1646    /// - 响应体为 JSON 格式
1647    /// - 响应体最大为 3MB
1648    ///
1649    /// 另外:
1650    ///
1651    /// - 支持同时配置最多 5 个 URL,多个 URL 间以分号(;)分隔。OSS 会依次发送请求,直到第一个回调请求成功返回。
1652    /// - 支持 HTTPS 协议地址。
1653    /// - **不支持**填写 IPV6 地址,也**不支持**填写指向 IPV6 地址的域名。
1654    /// - 为了保证正确处理中文等情况,此 URL 需做编码处理,
1655    ///   例如 `https://example.com/中文.php?key=value&中文名称=中文值`
1656    ///   需要编码为 `https://example.com/%E4%B8%AD%E6%96%87.php?key=value&%E4%B8%AD%E6%96%87%E5%90%8D%E7%A7%B0=%E4%B8%AD%E6%96%87%E5%80%BC`。
1657    #[serde(rename = "callbackUrl")]
1658    pub url: String,
1659
1660    /// 发起回调请求时 `Host` 头的值,格式为域名或 IP 地址。仅在设置了 `url` 时有效。
1661    /// 如果没有配置 `host`,则解析 `url` 中的 URL,并将解析的 `Host` 填充到 `Host` 请求头中。
1662    #[serde(rename = "callbackHost", skip_serializing_if = "Option::is_none")]
1663    pub host: Option<String>,
1664
1665    /// 发起回调时请求Body的值,例如 `key=${object}&etag=${etag}&my_var=${x:my_var}`。
1666    ///
1667    /// 支持:
1668    ///
1669    /// - OSS系统参数,要写成 `${var_name}` 格式
1670    ///   - `bucket`: The bucket name
1671    ///   - `object`: The object key
1672    ///   - `etag`: The ETag of the object
1673    ///   - `size`: 以字节为单位的 Object 大小。调用 `complete_multipart_upload` 时,`size` 为整个 Object 的大小
1674    ///   - `mimeType`: 资源类型,例如 jpeg 图片的资源类型为 `image/jpeg`
1675    ///   - `imageInfo.height`: 图片高度。该变量仅适用于图片格式,对于非图片格式,该变量的值为空
1676    ///   - `imageInfo.width`: 图片宽度。该变量仅适用于图片格式,对于非图片格式,该变量的值为空
1677    ///   - `imageInfo.format`: 图片格式,例如 JPG、PNG 等。该变量仅适用于图片格式,对于非图片格式,该变量的值为空
1678    ///   - `crc64`: 与上传文件后返回的 `x-oss-hash-crc64ecma` 头内容一致
1679    ///   - `contentMd5`: 与上传文件后返回的 `Content-MD5` 头内容一致。仅在 `put_object_from_xxx` 时候该变量的值不为空
1680    ///   - `vpcId`: 发起请求的客户端所在的 VpcId。如果不是通过 VPC 发起请求,则该变量的值为空
1681    ///   - `clientIp`: 发起请求的客户端 IP 地址
1682    ///   - `reqId`: 发起请求的 RequestId
1683    ///   - 发起请求的接口名称,例如 `PutObject`、 `PostObject` 等
1684    /// - 自定义参数,其使用格式为 `${x:var_name}`。参数值放到 `custom_variables` 中
1685    /// - 常量(字面量)
1686    #[serde(rename = "callbackBody")]
1687    pub body: String,
1688
1689    /// 客户端发起回调请求时,OSS是否向地址发送服务器名称指示 SNI(Server Name Indication)。
1690    /// 是否发送 SNI 取决于服务器的配置和需求。
1691    /// 对于使用同一个 IP 地址来托管多个 TLS/SSL 证书的服务器的情况,建议选择发送 SNI
1692    #[serde(rename = "callbackSNI", skip_serializing_if = "Option::is_none")]
1693    pub sni: Option<bool>,
1694
1695    /// 发起回调请求时候的 `Content-Type`。默认是:`application/x-www-form-urlencoded`
1696    #[serde(rename = "callbackBodyType", skip_serializing_if = "Option::is_none")]
1697    pub body_type: Option<CallbackBodyType>,
1698
1699    /// 在回调请求体 (`body`) 中携带的数据,如果有自定义参数,请使用此属性容纳自定义参数的值。
1700    /// Key 为自定义变量名,但是不包含 `x:` 前缀。在生成 `body` 的时候会自动增加
1701    /// 注意:这里我使用了 Map 来接受自定义参数,也就是说,这里**不支持**多个同名的自定义参数来表示集合数据类型
1702    #[serde(skip_serializing)]
1703    pub custom_variables: HashMap<String, String>,
1704}
1705
1706/// 回调请求数据枚举值
1707///
1708/// `Oss` 开头的,其中 `.0` 是此参数值对应的参数名。
1709/// 因为大部分时候,传入的参数名是固定的,自定参数的 body 参数名和自定义参数的参数值、常量参数值,一般都是静态字符串,
1710/// 所以这里用了 `&'a str` 的形态。如果 `&'a str` 不满足你的需求,请使用 `CallbackBodyParameter::Literal(String, String)`
1711///
1712/// # Example
1713///
1714/// ```rust
1715/// use ali_oss_rs::object_common::CallbackBodyParameter;
1716///
1717///
1718/// assert_eq!("foo=${bucket}", CallbackBodyParameter::OssBucket("foo").to_body_string());
1719/// assert_eq!("foo=${x:bar}", CallbackBodyParameter::Custom(
1720///     "foo",
1721///     "bar",
1722///     "Are you OK?".to_string()
1723/// ).to_body_string());
1724/// assert_eq!("foo=bar", CallbackBodyParameter::Constant("foo", "bar").to_body_string());
1725/// assert_eq!(
1726///     "foo=${x:bar}",
1727///     CallbackBodyParameter::Literal(
1728///         "foo".to_string(),
1729///         "${x:bar}".to_string()
1730///     ).to_body_string()
1731/// );
1732///
1733/// ```
1734pub enum CallbackBodyParameter<'a> {
1735    OssBucket(&'a str),
1736    OssObject(&'a str),
1737    OssETag(&'a str),
1738    OssSize(&'a str),
1739    OssMimeType(&'a str),
1740    OssImageHeight(&'a str),
1741    OssImageWidth(&'a str),
1742    OssImageFormat(&'a str),
1743    OssCrc64(&'a str),
1744    OssContentMd5(&'a str),
1745    OssVpcId(&'a str),
1746    OssClientIp(&'a str),
1747    OssRequestId(&'a str),
1748    OssOperation(&'a str),
1749
1750    /// - `.0` 是在回调中的参数名,
1751    /// - `.1` 是在回调中参数值对应的自定义变量的名字,也就是 `${x:custom_var_name}` 中的 `custom_var_name`。
1752    ///   例如:`CallbackBodyParameter::Custom("foo", "bar", "Are you OK?".to_string())`,
1753    ///   在请求体中就是 `foo=${x:bar}`
1754    /// - `.2` 是在自定义参数表中的自定参数对应的值。例如,上面的例子中,会在 `custom_variables` 中加入一个 `("x:bar", "Are you OK?")` 的条目
1755    ///
1756    /// 这么设计是为了防止在 body 中增加了自定义参数,但是忘记了传递自定义参数的值
1757    Custom(&'a str, &'a str, String),
1758
1759    /// `.0` 是在回调中的参数名,`.1` 是在回调中的参数值,不做解析,在回调的时候原封不动插入给定的字符。
1760    ///
1761    /// 例如:`CallbackBodyParameter::Constant("hello", "world")`,
1762    /// 在请求体中就是 `hello=world`
1763    Constant(&'a str, &'a str),
1764
1765    /// 这是一个兜底的,如果上面 `&'str` 不满足需求,就使用这个。
1766    /// 因为它可以获取所有权。在生成回调请求体的时候,会按照字符串直接拼接。
1767    ///
1768    /// 例如:`CallbackBodyParameter::Literal("foo".to_string(), "${x:bar}")`,
1769    /// 在请求体中就是 `foo=${x:bar}`
1770    Literal(String, String),
1771}
1772
1773impl CallbackBodyParameter<'_> {
1774    /// 转换成 callback body 中的格式
1775    pub fn to_body_string(&self) -> String {
1776        match self {
1777            CallbackBodyParameter::OssBucket(k) => format!("{}=${{bucket}}", k),
1778            CallbackBodyParameter::OssObject(k) => format!("{}=${{object}}", k),
1779            CallbackBodyParameter::OssETag(k) => format!("{}=${{etag}}", k),
1780            CallbackBodyParameter::OssSize(k) => format!("{}=${{size}}", k),
1781            CallbackBodyParameter::OssMimeType(k) => format!("{}=${{mimeType}}", k),
1782            CallbackBodyParameter::OssImageHeight(k) => format!("{}=${{imageInfo.height}}", k),
1783            CallbackBodyParameter::OssImageWidth(k) => format!("{}=${{imageInfo.width}}", k),
1784            CallbackBodyParameter::OssImageFormat(k) => format!("{}=${{imageInfo.format}}", k),
1785            CallbackBodyParameter::OssCrc64(k) => format!("{}=${{crc64}}", k),
1786            CallbackBodyParameter::OssContentMd5(k) => format!("{}=${{contentMd5}}", k),
1787            CallbackBodyParameter::OssVpcId(k) => format!("{}=${{vpcId}}", k),
1788            CallbackBodyParameter::OssClientIp(k) => format!("{}=${{clientIp}}", k),
1789            CallbackBodyParameter::OssRequestId(k) => format!("{}=${{reqId}}", k),
1790            CallbackBodyParameter::OssOperation(k) => format!("{}=${{operation}}", k),
1791            CallbackBodyParameter::Custom(k, v, _) => format!("{}=${{x:{}}}", k, v),
1792            CallbackBodyParameter::Constant(k, v) => format!("{}={}", k, v),
1793            CallbackBodyParameter::Literal(k, v) => format!("{}={}", k, v),
1794        }
1795    }
1796}
1797
1798/// Callback builder, hope it's helpful when you building your callback
1799pub struct CallbackBuilder<'a> {
1800    url: String,
1801    host: Option<String>,
1802    sni: Option<bool>,
1803    body_type: Option<CallbackBodyType>,
1804    body_parameters: Vec<CallbackBodyParameter<'a>>,
1805    custom_variables: HashMap<String, String>,
1806}
1807
1808impl<'a> CallbackBuilder<'a> {
1809    pub fn new(url: impl Into<String>) -> Self {
1810        Self {
1811            url: url.into(),
1812            host: None,
1813            sni: None,
1814            body_type: None,
1815            body_parameters: vec![],
1816            custom_variables: HashMap::new(),
1817        }
1818    }
1819
1820    pub fn host(mut self, host: impl Into<String>) -> Self {
1821        self.host = Some(host.into());
1822        self
1823    }
1824
1825    pub fn sni(mut self, sni: bool) -> Self {
1826        self.sni = Some(sni);
1827        self
1828    }
1829
1830    pub fn body_type(mut self, body_type: CallbackBodyType) -> Self {
1831        self.body_type = Some(body_type);
1832        self
1833    }
1834
1835    pub fn body_parameter(mut self, param: CallbackBodyParameter<'a>) -> Self {
1836        if let CallbackBodyParameter::Custom(_, custom_var_name, custom_var_value) = &param {
1837            self.custom_variables.insert(
1838                format!("x:{}", custom_var_name.strip_prefix("x:").unwrap_or(custom_var_name)),
1839                custom_var_value.clone(),
1840            );
1841        }
1842
1843        self.body_parameters.push(param);
1844        self
1845    }
1846
1847    /// 一般情况下,不需要直接调用这个函数。 `body_parameter` 已经处理了自定义参数的情况了。
1848    /// 除非你使用 `CallbackBodyParameter::Literal`,并且涉及到自定义参数的,就需要使用这个函数把自定义参数值加入进来。
1849    /// `k` 无需携带 `x:` 前缀,在插入的时候自动追加前缀
1850    pub fn custom_variable(mut self, k: impl Into<String>, v: impl Into<String>) -> Self {
1851        let key: String = k.into();
1852        self.custom_variables
1853            .insert(format!("x:{}", key.strip_prefix("x:").unwrap_or(key.as_str())), v.into());
1854        self
1855    }
1856
1857    pub fn build(self) -> Callback {
1858        let body_string = self.body_parameters.into_iter().map(|bp| bp.to_body_string()).collect::<Vec<_>>().join("&");
1859
1860        Callback {
1861            url: self.url,
1862            host: self.host,
1863            body: body_string,
1864            sni: self.sni,
1865            body_type: self.body_type,
1866            custom_variables: self.custom_variables,
1867        }
1868    }
1869}
1870
1871/// Job parameters tier for restoring object
1872///
1873/// 冷归档、深度冷归档类型 Object 解冻优先级。取值范围如下:
1874///
1875/// - 冷归档类型 Object
1876///   - 高优先级(Expedited):表示 1 小时内完成解冻。
1877///   - 标准(Standard,默认值):表示 2~5 小时内完成解冻。
1878///   - 批量(Bulk):表示 5~12 小时内完成解冻。
1879/// - 深度冷归档类型 Object
1880///   - 高优先级(Expedited):表示 12 小时内完成解冻。
1881///   - 标准(Standard,默认值):表示48小时内完成解冻。
1882#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
1883#[cfg_attr(feature = "serde-support", derive(serde::Serialize, serde::Deserialize))]
1884pub enum RestoreJobTier {
1885    #[cfg_attr(feature = "serde-support", serde(rename = "Standard"))]
1886    #[default]
1887    Standard,
1888
1889    #[cfg_attr(feature = "serde-support", serde(rename = "Expedited"))]
1890    Expedited,
1891
1892    #[cfg_attr(feature = "serde-support", serde(rename = "Bulk"))]
1893    Bulk,
1894}
1895
1896impl RestoreJobTier {
1897    pub fn as_str(&self) -> &str {
1898        match self {
1899            RestoreJobTier::Standard => "Standard",
1900            RestoreJobTier::Expedited => "Expedited",
1901            RestoreJobTier::Bulk => "Bulk",
1902        }
1903    }
1904}
1905
1906impl AsRef<str> for RestoreJobTier {
1907    fn as_ref(&self) -> &str {
1908        self.as_str()
1909    }
1910}
1911
1912impl TryFrom<&str> for RestoreJobTier {
1913    type Error = Error;
1914
1915    fn try_from(s: &str) -> std::result::Result<Self, Self::Error> {
1916        match s {
1917            "Standard" => Ok(Self::Standard),
1918            "Expedited" => Ok(Self::Expedited),
1919            "Bulk" => Ok(Self::Bulk),
1920            _ => Err(Error::Other(format!("invalid job tier: {}", s))),
1921        }
1922    }
1923}
1924
1925impl TryFrom<&String> for RestoreJobTier {
1926    type Error = Error;
1927
1928    fn try_from(s: &String) -> std::result::Result<Self, Self::Error> {
1929        Self::try_from(s.as_str())
1930    }
1931}
1932
1933impl TryFrom<String> for RestoreJobTier {
1934    type Error = Error;
1935
1936    fn try_from(s: String) -> std::result::Result<Self, Self::Error> {
1937        Self::try_from(s.as_str())
1938    }
1939}
1940
1941/// Restore object request
1942#[derive(Debug, Clone, Default)]
1943#[cfg_attr(feature = "serde-support", derive(serde::Serialize, serde::Deserialize))]
1944#[cfg_attr(feature = "serde-camelcase", serde(rename_all = "camelCase"))]
1945pub struct RestoreObjectRequest {
1946    /// 设置归档、冷归档以及深度冷归档类型 Object 的解冻天数。
1947    ///
1948    /// - 归档类型 Object 解冻天数的取值范围为 1~7,单位为天
1949    /// - 冷归档以及深度冷归档类型 Object 解冻天数的取值范围为 1~365,单位为天。
1950    pub days: u16,
1951
1952    /// 对于开启版本控制的 Bucket,Object 的各个版本可以对应不同的存储类型。
1953    /// 默认解冻 Object 当前版本,您可以通过指定 `versionId` 的方式来解冻 Object 指定版本。
1954    pub version_id: Option<String>,
1955
1956    /// 冷归档、深度冷归档类型Object解冻优先级. 参见 [`RestoreJobTier`]
1957    pub tier: Option<RestoreJobTier>,
1958}
1959
1960impl RestoreObjectRequest {
1961    /// Consume value and build XML content for requesting
1962    pub(crate) fn into_xml(self) -> Result<String> {
1963        let mut writer = quick_xml::Writer::new(Vec::new());
1964
1965        writer.write_event(Event::Decl(BytesDecl::new("1.0", Some("UTF-8"), None)))?;
1966
1967        writer.write_event(Event::Start(BytesStart::new("RestoreRequest")))?;
1968
1969        writer.write_event(Event::Start(BytesStart::new("Days")))?;
1970        writer.write_event(Event::Text(BytesText::new(self.days.to_string().as_str())))?;
1971        writer.write_event(Event::End(BytesEnd::new("Days")))?;
1972
1973        if let Some(t) = self.tier {
1974            writer.write_event(Event::Start(BytesStart::new("JobParameters")))?;
1975            writer.write_event(Event::Start(BytesStart::new("Tier")))?;
1976            writer.write_event(Event::Text(BytesText::new(t.as_str())))?;
1977            writer.write_event(Event::End(BytesEnd::new("Tier")))?;
1978            writer.write_event(Event::End(BytesEnd::new("JobParameters")))?;
1979        }
1980
1981        writer.write_event(Event::End(BytesEnd::new("RestoreRequest")))?;
1982
1983        Ok(String::from_utf8(writer.into_inner())?)
1984    }
1985}
1986
1987/// Restore object result
1988#[derive(Debug, Clone)]
1989#[cfg_attr(feature = "serde-support", derive(serde::Serialize, serde::Deserialize))]
1990#[cfg_attr(feature = "serde-camelcase", serde(rename_all = "camelCase"))]
1991pub struct RestoreObjectResult {
1992    pub request_id: String,
1993    pub object_restore_priority: Option<String>,
1994    pub version_id: Option<String>,
1995}
1996
1997impl From<HashMap<String, String>> for RestoreObjectResult {
1998    fn from(mut headers: HashMap<String, String>) -> Self {
1999        Self {
2000            request_id: headers.remove("x-oss-request-id").unwrap_or_default(),
2001            object_restore_priority: headers.remove("x-oss-object-restore-priority"),
2002            version_id: headers.remove("x-oss-version-id"),
2003        }
2004    }
2005}
2006
2007pub(crate) fn build_restore_object_request(bucket_name: &str, object_key: &str, config: RestoreObjectRequest) -> Result<OssRequest> {
2008    if !validate_bucket_name(bucket_name) {
2009        return Err(Error::Other(format!("invalid bucket name: {}", bucket_name)));
2010    }
2011
2012    if !validate_object_key(object_key) {
2013        return Err(Error::Other(format!("invalid object key: {}", object_key)));
2014    }
2015
2016    let mut request = OssRequest::new()
2017        .method(RequestMethod::Post)
2018        .bucket(bucket_name)
2019        .object(object_key)
2020        .add_query("restore", "");
2021
2022    if let Some(s) = &config.version_id {
2023        request = request.add_query("versionId", s);
2024    }
2025
2026    let xml = config.into_xml()?;
2027    request = request.content_type("application/xml").content_length(xml.len() as u64).text_body(xml);
2028
2029    Ok(request)
2030}
2031
2032#[cfg(test)]
2033mod test_object_common {
2034    use crate::object_common::CallbackBodyParameter;
2035
2036    #[cfg(feature = "serde-support")]
2037    use super::PutObjectResult;
2038
2039    #[test]
2040    fn test_callback_body_parameter() {
2041        assert_eq!("foo=${bucket}", CallbackBodyParameter::OssBucket("foo").to_body_string());
2042        assert_eq!(
2043            "foo=${x:bar}",
2044            CallbackBodyParameter::Custom("foo", "bar", "Are you OK?".to_string()).to_body_string()
2045        );
2046
2047        assert_eq!("foo=bar", CallbackBodyParameter::Constant("foo", "bar").to_body_string());
2048        assert_eq!(
2049            "foo=${x:bar}",
2050            CallbackBodyParameter::Literal("foo".to_string(), "${x:bar}".to_string()).to_body_string()
2051        );
2052    }
2053
2054    #[test]
2055    #[cfg(feature = "serde-support")]
2056    fn test_put_object_result_serde() {
2057        use crate::object_common::PutObjectApiResponse;
2058
2059        let ret = PutObjectResult::ApiResponse(PutObjectApiResponse {
2060            request_id: "abc".to_string(),
2061            etag: "1232".to_string(),
2062            content_md5: "abcsdf".to_string(),
2063            hash_crc64ecma: 1232344,
2064            version_id: None,
2065        });
2066
2067        let s = serde_json::to_string(&ret).unwrap();
2068        println!("{}", s);
2069    }
2070}