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::disco_api;
use crate::query::{post_query, QueryClient};
use graphql_client::GraphQLQuery;
use serde_json::Value as JSON;
use std::sync::Arc;

disco_api!(
    CreateFile,
    "createFile",
    "src/api/graphql/CreateFile.graphql"
);
disco_api!(
    RegisterUploadedFile,
    "registerUploadedFile",
    "src/api/graphql/RegisterUploadedFile.graphql"
);

/// Creates a new file entry in the specified workbook and returns a signed URL for uploading the file.
///
/// # Arguments
///
/// * `qc` - An `Arc<QueryClient>` object that represents the connection to the Connect server.
/// * `workbook_uuid` - The UUID of the workbook in which to create the file.
/// * `path` - The path to the file, relative to the workbook's root directory.
///
/// # Returns
///
/// A `Result` containing either the response data on success (`create_file::ResponseData`),
/// or an `reqwest::Error` on failure. The response data contains:
/// - the UUID of the newly created file
/// - a signed URL for uploading the file
///
/// # Errors
///
/// Returns an `Err` of type `reqwest::Error` if the file creation request fails or if the server
/// response status is not OK.
///
/// # Example
///
/// ```
/// use discovery_connect::file::{CreateFile, create_file};
/// use discovery_connect::{QueryClient};
/// use std::sync::Arc;
///
/// let qc = Arc::new(QueryClient::new(
///         "https://api.example.discovery.retinai.com",
///         "client_id",
///         "client_secret",
///         "user@example",
///         "password123",
///         std::time::Duration::from_secs(10)));
/// async {
///     let res = create_file(qc, "123e4567-e89b-12d3-a456-426614174000", "path/to/file").await;
///     match res {
///         Ok(data) => println!("File created: {}", data.create_file.unwrap().uuid.unwrap()),
///         Err(e) => println!("Error: {:?}", e),
///     };
/// };
///
/// ```
pub async fn create_file(
    qc: Arc<QueryClient>,
    workbook_uuid: &str,
    path: &str,
) -> Result<create_file::ResponseData, reqwest::Error> {
    let input = create_file::CreateFileInput {
        uuid: None,
        filename: path.to_string(),
        tags: Some(vec![]),
        remarks: Some(serde_json::Value::Object(serde_json::Map::new())),
        overwrite: Some(serde_json::Value::Object(serde_json::Map::new())),
        workbook_uuid: Some(workbook_uuid.to_string()),
    };
    let variables = create_file::Variables { input };
    post_query::<CreateFile>(qc, variables).await
}

/// Registers the status of an uploaded file in the system, marking it as successfully uploaded or failed.
///
/// If successful, the server will start processing the file.
///
/// # Arguments
///
/// * `qc` - An `Arc<QueryClient>` object representing the connection to the Connect server.
/// * `uuid` - A String reference representing the UUID of the file whose upload status is being registered.
/// * `upload_failed` - An `Option<bool>` indicating whether the upload failed (Some(true)) or succeeded (Some(false) or None).
///
/// # Returns
///
/// A `Result` containing either the response data on success (`register_uploaded_file::ResponseData`),
/// or an `reqwest::Error` on failure. The response data typically includes confirmation of the registration.
///
/// # Errors
///
/// Returns an `Err` of type `reqwest::Error` if the registration request fails or if the server
/// response status is not OK.
///
/// # Example
///
/// ```
/// use discovery_connect::file::{RegisterUploadedFile, register_uploaded_file};
/// use discovery_connect::{QueryClient};
/// use std::sync::Arc;
///
/// let qc = Arc::new(QueryClient::new(
///     "https://api.example.discovery.retinai.com",
///     "client_id",
///     "client_secret",
///     "user@example",
///     "password123",
///     std::time::Duration::from_secs(10)));
/// let file_uuid = "123e4567-e89b-12d3-a456-426614174000".to_string();
/// let upload_status = Some(false);
/// async {
///     let res = register_uploaded_file(qc.clone(), &file_uuid, upload_status).await;
///     match res {
///         Ok(response) => println!("File registration successful"),
///         Err(e) => println!("Error registering file: {}", e),
///     }
/// };
/// ```
pub async fn register_uploaded_file(
    qc: Arc<QueryClient>,
    uuid: &String,
    upload_failed: Option<bool>,
) -> Result<register_uploaded_file::ResponseData, reqwest::Error> {
    let variables = register_uploaded_file::Variables {
        uuid: uuid.to_string(),
        upload_failed: Some(upload_failed.unwrap_or_default()),
    };
    post_query::<RegisterUploadedFile>(qc, variables).await
}

/// Streams a file to AWS S3 using a pre-signed URL.
///
/// # Arguments
///
/// * `qc` - An `Arc<QueryClient>` object that represents the connection to the Connect server.
/// * `signed_url` - The pre-signed URL provided by AWS S3 for file upload.
/// * `filename` - The local path to the file that needs to be uploaded.
///
/// # Returns
///
/// A `Result` containing either a `reqwest::Response` on success, or a `Box<dyn std::error::Error>` on failure.
/// The successful response indicates that the file has been successfully uploaded to the specified S3 location.
///
/// # Errors
///
/// Returns an `Err` of type `Box<dyn std::error::Error>` if there's an error in reading the file, preparing the HTTP request,
/// or if the server response status is not OK. Includes a `reqwest::Error` if the upload fails.
///
/// # Notes
///
/// Currently, this function has a known issue with streaming from disk to S3 (returns a 501 error).
/// This needs investigation and fixing. It works correctly in the C# client implementation.
///
/// # Example
///
/// ```
/// use discovery_connect::file::{create_file, stream_to_s3};
/// use discovery_connect::{QueryClient};
/// use std::sync::Arc;
///
/// let qc = Arc::new(QueryClient::new(
///     "https://api.example.discovery.retinai.com",
///     "client_id",
///     "client_secret",
///     "user@example",
///     "password123",
///     std::time::Duration::from_secs(10)));
/// async {
///     let filename = "path/to/local/file";
///     let file = create_file(qc.clone(), "123e4567-e89b-12d3-a456-426614174000", &filename).await.unwrap();
///     let binding = file.create_file.unwrap().signed_upload_url.unwrap();
///     let signed_url = binding.as_str();
///
///     let res = stream_to_s3(qc.clone(), signed_url, &filename).await;
///     match res {
///         Ok(response) => println!("File uploaded successfully: {}", response.status()),
///         Err(e) => println!("Error during file upload: {}", e),
///     }
/// };
/// ```
pub async fn stream_to_s3(
    qc: Arc<QueryClient>,
    signed_url: &str,
    filename: &str,
) -> Result<reqwest::Response, Box<dyn std::error::Error>> {
    // TODO: streaming from disk to S3 returns a 501 error.
    // Investigate and fix. This works fine in the C# client.
    // let file = tokio::fs::File::open(filename).await?;
    // let size = file.metadata().await?.len();
    // let stream = tokio_util::codec::FramedRead::new(file, tokio_util::codec::BytesCodec::new());
    let file = std::fs::read(filename)?;

    match qc
        .client
        .put(signed_url.to_owned())
        // .header("Content-Type", "application/octet-stream")
        // .header("Content-Size", size.to_string())
        // .body(reqwest::Body::wrap_stream(stream))
        .body(file)
        .send()
        .await
    {
        Ok(response) => {
            if response.status() != reqwest::StatusCode::OK {
                let e: reqwest::Error = response.error_for_status().unwrap_err();
                return Err(Box::new(e));
            }
            Ok(response)
        }
        Err(error) => {
            eprintln!("  error: {:?}", error);
            Err(Box::new(error))
        }
    }
}