1#![warn(missing_docs)]
7#![warn(missing_debug_implementations)]
8
9use std::borrow::Cow;
10use std::collections::BTreeMap;
11use std::fmt;
12use std::str::FromStr;
13use std::time::{Duration, SystemTime};
14
15use http::header::{self, HeaderMap, HeaderName};
16use humantime::{format_duration, format_rfc3339_seconds, parse_duration};
17use serde::{Deserialize, Serialize};
18
19pub const HEADER_EXPIRATION: &str = "x-sn-expiration";
21pub const HEADER_REDIRECT_TOMBSTONE: &str = "x-sn-redirect-tombstone";
23pub const HEADER_META_PREFIX: &str = "x-snme-";
25
26pub const PARAM_SCOPE: &str = "scope";
28pub const PARAM_USECASE: &str = "usecase";
30
31pub const DEFAULT_CONTENT_TYPE: &str = "application/octet-stream";
33
34#[derive(Debug, thiserror::Error)]
36pub enum Error {
37 #[error("error dealing with http headers")]
39 Header(#[from] Option<http::Error>),
40 #[error("invalid expiration policy value")]
42 InvalidExpiration(#[from] Option<humantime::DurationError>),
43 #[error("invalid compression value")]
45 InvalidCompression,
46}
47impl From<http::header::InvalidHeaderValue> for Error {
48 fn from(err: http::header::InvalidHeaderValue) -> Self {
49 Self::Header(Some(err.into()))
50 }
51}
52impl From<http::header::InvalidHeaderName> for Error {
53 fn from(err: http::header::InvalidHeaderName) -> Self {
54 Self::Header(Some(err.into()))
55 }
56}
57impl From<http::header::ToStrError> for Error {
58 fn from(_err: http::header::ToStrError) -> Self {
59 Self::Header(None)
61 }
62}
63
64#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
70pub enum ExpirationPolicy {
71 #[default]
74 Manual,
75 TimeToLive(Duration),
77 TimeToIdle(Duration),
79}
80impl ExpirationPolicy {
81 pub fn expires_in(&self) -> Option<Duration> {
83 match self {
84 ExpirationPolicy::Manual => None,
85 ExpirationPolicy::TimeToLive(duration) => Some(*duration),
86 ExpirationPolicy::TimeToIdle(duration) => Some(*duration),
87 }
88 }
89
90 pub fn is_timeout(&self) -> bool {
92 match self {
93 ExpirationPolicy::TimeToLive(_) => true,
94 ExpirationPolicy::TimeToIdle(_) => true,
95 ExpirationPolicy::Manual => false,
96 }
97 }
98
99 pub fn is_manual(&self) -> bool {
101 *self == ExpirationPolicy::Manual
102 }
103}
104impl fmt::Display for ExpirationPolicy {
105 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106 match self {
107 ExpirationPolicy::TimeToLive(duration) => {
108 write!(f, "ttl:{}", format_duration(*duration))
109 }
110 ExpirationPolicy::TimeToIdle(duration) => {
111 write!(f, "tti:{}", format_duration(*duration))
112 }
113 ExpirationPolicy::Manual => f.write_str("manual"),
114 }
115 }
116}
117impl FromStr for ExpirationPolicy {
118 type Err = Error;
119
120 fn from_str(s: &str) -> Result<Self, Self::Err> {
121 if s == "manual" {
122 return Ok(ExpirationPolicy::Manual);
123 }
124 if let Some(duration) = s.strip_prefix("ttl:") {
125 return Ok(ExpirationPolicy::TimeToLive(parse_duration(duration)?));
126 }
127 if let Some(duration) = s.strip_prefix("tti:") {
128 return Ok(ExpirationPolicy::TimeToIdle(parse_duration(duration)?));
129 }
130 Err(Error::InvalidExpiration(None))
131 }
132}
133
134#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
136pub enum Compression {
137 Zstd,
139 }
144
145impl Compression {
146 pub fn as_str(&self) -> &str {
148 match self {
149 Compression::Zstd => "zstd",
150 }
153 }
154}
155
156impl fmt::Display for Compression {
157 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158 f.write_str(self.as_str())
159 }
160}
161
162impl FromStr for Compression {
163 type Err = Error;
164
165 fn from_str(s: &str) -> Result<Self, Self::Err> {
166 match s {
167 "zstd" => Ok(Compression::Zstd),
168 _ => Err(Error::InvalidCompression),
171 }
172 }
173}
174
175#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
180#[serde(default)]
181pub struct Metadata {
182 #[serde(skip_serializing_if = "Option::is_none")]
189 pub is_redirect_tombstone: Option<bool>,
190
191 #[serde(skip_serializing_if = "ExpirationPolicy::is_manual")]
193 pub expiration_policy: ExpirationPolicy,
194
195 pub content_type: Cow<'static, str>,
197
198 #[serde(skip_serializing_if = "Option::is_none")]
200 pub compression: Option<Compression>,
201
202 #[serde(skip_serializing_if = "Option::is_none")]
204 pub size: Option<usize>,
205
206 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
208 pub custom: BTreeMap<String, String>,
209}
210
211impl Metadata {
212 pub fn from_headers(headers: &HeaderMap, prefix: &str) -> Result<Self, Error> {
216 let mut metadata = Metadata::default();
217
218 for (name, value) in headers {
219 if name == header::CONTENT_TYPE {
220 let content_type = value.to_str()?;
221 metadata.content_type = content_type.to_owned().into();
222 } else if name == header::CONTENT_ENCODING {
223 let compression = value.to_str()?;
224 metadata.compression = Some(Compression::from_str(compression)?);
225 } else if let Some(name) = name.as_str().strip_prefix(prefix) {
226 if name == HEADER_EXPIRATION {
227 let expiration_policy = value.to_str()?;
228 metadata.expiration_policy = ExpirationPolicy::from_str(expiration_policy)?;
229 } else if name == HEADER_REDIRECT_TOMBSTONE {
230 if value.to_str()? == "true" {
231 metadata.is_redirect_tombstone = Some(true);
232 }
233 } else if let Some(name) = name.strip_prefix(HEADER_META_PREFIX) {
234 let value = value.to_str()?;
235 metadata.custom.insert(name.into(), value.into());
236 }
237 }
238 }
239
240 Ok(metadata)
241 }
242
243 pub fn to_headers(&self, prefix: &str, with_expiration: bool) -> Result<HeaderMap, Error> {
249 let Self {
250 is_redirect_tombstone,
251 content_type,
252 compression,
253 expiration_policy,
254 size: _,
255 custom,
256 } = self;
257
258 let mut headers = HeaderMap::new();
259 headers.append(header::CONTENT_TYPE, content_type.parse()?);
260
261 if matches!(is_redirect_tombstone, Some(true)) {
262 let name = HeaderName::try_from(format!("{prefix}{HEADER_REDIRECT_TOMBSTONE}"))?;
263 headers.append(name, "true".parse()?);
264 }
265
266 if let Some(compression) = compression {
267 headers.append(header::CONTENT_ENCODING, compression.as_str().parse()?);
268 }
269
270 if *expiration_policy != ExpirationPolicy::Manual {
271 let name = HeaderName::try_from(format!("{prefix}{HEADER_EXPIRATION}"))?;
272 headers.append(name, expiration_policy.to_string().parse()?);
273 if with_expiration {
274 let expires_in = expiration_policy.expires_in().unwrap_or_default();
275 let expires_at = format_rfc3339_seconds(SystemTime::now() + expires_in);
276 headers.append("x-goog-custom-time", expires_at.to_string().parse()?);
277 }
278 }
279
280 for (key, value) in custom {
281 let name = HeaderName::try_from(format!("{prefix}{HEADER_META_PREFIX}{key}"))?;
282 headers.append(name, value.parse()?);
283 }
284
285 Ok(headers)
286 }
287}
288
289impl Default for Metadata {
290 fn default() -> Self {
291 Self {
292 is_redirect_tombstone: None,
293 expiration_policy: ExpirationPolicy::Manual,
294 content_type: DEFAULT_CONTENT_TYPE.into(),
295 compression: None,
296 size: None,
297 custom: BTreeMap::new(),
298 }
299 }
300}