1use std::{convert::TryInto, fmt::Display, str::FromStr};
2
3use base64::Engine;
4
5use crate::storage::{Error, StorageResult};
6
7#[derive(Debug, PartialEq, Eq, serde::Serialize, Clone)]
8#[serde(rename_all = "camelCase")]
9pub enum Projection {
10 Full,
11 NoAcl,
12}
13
14#[derive(Debug, PartialEq, Eq, serde::Serialize, Default, Clone)]
16#[serde(rename_all = "camelCase")]
17pub struct ObjectsListRequest {
18 pub fields: Option<String>,
20 pub delimiter: Option<String>,
21 pub end_offset: Option<String>,
22 pub include_trailing_delimiter: Option<bool>,
23 pub max_results: Option<usize>,
24 pub page_token: Option<String>,
25 pub prefix: Option<String>,
26 pub projection: Option<Projection>,
27 pub start_offset: Option<String>,
28 pub versions: Option<bool>,
29}
30
31#[derive(Debug, PartialEq, Eq, serde::Serialize, Default, Clone)]
32#[serde(rename_all = "camelCase")]
33pub struct ObjectMetadata {
34 pub metadata: Metadata,
35}
36#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize, Default, Clone)]
37#[serde(rename_all = "camelCase")]
38pub struct Metadata {
39 #[serde(
40 rename = "goog-reserved-file-mtime",
41 deserialize_with = "from_string_option"
42 )] pub modification_time: Option<i64>,
44}
45
46#[derive(Debug, serde::Deserialize, Default)]
48#[serde(rename_all = "camelCase")]
49pub struct Objects {
50 pub kind: Option<String>,
51
52 #[serde(default = "Vec::new")]
53 pub items: Vec<PartialObject>,
54
55 #[serde(default = "Vec::new")]
56 pub prefixes: Vec<String>,
57
58 pub next_page_token: Option<String>,
59}
60
61#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
62#[serde(rename_all = "camelCase")]
63pub struct Object {
64 pub bucket: String,
65 pub name: String,
66}
67
68impl Display for Object {
69 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70 write!(f, "{}", self.gs_url())
71 }
72}
73
74impl FromStr for Object {
75 type Err = Error;
76
77 fn from_str(s: &str) -> Result<Self, Self::Err> {
78 s.strip_prefix("gs://")
79 .and_then(|part| part.split_once('/'))
80 .ok_or(Error::GcsInvalidUrl {
81 url: s.to_owned(),
82 message: "gs url should be gs://bucket/object/path/name".to_owned(),
83 })
84 .and_then(|(bucket, name)| Object::new(bucket, name))
85 }
86}
87
88type GsUrl = String;
89
90const BASE_URL: &str = "storage/v1";
92const UPLOAD_BASE_URL: &str = "upload/storage/v1";
93
94fn percent_encode(input: &str) -> String {
95 percent_encoding::utf8_percent_encode(input, percent_encoding::NON_ALPHANUMERIC).to_string()
96}
97
98impl Object {
99 pub fn gs_url(&self) -> GsUrl {
100 format!("gs://{}/{}", &self.bucket, &self.name)
101 }
102
103 pub fn new(bucket: &str, name: &str) -> StorageResult<Self> {
105 if bucket.is_empty() {
106 return Err(Error::GcsInvalidObjectName);
107 }
108
109 if name.is_empty() || name.starts_with('.') {
110 return Err(Error::GcsInvalidObjectName);
111 }
112
113 Ok(Self {
114 bucket: bucket.to_owned(),
115 name: name.to_owned(),
116 })
117 }
118
119 pub fn url(&self) -> String {
120 format!(
121 "{}/b/{}/o/{}",
122 BASE_URL,
123 percent_encode(&self.bucket),
124 percent_encode(&self.name)
125 )
126 }
127
128 pub fn upload_url(&self, upload_type: &str) -> String {
129 format!(
130 "{}/b/{}/o?uploadType={}&name={}",
131 UPLOAD_BASE_URL,
132 percent_encode(&self.bucket),
133 upload_type,
134 percent_encode(&self.name)
135 )
136 }
137}
138
139#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
140#[serde(rename_all = "camelCase")]
141pub struct Bucket {
142 name: String,
143}
144
145impl Bucket {
146 pub fn new(name: &str) -> Self {
147 Self {
148 name: name.to_owned(),
149 }
150 }
151
152 pub fn url(&self) -> String {
153 format!("{}/b/{}", BASE_URL, percent_encode(&self.name))
154 }
155}
156
157impl TryInto<Object> for PartialObject {
158 type Error = Error;
159
160 fn try_into(self) -> Result<Object, Self::Error> {
161 fn err(e: &str) -> Error {
162 Error::GcsPartialResponseError(e.to_owned())
163 }
164
165 match (self.bucket, self.name) {
166 (Some(bucket), Some(name)) => Ok(Object { bucket, name }),
167 (None, Some(_)) => Err(err("bucket field is missing")),
168 (Some(_), None) => Err(err("name field is missing")),
169 (None, None) => Err(err("bucket and name fields are missing")),
170 }
171 }
172}
173
174#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize, Default)]
175#[serde(rename_all = "camelCase")]
176pub struct PartialObject {
177 pub bucket: Option<String>,
178 pub id: Option<String>,
179 pub self_link: Option<String>,
180 pub name: Option<String>,
181 pub content_type: Option<String>,
182 pub time_created: Option<chrono::DateTime<chrono::Utc>>,
183 pub updated: Option<chrono::DateTime<chrono::Utc>>,
184 pub time_deleted: Option<chrono::DateTime<chrono::Utc>>,
185 pub retention_expiration_time: Option<chrono::DateTime<chrono::Utc>>,
186 pub storage_class: Option<String>,
187 #[serde(default, deserialize_with = "from_string_option")]
188 pub size: Option<u64>,
189 pub media_link: Option<String>,
190 pub content_encoding: Option<String>,
191 pub content_disposition: Option<String>,
192 pub content_language: Option<String>,
193 pub cache_control: Option<String>,
194 pub metadata: Option<Metadata>,
195 #[serde(default, deserialize_with = "from_string_option")]
196 pub crc32c: Option<CRC32C>,
197 pub etag: Option<String>,
198}
199
200#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize, Default)]
201#[serde(rename_all = "camelCase")]
202pub struct CRC32C {
203 value: u32,
204}
205
206impl CRC32C {
207 pub fn new(value: u32) -> Self {
208 Self { value }
209 }
210 pub fn to_u32(&self) -> u32 {
211 self.value
212 }
213}
214#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
215pub enum Base64EncodedCRC32CError {
216 Base64DecodeError(String),
217 Base64ToU32BigEndianError(Vec<u8>),
218}
219
220impl Display for Base64EncodedCRC32CError {
221 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
222 write!(f, "{:?}", self)
223 }
224}
225
226impl FromStr for CRC32C {
227 type Err = Base64EncodedCRC32CError;
228
229 fn from_str(base64crc32c: &str) -> Result<Self, Self::Err> {
230 let decoded = base64::engine::general_purpose::STANDARD
231 .decode(base64crc32c)
232 .map_err(|err| Base64EncodedCRC32CError::Base64DecodeError(format!("{:?}", err)))?;
233 let crc32c = decoded
234 .try_into()
235 .map(u32::from_be_bytes)
236 .map_err(Base64EncodedCRC32CError::Base64ToU32BigEndianError)?;
237 Ok(CRC32C::new(crc32c))
238 }
239}
240
241fn from_string_option<'de, T, D>(deserializer: D) -> std::result::Result<Option<T>, D::Error>
242where
243 T: std::str::FromStr,
244 T::Err: std::fmt::Display,
245 D: serde::Deserializer<'de>,
246{
247 use serde::{de::Error, Deserialize};
248 use serde_json::Value;
249 match Deserialize::deserialize(deserializer) {
250 Ok(Value::String(s)) => T::from_str(&s).map(Option::from).map_err(Error::custom),
251 Ok(Value::Number(num)) => T::from_str(&num.to_string())
252 .map(Option::from)
253 .map_err(Error::custom),
254 Ok(value) => Err(Error::custom(format!(
255 "Wrong type, expected type {} but got value {:?}",
256 std::any::type_name::<T>(),
257 value,
258 ))),
259 Err(_) => Ok(None),
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use std::{convert::TryInto, str::FromStr};
266
267 use crate::storage::{Bucket, Error, Object};
268
269 use super::PartialObject;
270
271 #[test]
272 fn fn_gs_url_parsing_to_object() {
273 assert_eq!(
274 Object::new("hello", "world/object").unwrap(),
275 Object::from_str("gs://hello/world/object").unwrap(),
276 )
277 }
278
279 #[test]
280 fn test_invalid_object() {
281 fn assert_object_error(bucket: &str, name: &str) {
282 assert!(matches!(
283 Object::new(bucket, name).unwrap_err(),
284 Error::GcsInvalidObjectName
285 ))
286 }
287 assert_object_error("", "name");
288 assert_object_error("bucket", "");
289 assert_object_error("bucket", ".");
290 assert_object_error("bucket", "..");
291 }
292 #[test]
293 fn test_object_display() {
294 let o = Object::new("hello", "world").unwrap();
295 assert_eq!("gs://hello/world", o.gs_url());
296 assert_eq!("gs://hello/world", format!("{}", o));
297 }
298
299 #[test]
300 fn test_object_url() {
301 let o = Object::new("hello/hello", "world/world").unwrap();
302 assert_eq!("storage/v1/b/hello%2Fhello/o/world%2Fworld", o.url());
303 }
304
305 #[test]
306 fn test_object_upload_url() {
307 let o = Object::new("hello/hello", "world/world").unwrap();
308 assert_eq!(
309 "upload/storage/v1/b/hello%2Fhello/o?uploadType=media&name=world%2Fworld",
310 o.upload_url("media")
311 );
312 }
313
314 #[test]
315 fn test_bucket_url() {
316 let b = Bucket::new("hello/hello");
317 assert_eq!("storage/v1/b/hello%2Fhello", b.url());
318 }
319
320 #[test]
321 fn test_partial_object_into_object() {
322 let p = PartialObject {
323 bucket: Some("hello".to_owned()),
324 name: Some("world".to_owned()),
325 ..Default::default()
326 };
327
328 assert_eq!(
329 Object::new("hello", "world").unwrap(),
330 p.try_into().unwrap()
331 );
332 }
333}