1#![warn(missing_docs)]
7#![warn(missing_debug_implementations)]
8
9pub mod scope;
10
11use std::borrow::Cow;
12use std::collections::{BTreeMap, HashSet};
13use std::fmt;
14use std::str::FromStr;
15use std::time::{Duration, SystemTime};
16
17use http::header::{self, HeaderMap, HeaderName};
18use humantime::{
19 format_duration, format_rfc3339_micros, format_rfc3339_seconds, parse_duration, parse_rfc3339,
20};
21use serde::{Deserialize, Serialize};
22
23pub const HEADER_EXPIRATION: &str = "x-sn-expiration";
25pub const HEADER_REDIRECT_TOMBSTONE: &str = "x-sn-redirect-tombstone";
27pub const HEADER_TIME_CREATED: &str = "x-sn-time-created";
29pub const HEADER_TIME_EXPIRES: &str = "x-sn-time-expires";
31pub const HEADER_META_PREFIX: &str = "x-snme-";
33
34pub const DEFAULT_CONTENT_TYPE: &str = "application/octet-stream";
36
37#[derive(Debug, thiserror::Error)]
39pub enum Error {
40 #[error("error dealing with http headers")]
42 Header(#[from] Option<http::Error>),
43 #[error("invalid expiration policy value")]
45 InvalidExpiration(#[from] Option<humantime::DurationError>),
46 #[error("invalid compression value")]
48 InvalidCompression,
49 #[error("invalid content type")]
51 InvalidContentType(#[from] mediatype::MediaTypeError),
52 #[error("invalid creation time")]
54 InvalidCreationTime(#[from] humantime::TimestampError),
55}
56impl From<http::header::InvalidHeaderValue> for Error {
57 fn from(err: http::header::InvalidHeaderValue) -> Self {
58 Self::Header(Some(err.into()))
59 }
60}
61impl From<http::header::InvalidHeaderName> for Error {
62 fn from(err: http::header::InvalidHeaderName) -> Self {
63 Self::Header(Some(err.into()))
64 }
65}
66impl From<http::header::ToStrError> for Error {
67 fn from(_err: http::header::ToStrError) -> Self {
68 Self::Header(None)
70 }
71}
72
73#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
79pub enum ExpirationPolicy {
80 #[default]
83 Manual,
84 TimeToLive(Duration),
86 TimeToIdle(Duration),
88}
89impl ExpirationPolicy {
90 pub fn expires_in(&self) -> Option<Duration> {
92 match self {
93 ExpirationPolicy::Manual => None,
94 ExpirationPolicy::TimeToLive(duration) => Some(*duration),
95 ExpirationPolicy::TimeToIdle(duration) => Some(*duration),
96 }
97 }
98
99 pub fn is_timeout(&self) -> bool {
101 match self {
102 ExpirationPolicy::TimeToLive(_) => true,
103 ExpirationPolicy::TimeToIdle(_) => true,
104 ExpirationPolicy::Manual => false,
105 }
106 }
107
108 pub fn is_manual(&self) -> bool {
110 *self == ExpirationPolicy::Manual
111 }
112}
113impl fmt::Display for ExpirationPolicy {
114 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115 match self {
116 ExpirationPolicy::TimeToLive(duration) => {
117 write!(f, "ttl:{}", format_duration(*duration))
118 }
119 ExpirationPolicy::TimeToIdle(duration) => {
120 write!(f, "tti:{}", format_duration(*duration))
121 }
122 ExpirationPolicy::Manual => f.write_str("manual"),
123 }
124 }
125}
126impl FromStr for ExpirationPolicy {
127 type Err = Error;
128
129 fn from_str(s: &str) -> Result<Self, Self::Err> {
130 if s == "manual" {
131 return Ok(ExpirationPolicy::Manual);
132 }
133 if let Some(duration) = s.strip_prefix("ttl:") {
134 return Ok(ExpirationPolicy::TimeToLive(parse_duration(duration)?));
135 }
136 if let Some(duration) = s.strip_prefix("tti:") {
137 return Ok(ExpirationPolicy::TimeToIdle(parse_duration(duration)?));
138 }
139 Err(Error::InvalidExpiration(None))
140 }
141}
142
143#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
145pub enum Compression {
146 Zstd,
148 }
153
154impl Compression {
155 pub fn as_str(&self) -> &str {
157 match self {
158 Compression::Zstd => "zstd",
159 }
162 }
163}
164
165impl fmt::Display for Compression {
166 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167 f.write_str(self.as_str())
168 }
169}
170
171impl FromStr for Compression {
172 type Err = Error;
173
174 fn from_str(s: &str) -> Result<Self, Self::Err> {
175 match s {
176 "zstd" => Ok(Compression::Zstd),
177 _ => Err(Error::InvalidCompression),
180 }
181 }
182}
183
184#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)]
186pub enum Permission {
187 #[serde(rename = "object.read")]
189 ObjectRead,
190
191 #[serde(rename = "object.write")]
193 ObjectWrite,
194
195 #[serde(rename = "object.delete")]
197 ObjectDelete,
198}
199
200impl Permission {
201 pub fn rwd() -> HashSet<Permission> {
203 HashSet::from([
204 Permission::ObjectRead,
205 Permission::ObjectWrite,
206 Permission::ObjectDelete,
207 ])
208 }
209}
210
211#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
216#[serde(default)]
217pub struct Metadata {
218 #[serde(skip_serializing_if = "Option::is_none")]
225 pub is_redirect_tombstone: Option<bool>,
226
227 #[serde(skip_serializing_if = "ExpirationPolicy::is_manual")]
229 pub expiration_policy: ExpirationPolicy,
230
231 #[serde(skip_serializing_if = "Option::is_none")]
236 pub time_created: Option<SystemTime>,
237
238 #[serde(skip_serializing_if = "Option::is_none")]
243 pub time_expires: Option<SystemTime>,
244
245 pub content_type: Cow<'static, str>,
247
248 #[serde(skip_serializing_if = "Option::is_none")]
250 pub compression: Option<Compression>,
251
252 #[serde(skip_serializing_if = "Option::is_none")]
254 pub size: Option<usize>,
255
256 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
258 pub custom: BTreeMap<String, String>,
259}
260
261impl Metadata {
262 pub fn from_headers(headers: &HeaderMap, prefix: &str) -> Result<Self, Error> {
266 let mut metadata = Metadata::default();
267
268 for (name, value) in headers {
269 match *name {
270 header::CONTENT_TYPE => {
272 let content_type = value.to_str()?;
273 validate_content_type(content_type)?;
274 metadata.content_type = content_type.to_owned().into();
275 }
276 header::CONTENT_ENCODING => {
277 let compression = value.to_str()?;
278 metadata.compression = Some(Compression::from_str(compression)?);
279 }
280 _ => {
281 let Some(name) = name.as_str().strip_prefix(prefix) else {
282 continue;
283 };
284
285 match name {
286 HEADER_EXPIRATION => {
288 let expiration_policy = value.to_str()?;
289 metadata.expiration_policy =
290 ExpirationPolicy::from_str(expiration_policy)?;
291 }
292 HEADER_REDIRECT_TOMBSTONE => {
293 if value.to_str()? == "true" {
294 metadata.is_redirect_tombstone = Some(true);
295 }
296 }
297 HEADER_TIME_CREATED => {
298 let timestamp = value.to_str()?;
299 let time = parse_rfc3339(timestamp)?;
300 metadata.time_created = Some(time);
301 }
302 HEADER_TIME_EXPIRES => {
303 let timestamp = value.to_str()?;
304 let time = parse_rfc3339(timestamp)?;
305 metadata.time_expires = Some(time);
306 }
307 _ => {
308 if let Some(name) = name.strip_prefix(HEADER_META_PREFIX) {
310 let value = value.to_str()?;
311 metadata.custom.insert(name.into(), value.into());
312 }
313 }
314 }
315 }
316 }
317 }
318
319 Ok(metadata)
320 }
321
322 pub fn to_headers(&self, prefix: &str, with_expiration: bool) -> Result<HeaderMap, Error> {
328 let Self {
329 is_redirect_tombstone,
330 content_type,
331 compression,
332 expiration_policy,
333 time_created,
334 time_expires,
335 size: _,
336 custom,
337 } = self;
338
339 let mut headers = HeaderMap::new();
340
341 headers.append(header::CONTENT_TYPE, content_type.parse()?);
343 if let Some(compression) = compression {
344 headers.append(header::CONTENT_ENCODING, compression.as_str().parse()?);
345 }
346
347 if matches!(is_redirect_tombstone, Some(true)) {
349 let name = HeaderName::try_from(format!("{prefix}{HEADER_REDIRECT_TOMBSTONE}"))?;
350 headers.append(name, "true".parse()?);
351 }
352 if *expiration_policy != ExpirationPolicy::Manual {
353 let name = HeaderName::try_from(format!("{prefix}{HEADER_EXPIRATION}"))?;
354 headers.append(name, expiration_policy.to_string().parse()?);
355 if with_expiration {
356 let expires_in = expiration_policy.expires_in().unwrap_or_default();
357 let expires_at = format_rfc3339_seconds(SystemTime::now() + expires_in);
358 headers.append("x-goog-custom-time", expires_at.to_string().parse()?);
359 }
360 }
361 if let Some(time) = time_created {
362 let name = HeaderName::try_from(format!("{prefix}{HEADER_TIME_CREATED}"))?;
363 let timestamp = format_rfc3339_micros(*time);
364 headers.append(name, timestamp.to_string().parse()?);
365 }
366 if let Some(time) = time_expires {
367 let name = HeaderName::try_from(format!("{prefix}{HEADER_TIME_EXPIRES}"))?;
368 let timestamp = format_rfc3339_micros(*time);
369 headers.append(name, timestamp.to_string().parse()?);
370 }
371
372 for (key, value) in custom {
374 let name = HeaderName::try_from(format!("{prefix}{HEADER_META_PREFIX}{key}"))?;
375 headers.append(name, value.parse()?);
376 }
377
378 Ok(headers)
379 }
380}
381
382fn validate_content_type(content_type: &str) -> Result<(), Error> {
385 mediatype::MediaType::parse(content_type)?;
386 Ok(())
387}
388
389impl Default for Metadata {
390 fn default() -> Self {
391 Self {
392 is_redirect_tombstone: None,
393 expiration_policy: ExpirationPolicy::Manual,
394 time_created: None,
395 time_expires: None,
396 content_type: DEFAULT_CONTENT_TYPE.into(),
397 compression: None,
398 size: None,
399 custom: BTreeMap::new(),
400 }
401 }
402}