s3s/dto/
copy_source.rs

1//! x-amz-copy-source
2
3use crate::http;
4use crate::path;
5
6use std::fmt::Write;
7
8/// x-amz-copy-source
9#[derive(Debug, Clone, PartialEq)]
10pub enum CopySource {
11    /// bucket repr
12    Bucket {
13        /// bucket
14        bucket: Box<str>,
15        /// key
16        key: Box<str>,
17        /// version id
18        version_id: Option<Box<str>>,
19    },
20    /// access point repr
21    AccessPoint {
22        /// region
23        region: Box<str>,
24        /// account id
25        account_id: Box<str>,
26        /// access point name
27        access_point_name: Box<str>,
28        /// key
29        key: Box<str>,
30    },
31}
32
33/// [`CopySource`]
34#[derive(Debug, thiserror::Error)]
35pub enum ParseCopySourceError {
36    /// pattern mismatch
37    #[error("ParseAmzCopySourceError: PatternMismatch")]
38    PatternMismatch,
39
40    /// invalid bucket name
41    #[error("ParseAmzCopySourceError: InvalidBucketName")]
42    InvalidBucketName,
43
44    /// invalid key
45    #[error("ParseAmzCopySourceError: InvalidKey")]
46    InvalidKey,
47
48    #[error("ParseAmzCopySourceError: InvalidEncoding")]
49    InvalidEncoding,
50}
51
52impl CopySource {
53    /// Parses [`CopySource`] from header
54    /// # Errors
55    /// Returns an error if the header is invalid
56    pub fn parse(header: &str) -> Result<Self, ParseCopySourceError> {
57        let header = urlencoding::decode(header).map_err(|_| ParseCopySourceError::InvalidEncoding)?;
58        let header = header.strip_prefix('/').unwrap_or(&header);
59
60        // FIXME: support access point
61        match header.split_once('/') {
62            None => Err(ParseCopySourceError::PatternMismatch),
63            Some((bucket, remaining)) => {
64                let (key, version_id) = match remaining.split_once('?') {
65                    Some((key, remaining)) => {
66                        let version_id = remaining
67                            .split_once('=')
68                            .and_then(|(name, val)| (name == "versionId").then_some(val));
69                        (key, version_id)
70                    }
71                    None => (remaining, None),
72                };
73
74                if !path::check_bucket_name(bucket) {
75                    return Err(ParseCopySourceError::InvalidBucketName);
76                }
77
78                if !path::check_key(key) {
79                    return Err(ParseCopySourceError::InvalidKey);
80                }
81
82                Ok(Self::Bucket {
83                    bucket: bucket.into(),
84                    key: key.into(),
85                    version_id: version_id.map(Into::into),
86                })
87            }
88        }
89    }
90
91    #[must_use]
92    pub fn format_to_string(&self) -> String {
93        let mut buf = String::new();
94        match self {
95            CopySource::Bucket { bucket, key, version_id } => {
96                write!(&mut buf, "{bucket}/{key}").unwrap();
97                if let Some(version_id) = version_id {
98                    write!(&mut buf, "?versionId={version_id}").unwrap();
99                }
100            }
101            CopySource::AccessPoint { .. } => {
102                unimplemented!()
103            }
104        }
105        buf
106    }
107}
108
109impl http::TryFromHeaderValue for CopySource {
110    type Error = ParseCopySourceError;
111
112    fn try_from_header_value(val: &http::HeaderValue) -> Result<Self, Self::Error> {
113        let header = val.to_str().map_err(|_| ParseCopySourceError::InvalidEncoding)?;
114        Self::parse(header)
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn path_style() {
124        {
125            let header = "awsexamplebucket/reports/january.pdf";
126            let val = CopySource::parse(header).unwrap();
127            match val {
128                CopySource::Bucket { bucket, key, version_id } => {
129                    assert_eq!(&*bucket, "awsexamplebucket");
130                    assert_eq!(&*key, "reports/january.pdf");
131                    assert!(version_id.is_none());
132                }
133                CopySource::AccessPoint { .. } => panic!(),
134            }
135        }
136
137        {
138            let header = "awsexamplebucket/reports/january.pdf?versionId=QUpfdndhfd8438MNFDN93jdnJFkdmqnh893";
139            let val = CopySource::parse(header).unwrap();
140            match val {
141                CopySource::Bucket { bucket, key, version_id } => {
142                    assert_eq!(&*bucket, "awsexamplebucket");
143                    assert_eq!(&*key, "reports/january.pdf");
144                    assert_eq!(version_id.as_deref().unwrap(), "QUpfdndhfd8438MNFDN93jdnJFkdmqnh893");
145                }
146                CopySource::AccessPoint { .. } => panic!(),
147            }
148        }
149    }
150}