[pbs-devel] [RFC proxmox-backup 08/39] s3 client: add helper for last modified timestamp parsing
Christian Ebner
c.ebner at proxmox.com
Mon May 19 13:46:09 CEST 2025
Adds a helper to parse modified timestamps as encountered in s3 list
objects v2 and copy object api calls. Further, allow to convert a
timestamp to a Duration since unix epoch in order for easy comparison
between timestamps during phase 2 of garbage collection.
Signed-off-by: Christian Ebner <c.ebner at proxmox.com>
---
Cargo.toml | 1 +
pbs-s3-client/Cargo.toml | 2 +
pbs-s3-client/src/lib.rs | 118 +++++++++++++++++++++++++++++++++++++++
3 files changed, 121 insertions(+)
diff --git a/Cargo.toml b/Cargo.toml
index c2b0029ac..3f51b356c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -144,6 +144,7 @@ regex = "1.5.5"
rustyline = "9"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
+serde_plain = "1.0"
siphasher = "0.3"
syslog = "6"
tar = "0.4"
diff --git a/pbs-s3-client/Cargo.toml b/pbs-s3-client/Cargo.toml
index 11189ea50..9ee546200 100644
--- a/pbs-s3-client/Cargo.toml
+++ b/pbs-s3-client/Cargo.toml
@@ -11,6 +11,8 @@ anyhow.workspace = true
hex = { workspace = true, features = [ "serde" ] }
hyper.workspace = true
openssl.workspace = true
+serde.workspace = true
+serde_plain.workspace = true
tracing.workspace = true
url.workspace = true
diff --git a/pbs-s3-client/src/lib.rs b/pbs-s3-client/src/lib.rs
index a4081df15..308db64d8 100644
--- a/pbs-s3-client/src/lib.rs
+++ b/pbs-s3-client/src/lib.rs
@@ -3,3 +3,121 @@ mod client;
pub use client::{S3Client, S3ClientOptions};
mod object_key;
pub use object_key::{S3ObjectKey, S3_CONTENT_PREFIX};
+
+use std::time::Duration;
+
+use anyhow::{bail, Error};
+
+#[derive(Debug)]
+pub struct LastModifiedTimestamp {
+ epoch: i64,
+ milliseconds: u64,
+}
+
+impl LastModifiedTimestamp {
+ pub fn to_duration(&self) -> Result<Duration, Error> {
+ let secs = u64::try_from(self.epoch)?;
+ let mut duration = Duration::from_secs(secs);
+ duration += Duration::from_millis(self.milliseconds);
+ Ok(duration)
+ }
+}
+
+impl std::str::FromStr for LastModifiedTimestamp {
+ type Err = Error;
+
+ fn from_str(timestamp: &str) -> Result<Self, Self::Err> {
+ let input = timestamp.as_bytes();
+
+ let expect = |pos: usize, c: u8| {
+ if input[pos] != c {
+ bail!("unexpected char at pos {pos}");
+ }
+ Ok(())
+ };
+
+ let digit = |pos: usize| -> Result<i32, Error> {
+ let digit = input[pos] as i32;
+ if !(48..=57).contains(&digit) {
+ bail!("unexpected char at pos {pos}");
+ }
+ Ok(digit - 48)
+ };
+
+ fn check_max(i: i32, max: i32) -> Result<i32, Error> {
+ if i > max {
+ bail!("value too large ({i} > {max})");
+ }
+ Ok(i)
+ }
+
+ if input.len() < 20 || input.len() > 25 {
+ bail!("timestamp of unexpected length");
+ }
+
+ if b'.' != input[19] {
+ bail!("unexpected milliseconds separator");
+ }
+ let tz = input[23];
+
+ match tz {
+ b'Z' => {
+ if input.len() != 24 {
+ bail!("unexpected length in UTC timestamp");
+ }
+ }
+ b'+' | b'-' => {
+ if input.len() != 29 {
+ bail!("unexpected length in timestamp");
+ }
+ }
+ _ => bail!("unexpected timezone indicator"),
+ }
+
+ let mut tm = proxmox_time::TmEditor::new(true);
+
+ tm.set_year(digit(0)? * 1000 + digit(1)? * 100 + digit(2)? * 10 + digit(3)?)?;
+ expect(4, b'-')?;
+ tm.set_mon(check_max(digit(5)? * 10 + digit(6)?, 12)?)?;
+ expect(7, b'-')?;
+ tm.set_mday(check_max(digit(8)? * 10 + digit(9)?, 31)?)?;
+
+ expect(10, b'T')?;
+
+ tm.set_hour(check_max(digit(11)? * 10 + digit(12)?, 23)?)?;
+ expect(13, b':')?;
+ tm.set_min(check_max(digit(14)? * 10 + digit(15)?, 59)?)?;
+ expect(16, b':')?;
+ tm.set_sec(check_max(digit(17)? * 10 + digit(18)?, 60)?)?;
+ expect(19, b'.')?;
+ let milliseconds: u64 = String::from_utf8(input[20..23].to_vec())?.parse()?;
+
+ let epoch = tm.into_epoch()?;
+
+ if tz == b'Z' {
+ return Ok(Self {
+ epoch,
+ milliseconds,
+ });
+ }
+
+ let hours = check_max(digit(20)? * 10 + digit(21)?, 23)?;
+ expect(22, b':')?;
+ let mins = check_max(digit(23)? * 10 + digit(24)?, 59)?;
+
+ let offset = (hours * 3600 + mins * 60) as i64;
+
+ let epoch = match tz {
+ b'+' => epoch - offset,
+ b'-' => epoch + offset,
+ _ => unreachable!(), // already checked above
+ };
+
+ Ok(Self {
+ epoch,
+ milliseconds,
+ })
+ }
+}
+
+serde_plain::derive_deserialize_from_fromstr!(LastModifiedTimestamp, "last modified timestamp");
--
2.39.5
More information about the pbs-devel
mailing list