//! `GET /admin/document`: the local document as Automerge bytes, for backups and as //! input to an `import` remake. //! //! The whole document is saved: history, and protocol data under `_continuum`. Where //! it came from is in headers: `X-Continuum-Swarm`, `X-Continuum-Epoch` //! (`:`) and `X-Continuum-Heads` (hex, comma-separated). use axum::{ extract::State, http::{HeaderName, HeaderValue, StatusCode, header}, response::{IntoResponse, Response}, }; use tokio::time::Instant; use tracing::{info, warn}; use crate::state::AppState; pub(crate) async fn get_document(State(state): State) -> Response { if !state.is_ready().await { return (StatusCode::SERVICE_UNAVAILABLE, "node is not ready\n").into_response(); } let swarm_id = state.swarm_id().await.unwrap_or_default(); let start = Instant::now(); let version = crate::swarm::stats::document_version(&state); let doc = std::sync::Arc::clone(&state.doc); // Both steps take time proportional to the document: off the async threads, and // the lock only for the copy. let saved = tokio::task::spawn_blocking(move || { let (mut copy, epoch) = { let replica = doc.blocking_read(); (replica.doc.clone(), replica.epoch) }; let heads = copy.get_heads(); (copy.save(), heads, epoch) }) .await; let (bytes, heads, epoch) = match saved { Ok(saved) => saved, Err(error) => { warn!(error = %error, "Saving the document for export failed"); return (StatusCode::INTERNAL_SERVER_ERROR, "saving failed\n").into_response(); } }; let took = start.elapsed(); state .stats .saved_size_measured(bytes.len() as u64, took, version); info!( bytes = bytes.len(), took_ms = took.as_millis() as u64, "Exported the document" ); let heads = heads .iter() .map(|head| head.to_string()) .collect::>() .join(","); let filename = format!( "attachment; filename=\"continuum-{swarm_id}-{}.automerge\"", epoch.generation ); let mut response = bytes.into_response(); let headers = response.headers_mut(); let mut set = |name: HeaderName, value: String| { if let Ok(value) = HeaderValue::from_str(&value) { headers.insert(name, value); } }; set(header::CONTENT_TYPE, "application/octet-stream".to_owned()); set(header::CONTENT_DISPOSITION, filename); set(HeaderName::from_static("x-continuum-swarm"), swarm_id); set( HeaderName::from_static("x-continuum-epoch"), format!("{}:{}", epoch.generation, epoch.id_hex()), ); set(HeaderName::from_static("x-continuum-heads"), heads); response }