//! `GET /api/export`: the whole vault as a zip archive, built by the daemon //! so clients never open the vault themselves. use axum::{ body::Body, extract::State, http::header, response::{IntoResponse, Response}, }; use fil_core::Kasten; use tokio_stream::StreamExt as _; use tokio_util::io::ReaderStream; use tracing::instrument; use super::{FilState, error::ApiError}; /// Header carrying how many files the archive holds, database included. pub const FILES_HEADER: &str = "x-fil-export-files"; /// Builds the archive in a temp dir under the read lock (so files and /// database are a consistent snapshot), then streams it back. The temp dir /// lives as long as the response body. #[instrument(skip(state))] pub async fn export(State(state): State) -> Result { let dir = tempfile::Builder::new().prefix("fil-export-").tempdir()?; let name = Kasten::default_export_name(); let kt = state.kh.read().await; let summary = kt.export(dir.path().join(&name)).await?; drop(kt); let file = tokio::fs::File::open(&summary.path).await?; let len = file.metadata().await?.len(); let body = ReaderStream::new(file).map(move |chunk| { // keep the archive on disk until it has been sent let _ = &dir; chunk }); Ok(( [ (header::CONTENT_TYPE, "application/zip".to_owned()), (header::CONTENT_LENGTH, len.to_string()), ( header::CONTENT_DISPOSITION, format!("attachment; filename=\"{name}\""), ), ( header::HeaderName::from_static(FILES_HEADER), summary.files.to_string(), ), ], Body::from_stream(body), ) .into_response()) }