use std::sync::Arc;
use axum::body::Body;
use axum::http::header::{CONTENT_DISPOSITION, CONTENT_TYPE};
use axum::response::Response;
use sbol_db_app::Downloader;
use sbol_db_core::{DomainError, SerializationFormat, Triple};
use sbol_db_sparql::GraphScope;
use crate::error::ApiError;
use crate::serialize::{
serialize_closure, serialize_gff3, serialize_omex, OmexAttachment, OmexAttachmentSource,
Serialized,
};
use crate::v2::error::V2Error;
use crate::AppState;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DownloadFormat {
Sbol,
SbolNonRecursive,
GenBank,
Fasta,
Gff3,
Omex,
}
impl DownloadFormat {
pub fn parse(value: &str) -> Option<Self> {
match value.to_ascii_lowercase().as_str() {
"sbol" => Some(Self::Sbol),
"sbolnr" => Some(Self::SbolNonRecursive),
"gb" | "genbank" => Some(Self::GenBank),
"fasta" => Some(Self::Fasta),
"gff" | "gff3" => Some(Self::Gff3),
"omex" => Some(Self::Omex),
_ => None,
}
}
fn extension(self) -> &'static str {
match self {
Self::Sbol | Self::SbolNonRecursive => "xml",
Self::GenBank => "gb",
Self::Fasta => "fasta",
Self::Gff3 => "gff",
Self::Omex => "omex",
}
}
}
pub async fn render_download(
state: &AppState,
uri: &str,
display_id: &str,
format: DownloadFormat,
sbol2: bool,
scope: GraphScope,
) -> Result<Response, V2Error> {
let triples = fetch_closure(state, uri, format, scope).await?;
if triples.is_empty() {
return Err(V2Error::from(ApiError::NotFound(format!("object {uri}"))));
}
let attachments = match format {
DownloadFormat::Omex => Some(PrefetchedAttachments(omex_members(state, &triples).await?)),
_ => None,
};
let serialized = render(
&triples,
format,
sbol2,
attachments.as_ref().map(|a| a as &dyn OmexAttachmentSource),
)?;
attachment(serialized, display_id, format.extension())
}
async fn fetch_closure(
state: &AppState,
uri: &str,
format: DownloadFormat,
scope: GraphScope,
) -> Result<Vec<Triple>, V2Error> {
let resolver = Arc::new(state.app.federation());
let downloader = Downloader::new(state.app.sparql.clone())
.with_database_prefix(state.app.registry_namespace.database_prefix())
.with_remote_resolver(resolver);
let triples = match format {
DownloadFormat::SbolNonRecursive => downloader.fetch_non_recursive(uri, scope).await?,
_ => downloader.fetch_recursive(uri, scope).await?,
};
Ok(triples)
}
fn render(
triples: &[Triple],
format: DownloadFormat,
sbol2: bool,
attachments: Option<&dyn OmexAttachmentSource>,
) -> Result<Serialized, DomainError> {
match format {
DownloadFormat::Sbol | DownloadFormat::SbolNonRecursive => {
serialize_closure(triples, SerializationFormat::RdfXml, sbol2)
}
DownloadFormat::GenBank => serialize_closure(triples, SerializationFormat::GenBank, false),
DownloadFormat::Fasta => serialize_closure(triples, SerializationFormat::Fasta, false),
DownloadFormat::Gff3 => serialize_gff3(triples),
DownloadFormat::Omex => serialize_omex(triples, sbol2, attachments),
}
}
struct PrefetchedAttachments(Vec<OmexAttachment>);
impl OmexAttachmentSource for PrefetchedAttachments {
fn attachments_for(&self, _triples: &[Triple]) -> Result<Vec<OmexAttachment>, DomainError> {
Ok(self.0.clone())
}
}
async fn omex_members(
state: &AppState,
triples: &[Triple],
) -> Result<Vec<OmexAttachment>, V2Error> {
let mut members = Vec::new();
for uri in sbol_db_app::attachment_uris(triples) {
let Some(attachment) = sbol_db_app::read_attachment(triples, &uri) else {
continue;
};
let Some(hash) = attachment.hash else {
continue;
};
let Some(bytes) = state.app.blobs.get(&hash).await? else {
continue;
};
let filename = attachment
.name
.filter(|s| !s.is_empty())
.unwrap_or_else(|| uri.rsplit('/').next().unwrap_or("attachment").to_owned());
let format = attachment
.format
.unwrap_or_else(|| "application/octet-stream".to_owned());
members.push(OmexAttachment {
filename,
format,
bytes,
});
}
Ok(members)
}
fn attachment(
serialized: Serialized,
display_id: &str,
extension: &str,
) -> Result<Response, V2Error> {
let filename = format!("{}.{extension}", sanitize_filename(display_id));
Response::builder()
.header(CONTENT_TYPE, serialized.content_type)
.header(
CONTENT_DISPOSITION,
format!("attachment; filename=\"{filename}\""),
)
.header("Access-Control-Expose-Headers", "Content-Disposition")
.body(Body::from(serialized.bytes))
.map_err(|e| V2Error::from(ApiError::Domain(DomainError::Serialization(e.to_string()))))
}
fn sanitize_filename(display_id: &str) -> String {
display_id
.chars()
.map(|c| match c {
'"' | '\\' | '/' => '_',
other => other,
})
.collect()
}