aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-09 03:22:54 +0000
committertv <tv@krebsco.de>2026-11-09 04:27:44 +0100
commitacafad962b5eea4772479d7f44912038d71e528e (patch)
tree980fa159c58e10c2d2fa0f073363b9b8acd308cd
parent3c1403409877b0006594324a0daa5e9dd55ec3f4 (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.yaml10
-rw-r--r--docs/remakes.md7
-rw-r--r--src/remake/api.rs52
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")
+ );
+ }
+}