pubmed_client/europe_pmc/
supplementary.rs

1//! Europe PMC `supplementaryFiles` endpoint operations (native only).
2//!
3//! Europe PMC returns supplementary materials as a single ZIP archive. These
4//! helpers fetch that archive either into memory or onto disk; ZIP extraction is
5//! left to the caller to avoid pulling in an archive dependency.
6
7use std::path::{Path, PathBuf};
8
9use tokio::fs as tokio_fs;
10use tracing::{info, instrument};
11
12use pubmed_parser::ParseError;
13
14use crate::error::{PubMedError, Result};
15
16use super::client::EuropePmcClient;
17use super::id::EuropePmcId;
18
19impl EuropePmcClient {
20    /// Fetch the supplementary-files ZIP archive for a record into memory.
21    ///
22    /// Returns the raw bytes of the ZIP. As with full text, Europe PMC
23    /// addresses this endpoint by the record id alone
24    /// (`/{id}/supplementaryFiles`) rather than by `(source, id)`.
25    #[instrument(skip(self), fields(id = %id))]
26    pub async fn fetch_supplementary_files(&self, id: &EuropePmcId) -> Result<Vec<u8>> {
27        let endpoint = format!("{}/supplementaryFiles", id.id);
28        let response = self
29            .executor()
30            .get_endpoint(&self.base_url, &endpoint, &[])
31            .await?;
32        let bytes = response.bytes().await.map_err(PubMedError::from)?;
33        Ok(bytes.to_vec())
34    }
35
36    /// Download the supplementary-files ZIP archive for a record to `output_path`.
37    ///
38    /// `output_path` is the full path of the ZIP file to write. Parent
39    /// directories are created if needed. Returns the written path.
40    #[instrument(skip(self, output_path), fields(id = %id))]
41    pub async fn download_supplementary_files(
42        &self,
43        id: &EuropePmcId,
44        output_path: impl AsRef<Path>,
45    ) -> Result<PathBuf> {
46        let output_path = output_path.as_ref().to_path_buf();
47        if let Some(parent) = output_path.parent() {
48            tokio_fs::create_dir_all(parent)
49                .await
50                .map_err(|e| ParseError::IoError {
51                    message: format!("failed to create directory {}: {e}", parent.display()),
52                })?;
53        }
54
55        let bytes = self.fetch_supplementary_files(id).await?;
56        tokio_fs::write(&output_path, &bytes)
57            .await
58            .map_err(|e| ParseError::IoError {
59                message: format!("failed to write {}: {e}", output_path.display()),
60            })?;
61
62        info!(id = %id, path = %output_path.display(), bytes = bytes.len(), "Downloaded supplementary files");
63        Ok(output_path)
64    }
65}