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_META_PREFIX: &str = "x-snme-";
23
24pub const PARAM_SCOPE: &str = "scope";
26pub const PARAM_USECASE: &str = "usecase";
28
29pub const DEFAULT_CONTENT_TYPE: &str = "application/octet-stream";
31
32#[derive(Debug, thiserror::Error)]
34pub enum Error {
35 #[error("error dealing with http headers")]
37 Header(#[from] Option<http::Error>),
38 #[error("invalid expiration policy value")]
40 InvalidExpiration(#[from] Option<humantime::DurationError>),
41 #[error("invalid compression value")]
43 InvalidCompression,
44}
45impl From<http::header::InvalidHeaderValue> for Error {
46 fn from(err: http::header::InvalidHeaderValue) -> Self {
47 Self::Header(Some(err.into()))
48 }
49}
50impl From<http::header::InvalidHeaderName> for Error {
51 fn from(err: http::header::InvalidHeaderName) -> Self {
52 Self::Header(Some(err.into()))
53 }
54}
55impl From<http::header::ToStrError> for Error {
56 fn from(_err: http::header::ToStrError) -> Self {
57 Self::Header(None)
59 }
60}
61
62#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
68pub enum ExpirationPolicy {
69 #[default]
72 Manual,
73 TimeToLive(Duration),
75 TimeToIdle(Duration),
77}
78impl ExpirationPolicy {
79 pub fn expires_in(&self) -> Option<Duration> {
81 match self {
82 ExpirationPolicy::Manual => None,
83 ExpirationPolicy::TimeToLive(duration) => Some(*duration),
84 ExpirationPolicy::TimeToIdle(duration) => Some(*duration),
85 }
86 }
87
88 pub fn is_timeout(&self) -> bool {
90 match self {
91 ExpirationPolicy::TimeToLive(_) => true,
92 ExpirationPolicy::TimeToIdle(_) => true,
93 ExpirationPolicy::Manual => false,
94 }
95 }
96
97 pub fn is_manual(&self) -> bool {
99 *self == ExpirationPolicy::Manual
100 }
101}
102impl fmt::Display for ExpirationPolicy {
103 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104 match self {
105 ExpirationPolicy::TimeToLive(duration) => {
106 write!(f, "ttl:{}", format_duration(*duration))
107 }
108 ExpirationPolicy::TimeToIdle(duration) => {
109 write!(f, "tti:{}", format_duration(*duration))
110 }
111 ExpirationPolicy::Manual => f.write_str("manual"),
112 }
113 }
114}
115impl FromStr for ExpirationPolicy {
116 type Err = Error;
117
118 fn from_str(s: &str) -> Result<Self, Self::Err> {
119 if s == "manual" {
120 return Ok(ExpirationPolicy::Manual);
121 }
122 if let Some(duration) = s.strip_prefix("ttl:") {
123 return Ok(ExpirationPolicy::TimeToLive(parse_duration(duration)?));
124 }
125 if let Some(duration) = s.strip_prefix("tti:") {
126 return Ok(ExpirationPolicy::TimeToIdle(parse_duration(duration)?));
127 }
128 Err(Error::InvalidExpiration(None))
129 }
130}
131
132#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
134pub enum Compression {
135 Zstd,
137 }
142
143impl Compression {
144 pub fn as_str(&self) -> &str {
146 match self {
147 Compression::Zstd => "zstd",
148 }
151 }
152}
153
154impl fmt::Display for Compression {
155 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156 f.write_str(self.as_str())
157 }
158}
159
160impl FromStr for Compression {
161 type Err = Error;
162
163 fn from_str(s: &str) -> Result<Self, Self::Err> {
164 match s {
165 "zstd" => Ok(Compression::Zstd),
166 _ => Err(Error::InvalidCompression),
169 }
170 }
171}
172
173#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
178#[serde(default)]
179pub struct Metadata {
180 #[serde(skip_serializing_if = "ExpirationPolicy::is_manual")]
182 pub expiration_policy: ExpirationPolicy,
183
184 pub content_type: Cow<'static, str>,
186
187 #[serde(skip_serializing_if = "Option::is_none")]
189 pub compression: Option<Compression>,
190
191 #[serde(skip_serializing_if = "Option::is_none")]
193 pub size: Option<usize>,
194
195 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
197 pub custom: BTreeMap<String, String>,
198}
199
200impl Metadata {
201 pub fn from_headers(headers: &HeaderMap, prefix: &str) -> Result<Self, Error> {
205 let mut metadata = Metadata::default();
206
207 for (name, value) in headers {
208 if name == header::CONTENT_TYPE {
209 let content_type = value.to_str()?;
210 metadata.content_type = content_type.to_owned().into();
211 } else if name == header::CONTENT_ENCODING {
212 let compression = value.to_str()?;
213 metadata.compression = Some(Compression::from_str(compression)?);
214 } else if let Some(name) = name.as_str().strip_prefix(prefix) {
215 if name == HEADER_EXPIRATION {
216 let expiration_policy = value.to_str()?;
217 metadata.expiration_policy = ExpirationPolicy::from_str(expiration_policy)?;
218 } else if let Some(name) = name.strip_prefix(HEADER_META_PREFIX) {
219 let value = value.to_str()?;
220 metadata.custom.insert(name.into(), value.into());
221 }
222 }
223 }
224
225 Ok(metadata)
226 }
227
228 pub fn to_headers(&self, prefix: &str, with_expiration: bool) -> Result<HeaderMap, Error> {
234 let Self {
235 content_type,
236 compression,
237 expiration_policy,
238 size: _,
239 custom,
240 } = self;
241
242 let mut headers = HeaderMap::new();
243 headers.append(header::CONTENT_TYPE, content_type.parse()?);
244
245 if let Some(compression) = compression {
246 headers.append(header::CONTENT_ENCODING, compression.as_str().parse()?);
247 }
248
249 if *expiration_policy != ExpirationPolicy::Manual {
250 let name = HeaderName::try_from(format!("{prefix}{HEADER_EXPIRATION}"))?;
251 headers.append(name, expiration_policy.to_string().parse()?);
252 if with_expiration {
253 let expires_in = expiration_policy.expires_in().unwrap_or_default();
254 let expires_at = format_rfc3339_seconds(SystemTime::now() + expires_in);
255 headers.append("x-goog-custom-time", expires_at.to_string().parse()?);
256 }
257 }
258
259 for (key, value) in custom {
260 let name = HeaderName::try_from(format!("{prefix}{HEADER_META_PREFIX}{key}"))?;
261 headers.append(name, value.parse()?);
262 }
263
264 Ok(headers)
265 }
266}
267
268impl Default for Metadata {
269 fn default() -> Self {
270 Self {
271 expiration_policy: ExpirationPolicy::Manual,
272 content_type: DEFAULT_CONTENT_TYPE.into(),
273 compression: None,
274 size: None,
275 custom: BTreeMap::new(),
276 }
277 }
278}