discovery-connect 1.0.1

Library to upload data to RetinAI Discovery via the public API.
Documentation
// Copyright 2023 Ikerian AG
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use crate::file::{create_file, register_uploaded_file, stream_to_s3};
use crate::query::QueryClient;
use std::io::{Error, ErrorKind};
use std::path::Path;
use std::sync::Arc;
use tempfile;
use zip;

/// Uploads a file to a specified Discovery account.
///
/// This function takes a file path and uploads the file to an S3 bucket using a signed URL. It first creates a file
/// record in the system database, retrieves a unique file UUID and a signed URL for the upload, and then streams the
/// file to S3. After a successful upload, the file's status is updated in the database. If the upload fails, the function
/// attempts to register the failure in the system.
///
/// # Arguments
///
/// * `qc` - An `Arc<QueryClient>` instance for API operations.
/// * `workbook_uuid` - A `String` representing the UUID of the workbook that will store the file.
/// * `file` - A `String` representing the path to the file being uploaded.
///
/// # Returns
///
/// A `Result<String, Box<dyn std::error::Error>>` which is `Ok(String)` with the file UUID if the upload and registration
/// succeed, or `Err` with the appropriate error if any step of the process fails.
///
/// # Errors
///
/// This function returns an error if:
/// - The filename extraction fails.
/// - The call to create a file record in the system fails.
/// - No UUID is returned for the file.
/// - No signed URL is returned for the upload.
/// - The upload to S3 fails.
/// - Registering the uploaded file (either as successful or failed) in the system fails.
///
/// # Example
///
/// ```
/// use discovery_connect::{upload_file, QueryClient};
/// use std::sync::Arc;
///
/// async fn example() {
///     let timeout = std::time::Duration::from_secs(1);
///     let qc = Arc::new(QueryClient::new(
///         "https://api.example.discovery.retinai.com",
///         "client_id",
///         "client_secret",
///         "user@example.com",
///         "password123",
///         timeout));
///     match upload_file(qc, "workbook123", "path/to/file.txt", None).await {
///         Ok(file_uuid) => println!("File uploaded successfully with UUID: {}", file_uuid),
///         Err(e) => eprintln!("Error uploading file: {:?}", e),
///     }
/// }
/// ```
pub async fn upload_file(
    qc: Arc<QueryClient>,
    workbook_uuid: &str,
    file: &str,
    acquisition_name: Option<&str>,
) -> Result<String, Box<dyn std::error::Error>> {
    let path = Path::new(file);
    let filename = match &acquisition_name {
        Some(acquisition_name) => acquisition_name,
        None => path
            .file_name()
            .ok_or("Filename not found")?
            .to_str()
            .ok_or("Filename conversion error")?,
    };

    let create_file_response = create_file(qc.clone(), workbook_uuid, filename).await?;
    let create_file_result = create_file_response
        .create_file
        .ok_or("createFile returned None")?;

    let file_uuid = create_file_result.uuid.ok_or("No file UUID returned")?;
    let signed_url = create_file_result
        .signed_upload_url
        .ok_or("No signed URL returned")?;

    if let Err(e) = stream_to_s3(qc.clone(), &signed_url, file).await {
        eprintln!("  upload error: {:?}", e);
        if let Err(cancel_error) = register_uploaded_file(qc.clone(), &file_uuid, Some(true)).await
        {
            eprintln!("  upload cancel error: {:?}", cancel_error);
        }
        return Err(e);
    }

    let register_result = register_uploaded_file(qc, &file_uuid, None).await?;
    let registered_file_uuid = register_result
        .register_uploaded_file
        .ok_or("registerUploadedFile returned None")?
        .uuid;

    Ok(registered_file_uuid
        .ok_or_else(|| Box::new(Error::new(ErrorKind::Other, "No file UUID returned")))?)
}

/// Creates a zip file containing a list of files and uploads it as a single acquisition.
///
/// Use this to upload and import multi-file acquisitions atomically.
/// This ensures that DICOM OPT-OP-OPTBSV-SEG objects are linked correctly for display.
///
/// # Arguments
///
/// * `qc` - An `Arc<QueryClient>` instance for API operations.
/// * `workbook_uuid` - A `String` representing the UUID of the workbook that will store the file.
/// * `files` - A `Vec<String>` representing the paths to the files being uploaded.
///
/// # Returns
///
/// A `Result<String, Box<dyn std::error::Error>>` which is `Ok(String)` with the file UUID if the upload and registration
/// succeed, or `Err` with the appropriate error if any step of the process fails.
///
/// # Errors
///
/// This function returns an error if:
/// - The filename extraction fails.
/// - The call to create a file record in the system fails.
/// - No UUID is returned for the file.
/// - No signed URL is returned for the upload.
/// - The upload to S3 fails.
/// - Registering the uploaded file (either as successful or failed) in the system fails.
///
/// # Example
///
/// ```
/// use discovery_connect::{upload_acquisition, QueryClient};
/// use std::sync::Arc;
///
/// async fn example() {
///    let timeout = std::time::Duration::from_secs(1);
///    let qc = Arc::new(QueryClient::new(
///        "https://api.example.discovery.retinai.com",
///        "client_id",
///        "client_secret",
///        "user@example.com",
///        "password123",
///        timeout));
///    match upload_acquisition(qc, "workbook123", &["path/to/file1.dcm", "path/to/file2.dcm"], "example").await {
///        Ok(file_uuid) => println!("File uploaded successfully with UUID: {}", file_uuid),
///        Err(e) => eprintln!("Error uploading file: {:?}", e),
///   }
/// }
/// ```
pub async fn upload_acquisition(
    qc: Arc<QueryClient>,
    workbook_uuid: &str,
    files: &[&str],
    acquisition_name: &str,
) -> Result<String, Box<dyn std::error::Error>> {
    // create a temporary zip archive in the temp directory
    // and append the input files one by one
    let temp_file = tempfile::NamedTempFile::new()?;
    let path = Path::new(temp_file.path());
    println!("Creating zip archive at {:?}", path);
    let mut zip = zip::ZipWriter::new(&temp_file);
    let options = zip::write::FileOptions::default()
        .compression_method(zip::CompressionMethod::Stored)
        .unix_permissions(0o755);

    for file in files.iter() {
        let path = Path::new(file);
        let filename = path
            .file_name()
            .ok_or("Filename not found")?
            .to_str()
            .ok_or("Filename conversion error")?;
        zip.start_file(filename, options)?;
        let mut file = std::fs::File::open(file)?;
        std::io::copy(&mut file, &mut zip)?;
    }

    // flush the zip archive to disk
    zip.finish()?;

    // upload the zip archive
    upload_file(
        qc,
        workbook_uuid,
        path.to_str().unwrap(),
        Some(acquisition_name),
    )
    .await
}