use axum::extract::{Path, Query, State};
use axum::http::{HeaderMap, HeaderValue, header};
use axum::response::IntoResponse;
use crate::db::DbPool;
use crate::error::LificError;
use super::with_read;
#[derive(serde::Deserialize)]
pub(super) struct ProjectExportQuery {
pub format: Option<String>,
}
fn content_disposition(filename: &str) -> Result<HeaderValue, LificError> {
HeaderValue::from_str(&format!("attachment; filename=\"{filename}\""))
.map_err(|e| LificError::Internal(format!("invalid content-disposition header: {e}")))
}
pub(super) async fn export_issue(
State(db): State<DbPool>,
Path(identifier): Path<String>,
) -> Result<impl IntoResponse, LificError> {
let bundle = with_read(&db, |conn| crate::export::export_issue(conn, &identifier))?;
let file = bundle
.files
.into_iter()
.next()
.ok_or_else(|| LificError::Internal("issue export produced no files".into()))?;
let filename = file.path.rsplit('/').next().unwrap_or("issue.md").to_string();
let mut headers = HeaderMap::new();
headers.insert(header::CONTENT_TYPE, HeaderValue::from_static("text/markdown; charset=utf-8"));
headers.insert(header::CONTENT_DISPOSITION, content_disposition(&filename)?);
Ok((headers, file.content))
}
pub(super) async fn export_page(
State(db): State<DbPool>,
Path(identifier): Path<String>,
) -> Result<impl IntoResponse, LificError> {
let bundle = with_read(&db, |conn| crate::export::export_page(conn, &identifier))?;
let file = bundle
.files
.into_iter()
.next()
.ok_or_else(|| LificError::Internal("page export produced no files".into()))?;
let filename = file.path.rsplit('/').next().unwrap_or("page.md").to_string();
let mut headers = HeaderMap::new();
headers.insert(header::CONTENT_TYPE, HeaderValue::from_static("text/markdown; charset=utf-8"));
headers.insert(header::CONTENT_DISPOSITION, content_disposition(&filename)?);
Ok((headers, file.content))
}
pub(super) async fn export_project(
State(db): State<DbPool>,
Path(identifier): Path<String>,
Query(q): Query<ProjectExportQuery>,
) -> Result<impl IntoResponse, LificError> {
let format = q.format.as_deref().unwrap_or("zip");
let bundle = with_read(&db, |conn| crate::export::export_project(conn, &identifier))?;
match format {
"json" => Ok(axum::Json(bundle).into_response()),
"zip" => {
let filename = format!("{}-export.zip", bundle.root.to_ascii_lowercase());
let bytes = crate::export::bundle_to_zip(&bundle)?;
let mut headers = HeaderMap::new();
headers.insert(header::CONTENT_TYPE, HeaderValue::from_static("application/zip"));
headers.insert(header::CONTENT_DISPOSITION, content_disposition(&filename)?);
Ok((headers, bytes).into_response())
}
_ => Err(LificError::BadRequest(
"invalid export format. Expected 'zip' or 'json'".into(),
)),
}
}
#[cfg(test)]
mod tests {
use axum::http::{Request, StatusCode};
use http_body_util::BodyExt;
use tower::ServiceExt;
use crate::api::test_helpers::{json_post, parse_json, seed_project, test_app};
#[tokio::test]
async fn export_issue_returns_markdown_attachment() {
let app = test_app();
let (project_id, _) = seed_project(&app).await;
let created = parse_json(
json_post(
&app,
"/api/issues",
serde_json::json!({
"project_id": project_id,
"title": "Export me",
"description": "Body"
}),
)
.await,
)
.await;
let resp = app
.clone()
.oneshot(
Request::builder()
.uri(format!("/api/export/issues/{}", created["identifier"].as_str().unwrap()))
.body(axum::body::Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers()[axum::http::header::CONTENT_TYPE],
"text/markdown; charset=utf-8"
);
let body = resp.into_body().collect().await.unwrap().to_bytes();
let body = String::from_utf8(body.to_vec()).unwrap();
assert!(body.contains("identifier: TST-1"));
assert!(body.contains("# Export me"));
}
#[tokio::test]
async fn export_project_returns_zip_attachment() {
let app = test_app();
let (project_id, project) = seed_project(&app).await;
json_post(
&app,
"/api/issues",
serde_json::json!({
"project_id": project_id,
"title": "Export project"
}),
)
.await;
let resp = app
.clone()
.oneshot(
Request::builder()
.uri(format!("/api/export/projects/{}", project["identifier"].as_str().unwrap()))
.body(axum::body::Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(resp.headers()[axum::http::header::CONTENT_TYPE], "application/zip");
}
}