Skip to content

Commit d4fa51d

Browse files
authored
feat: Add time_expires as first-class metadata (#217)
1 parent f417e7c commit d4fa51d

File tree

5 files changed

+42
-0
lines changed

5 files changed

+42
-0
lines changed

clients/python/src/objectstore_client/metadata.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
HEADER_EXPIRATION = "x-sn-expiration"
1313
HEADER_TIME_CREATED = "x-sn-time-created"
14+
HEADER_TIME_EXPIRES = "x-sn-time-expires"
1415
HEADER_META_PREFIX = "x-snme-"
1516

1617

@@ -35,10 +36,22 @@ class Metadata:
3536
time_created: datetime | None
3637
"""
3738
Timestamp indicating when the object was created or the last time it was replaced.
39+
3840
This means that a PUT request to an existing object causes this value to be bumped.
3941
This field is computed by the server, it cannot be set by clients.
4042
"""
4143

44+
time_expires: datetime | None
45+
"""
46+
Timestamp indicating when the object will expire.
47+
48+
When using a Time To Idle expiration policy, this value will reflect the expiration
49+
timestamp present prior to the current access to the object.
50+
51+
This field is computed by the server, it cannot be set by clients.
52+
Use `expiration_policy` to set an expiration policy instead.
53+
"""
54+
4255
custom: dict[str, str]
4356

4457
@classmethod
@@ -47,6 +60,7 @@ def from_headers(cls, headers: Mapping[str, str]) -> Metadata:
4760
compression = None
4861
expiration_policy = None
4962
time_created = None
63+
time_expires = None
5064
custom_metadata = {}
5165

5266
for k, v in headers.items():
@@ -58,6 +72,8 @@ def from_headers(cls, headers: Mapping[str, str]) -> Metadata:
5872
expiration_policy = parse_expiration(v)
5973
elif k == HEADER_TIME_CREATED:
6074
time_created = datetime.fromisoformat(v)
75+
elif k == HEADER_TIME_EXPIRES:
76+
time_expires = datetime.fromisoformat(v)
6177
elif k.startswith(HEADER_META_PREFIX):
6278
custom_metadata[k[len(HEADER_META_PREFIX) :]] = v
6379

@@ -66,6 +82,7 @@ def from_headers(cls, headers: Mapping[str, str]) -> Metadata:
6682
compression=compression,
6783
expiration_policy=expiration_policy,
6884
time_created=time_created,
85+
time_expires=time_expires,
6986
custom=custom_metadata,
7087
)
7188

objectstore-service/src/backend/bigtable.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,7 @@ impl Backend for BigTableBackend {
263263
// TODO: Inject the access time from the request.
264264
let access_time = SystemTime::now();
265265
metadata.size = Some(value.len());
266+
metadata.time_expires = expire_at;
266267

267268
// Filter already expired objects but leave them to garbage collection
268269
if metadata.expiration_policy.is_timeout() && expire_at.is_some_and(|ts| ts < access_time) {

objectstore-service/src/backend/gcs.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ impl GcsObject {
144144
size,
145145
custom,
146146
time_created,
147+
time_expires: self.custom_time,
147148
})
148149
}
149150
}
@@ -478,6 +479,7 @@ mod tests {
478479
compression: None,
479480
custom: BTreeMap::from_iter([("hello".into(), "world".into())]),
480481
time_created: Some(SystemTime::now()),
482+
time_expires: None,
481483
size: None,
482484
};
483485

objectstore-service/src/backend/local_fs.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ mod tests {
130130
content_type: "text/plain".into(),
131131
expiration_policy: ExpirationPolicy::TimeToIdle(Duration::from_secs(3600)),
132132
time_created: Some(SystemTime::now()),
133+
time_expires: None,
133134
compression: Some(Compression::Zstd),
134135
custom: [("foo".into(), "bar".into())].into(),
135136
size: None,

objectstore-types/src/lib.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ pub const HEADER_EXPIRATION: &str = "x-sn-expiration";
2424
pub const HEADER_REDIRECT_TOMBSTONE: &str = "x-sn-redirect-tombstone";
2525
/// The custom HTTP header that contains the object creation time.
2626
pub const HEADER_TIME_CREATED: &str = "x-sn-time-created";
27+
/// The custom HTTP header that contains the object expiration time.
28+
pub const HEADER_TIME_EXPIRES: &str = "x-sn-time-expires";
2729
/// The prefix for custom HTTP headers containing custom per-object metadata.
2830
pub const HEADER_META_PREFIX: &str = "x-snme-";
2931

@@ -204,6 +206,13 @@ pub struct Metadata {
204206
#[serde(skip_serializing_if = "Option::is_none")]
205207
pub time_created: Option<SystemTime>,
206208

209+
/// The expiration time of the object, if any, in accordance with its expiration policy.
210+
///
211+
/// When using a Time To Idle expiration policy, this value will reflect the expiration
212+
/// timestamp present prior to the current access to the object.
213+
#[serde(skip_serializing_if = "Option::is_none")]
214+
pub time_expires: Option<SystemTime>,
215+
207216
/// The content type of the object, if known.
208217
pub content_type: Cow<'static, str>,
209218

@@ -261,6 +270,11 @@ impl Metadata {
261270
let time = parse_rfc3339(timestamp)?;
262271
metadata.time_created = Some(time);
263272
}
273+
HEADER_TIME_EXPIRES => {
274+
let timestamp = value.to_str()?;
275+
let time = parse_rfc3339(timestamp)?;
276+
metadata.time_expires = Some(time);
277+
}
264278
_ => {
265279
// customer-provided metadata
266280
if let Some(name) = name.strip_prefix(HEADER_META_PREFIX) {
@@ -288,6 +302,7 @@ impl Metadata {
288302
compression,
289303
expiration_policy,
290304
time_created,
305+
time_expires,
291306
size: _,
292307
custom,
293308
} = self;
@@ -319,6 +334,11 @@ impl Metadata {
319334
let timestamp = format_rfc3339_micros(*time);
320335
headers.append(name, timestamp.to_string().parse()?);
321336
}
337+
if let Some(time) = time_expires {
338+
let name = HeaderName::try_from(format!("{prefix}{HEADER_TIME_EXPIRES}"))?;
339+
let timestamp = format_rfc3339_micros(*time);
340+
headers.append(name, timestamp.to_string().parse()?);
341+
}
322342

323343
// customer-provided metadata
324344
for (key, value) in custom {
@@ -343,6 +363,7 @@ impl Default for Metadata {
343363
is_redirect_tombstone: None,
344364
expiration_policy: ExpirationPolicy::Manual,
345365
time_created: None,
366+
time_expires: None,
346367
content_type: DEFAULT_CONTENT_TYPE.into(),
347368
compression: None,
348369
size: None,

0 commit comments

Comments
 (0)