@@ -13,13 +13,17 @@ use std::str::FromStr;
1313use std:: time:: { Duration , SystemTime } ;
1414
1515use http:: header:: { self , HeaderMap , HeaderName } ;
16- use humantime:: { format_duration, format_rfc3339_seconds, parse_duration} ;
16+ use humantime:: {
17+ format_duration, format_rfc3339_micros, format_rfc3339_seconds, parse_duration, parse_rfc3339,
18+ } ;
1719use serde:: { Deserialize , Serialize } ;
1820
1921/// The custom HTTP header that contains the serialized [`ExpirationPolicy`].
2022pub const HEADER_EXPIRATION : & str = "x-sn-expiration" ;
2123/// The custom HTTP header that contains the serialized redirect tombstone.
2224pub const HEADER_REDIRECT_TOMBSTONE : & str = "x-sn-redirect-tombstone" ;
25+ /// The custom HTTP header that contains the object creation time.
26+ pub const HEADER_TIME_CREATED : & str = "x-sn-time-created" ;
2327/// The prefix for custom HTTP headers containing custom per-object metadata.
2428pub const HEADER_META_PREFIX : & str = "x-snme-" ;
2529
@@ -41,6 +45,9 @@ pub enum Error {
4145 /// The content type is invalid.
4246 #[ error( "invalid content type" ) ]
4347 InvalidContentType ( #[ from] mediatype:: MediaTypeError ) ,
48+ /// The creation time is invalid.
49+ #[ error( "invalid creation time" ) ]
50+ InvalidCreationTime ( #[ from] humantime:: TimestampError ) ,
4451}
4552impl From < http:: header:: InvalidHeaderValue > for Error {
4653 fn from ( err : http:: header:: InvalidHeaderValue ) -> Self {
@@ -190,6 +197,13 @@ pub struct Metadata {
190197 #[ serde( skip_serializing_if = "ExpirationPolicy::is_manual" ) ]
191198 pub expiration_policy : ExpirationPolicy ,
192199
200+ /// The creation/last replacement time of the object, if known.
201+ ///
202+ /// This is populated by the server when performing a POST or PUT request, i.e. when an object is
203+ /// first created or when an existing object is overwritten.
204+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
205+ pub time_created : Option < SystemTime > ,
206+
193207 /// The content type of the object, if known.
194208 pub content_type : Cow < ' static , str > ,
195209
@@ -242,6 +256,11 @@ impl Metadata {
242256 metadata. is_redirect_tombstone = Some ( true ) ;
243257 }
244258 }
259+ HEADER_TIME_CREATED => {
260+ let timestamp = value. to_str ( ) ?;
261+ let time = parse_rfc3339 ( timestamp) ?;
262+ metadata. time_created = Some ( time) ;
263+ }
245264 _ => {
246265 // customer-provided metadata
247266 if let Some ( name) = name. strip_prefix ( HEADER_META_PREFIX ) {
@@ -268,6 +287,7 @@ impl Metadata {
268287 content_type,
269288 compression,
270289 expiration_policy,
290+ time_created,
271291 size : _,
272292 custom,
273293 } = self ;
@@ -294,6 +314,11 @@ impl Metadata {
294314 headers. append ( "x-goog-custom-time" , expires_at. to_string ( ) . parse ( ) ?) ;
295315 }
296316 }
317+ if let Some ( time) = time_created {
318+ let name = HeaderName :: try_from ( format ! ( "{prefix}{HEADER_TIME_CREATED}" ) ) ?;
319+ let timestamp = format_rfc3339_micros ( * time) ;
320+ headers. append ( name, timestamp. to_string ( ) . parse ( ) ?) ;
321+ }
297322
298323 // customer-provided metadata
299324 for ( key, value) in custom {
@@ -317,6 +342,7 @@ impl Default for Metadata {
317342 Self {
318343 is_redirect_tombstone : None ,
319344 expiration_policy : ExpirationPolicy :: Manual ,
345+ time_created : None ,
320346 content_type : DEFAULT_CONTENT_TYPE . into ( ) ,
321347 compression : None ,
322348 size : None ,
0 commit comments