From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: <pbs-devel-bounces@lists.proxmox.com> Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 5098C1FF17F for <inbox@lore.proxmox.com>; Mon, 19 May 2025 13:47:13 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 0866482F5; Mon, 19 May 2025 13:47:09 +0200 (CEST) From: Christian Ebner <c.ebner@proxmox.com> To: pbs-devel@lists.proxmox.com Date: Mon, 19 May 2025 13:46:09 +0200 Message-Id: <20250519114640.303640-9-c.ebner@proxmox.com> X-Mailer: git-send-email 2.39.5 In-Reply-To: <20250519114640.303640-1-c.ebner@proxmox.com> References: <20250519114640.303640-1-c.ebner@proxmox.com> MIME-Version: 1.0 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.029 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: [pbs-devel] [RFC proxmox-backup 08/39] s3 client: add helper for last modified timestamp parsing X-BeenThere: pbs-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Backup Server development discussion <pbs-devel.lists.proxmox.com> List-Unsubscribe: <https://lists.proxmox.com/cgi-bin/mailman/options/pbs-devel>, <mailto:pbs-devel-request@lists.proxmox.com?subject=unsubscribe> List-Archive: <http://lists.proxmox.com/pipermail/pbs-devel/> List-Post: <mailto:pbs-devel@lists.proxmox.com> List-Help: <mailto:pbs-devel-request@lists.proxmox.com?subject=help> List-Subscribe: <https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel>, <mailto:pbs-devel-request@lists.proxmox.com?subject=subscribe> Reply-To: Proxmox Backup Server development discussion <pbs-devel@lists.proxmox.com> Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: pbs-devel-bounces@lists.proxmox.com Sender: "pbs-devel" <pbs-devel-bounces@lists.proxmox.com> 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@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 _______________________________________________ pbs-devel mailing list pbs-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel