diff options
| author | tv <tv@krebsco.de> | 2026-11-09 03:22:54 +0000 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-11-09 04:27:44 +0100 |
| commit | acafad962b5eea4772479d7f44912038d71e528e (patch) | |
| tree | 980fa159c58e10c2d2fa0f073363b9b8acd308cd | |
| parent | 3c1403409877b0006594324a0daa5e9dd55ec3f4 (diff) | |
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).
| -rw-r--r-- | docs/openapi.yaml | 10 | ||||
| -rw-r--r-- | docs/remakes.md | 7 | ||||
| -rw-r--r-- | src/remake/api.rs | 52 |
3 files changed, 64 insertions, 5 deletions
diff --git a/docs/openapi.yaml b/docs/openapi.yaml index efda5e2..8e6a688 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -1428,6 +1428,16 @@ paths: description: Octet-stream import only; a duration. schema: type: string + - name: replayWindow + in: query + description: 'Octet-stream import only: the retention as JSON, as in the body, e.g. `{"epochs":1,"age":"7d"}`.' + schema: + type: string + - name: archiveRetention + in: query + description: Octet-stream import only, as `replayWindow`. + schema: + type: string requestBody: content: application/json: diff --git a/docs/remakes.md b/docs/remakes.md index ccccb1a..6f4b740 100644 --- a/docs/remakes.md +++ b/docs/remakes.md @@ -173,9 +173,10 @@ curl -X POST localhost:3000/admin/remakes \ --url-query 'rules=[{"from":"","to":""}]' ``` -With `application/octet-stream`, `rules`, `baseHeads`, `settle` and -`switchDelay` go in the query; for the retention settings use the JSON form, with -the document base64-encoded: +With `application/octet-stream`, `rules`, `baseHeads`, `settle`, `switchDelay`, +`replayWindow` and `archiveRetention` go in the query, the rules and the retentions as +JSON (`--url-query 'replayWindow={"epochs":2}'`). The JSON form takes the document +base64-encoded: ```sh base64 -w0 backup.automerge | 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<Duration>, #[serde(default, deserialize_with = "crate::units::optional_duration")] switch_delay: Option<Duration>, + /// As JSON (as in the body), e.g. `{"epochs":1,"age":"7d"}`. + replay_window: Option<String>, + /// As JSON (as in the body). + archive_retention: Option<String>, } 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<String>, name: &str| { + given + .as_deref() + .map(|given| { + serde_json::from_str::<RetentionRequest>(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<H: Host>( } 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") + ); + } +} |
