From acafad962b5eea4772479d7f44912038d71e528e Mon Sep 17 00:00:00 2001 From: tv Date: Mon, 9 Nov 2026 03:22:54 +0000 Subject: remake: replayWindow and archiveRetention in an octet-stream import's query An import uploaded as `application/octet-stream` takes `rules`, `baseHeads`, `settle` and `switchDelay` in the query, but not the retentions: for those the document had to go base64-encoded in the JSON form. Now `replayWindow` and `archiveRetention` go in the query too, as JSON like the rules (`{"epochs":1,"age":"7d"}`, as in the body). --- src/remake/api.rs | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/remake/api.rs b/src/remake/api.rs index a767706..a4d4cf0 100644 --- a/src/remake/api.rs +++ b/src/remake/api.rs @@ -119,6 +119,10 @@ pub(crate) struct StartQuery { settle: Option, #[serde(default, deserialize_with = "crate::units::optional_duration")] switch_delay: Option, + /// As JSON (as in the body), e.g. `{"epochs":1,"age":"7d"}`. + replay_window: Option, + /// As JSON (as in the body). + archive_retention: Option, } impl StartQuery { @@ -127,6 +131,8 @@ impl StartQuery { || self.base_heads.is_some() || self.settle.is_some() || self.switch_delay.is_some() + || self.replay_window.is_some() + || self.archive_retention.is_some() } /// An import request for a document uploaded as is. @@ -136,6 +142,15 @@ impl StartQuery { Some(rules) => serde_json::from_str(rules) .map_err(|error| format!("rules are not valid JSON: {error}"))?, }; + let retention = |given: &Option, name: &str| { + given + .as_deref() + .map(|given| { + serde_json::from_str::(given) + .map_err(|error| format!("{name} is not a valid retention: {error}")) + }) + .transpose() + }; Ok(StartRequest { kind: Some("import".to_owned()), spec: Some(json!({ @@ -151,7 +166,8 @@ impl StartQuery { }), settle: self.settle, switch_delay: self.switch_delay, - ..StartRequest::default() + replay_window: retention(&self.replay_window, "replayWindow")?, + archive_retention: retention(&self.archive_retention, "archiveRetention")?, }) } } @@ -344,7 +360,7 @@ async fn start_kind( } else if query.has_request_fields() { return error_response( StatusCode::BAD_REQUEST, - "rules, baseHeads, settle and switchDelay go in the query only for an application/octet-stream import; put them in the JSON body", + "rules, baseHeads, settle, switchDelay, replayWindow and archiveRetention go in the query only for an application/octet-stream import; put them in the JSON body", None, ); } else if body.is_empty() { @@ -997,3 +1013,35 @@ pub(crate) mod testing { .collect() } } + +#[cfg(test)] +mod tests { + use super::*; + + /// An octet-stream import takes the retentions from the query, as JSON. + #[test] + fn import_takes_retentions_from_the_query() { + let query = StartQuery { + replay_window: Some(r#"{"epochs":2,"age":"1h"}"#.to_owned()), + archive_retention: Some(r#"{"epochs":3}"#.to_owned()), + ..StartQuery::default() + }; + assert!(query.has_request_fields()); + let request = query.import_request(b"").unwrap(); + let replay = Retention::from(request.replay_window.unwrap()); + assert_eq!(replay.epochs, Some(2)); + assert_eq!(replay.age_ms, Some(3_600_000)); + let archive = Retention::from(request.archive_retention.unwrap()); + assert_eq!((archive.epochs, archive.age_ms), (Some(3), None)); + + let bad = StartQuery { + replay_window: Some(r#"{"days":2}"#.to_owned()), + ..StartQuery::default() + }; + assert!( + bad.import_request(b"") + .unwrap_err() + .contains("replayWindow") + ); + } +} -- cgit v1.3.1