discovery_connect/api/
file.rs

1// Copyright 2023 Ikerian AG
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use crate::disco_api;
16use crate::query::{post_query, QueryClient};
17use graphql_client::GraphQLQuery;
18use serde_json::Value as JSON;
19use std::sync::Arc;
20
21disco_api!(
22    CreateFile,
23    "createFile",
24    "src/api/graphql/CreateFile.graphql"
25);
26disco_api!(
27    RegisterUploadedFile,
28    "registerUploadedFile",
29    "src/api/graphql/RegisterUploadedFile.graphql"
30);
31
32/// Creates a new file entry in the specified workbook and returns a signed URL for uploading the file.
33///
34/// # Arguments
35///
36/// * `qc` - An `Arc<QueryClient>` object that represents the connection to the Connect server.
37/// * `workbook_uuid` - The UUID of the workbook in which to create the file.
38/// * `path` - The path to the file, relative to the workbook's root directory.
39///
40/// # Returns
41///
42/// A `Result` containing either the response data on success (`create_file::ResponseData`),
43/// or an `reqwest::Error` on failure. The response data contains:
44/// - the UUID of the newly created file
45/// - a signed URL for uploading the file
46///
47/// # Errors
48///
49/// Returns an `Err` of type `reqwest::Error` if the file creation request fails or if the server
50/// response status is not OK.
51///
52/// # Example
53///
54/// ```
55/// use discovery_connect::file::{CreateFile, create_file};
56/// use discovery_connect::{QueryClient};
57/// use std::sync::Arc;
58///
59/// let qc = Arc::new(QueryClient::new(
60///         "https://api.example.discovery.retinai.com",
61///         "client_id",
62///         "client_secret",
63///         "user@example",
64///         "password123",
65///         std::time::Duration::from_secs(10)));
66/// async {
67///     let res = create_file(qc, "123e4567-e89b-12d3-a456-426614174000", "path/to/file").await;
68///     match res {
69///         Ok(data) => println!("File created: {}", data.create_file.unwrap().uuid.unwrap()),
70///         Err(e) => println!("Error: {:?}", e),
71///     };
72/// };
73///
74/// ```
75pub async fn create_file(
76    qc: Arc<QueryClient>,
77    workbook_uuid: &str,
78    path: &str,
79) -> Result<create_file::ResponseData, reqwest::Error> {
80    let input = create_file::CreateFileInput {
81        uuid: None,
82        filename: path.to_string(),
83        tags: Some(vec![]),
84        remarks: Some(serde_json::Value::Object(serde_json::Map::new())),
85        overwrite: Some(serde_json::Value::Object(serde_json::Map::new())),
86        workbook_uuid: Some(workbook_uuid.to_string()),
87    };
88    let variables = create_file::Variables { input };
89    post_query::<CreateFile>(qc, variables).await
90}
91
92/// Registers the status of an uploaded file in the system, marking it as successfully uploaded or failed.
93///
94/// If successful, the server will start processing the file.
95///
96/// # Arguments
97///
98/// * `qc` - An `Arc<QueryClient>` object representing the connection to the Connect server.
99/// * `uuid` - A String reference representing the UUID of the file whose upload status is being registered.
100/// * `upload_failed` - An `Option<bool>` indicating whether the upload failed (Some(true)) or succeeded (Some(false) or None).
101///
102/// # Returns
103///
104/// A `Result` containing either the response data on success (`register_uploaded_file::ResponseData`),
105/// or an `reqwest::Error` on failure. The response data typically includes confirmation of the registration.
106///
107/// # Errors
108///
109/// Returns an `Err` of type `reqwest::Error` if the registration request fails or if the server
110/// response status is not OK.
111///
112/// # Example
113///
114/// ```
115/// use discovery_connect::file::{RegisterUploadedFile, register_uploaded_file};
116/// use discovery_connect::{QueryClient};
117/// use std::sync::Arc;
118///
119/// let qc = Arc::new(QueryClient::new(
120///     "https://api.example.discovery.retinai.com",
121///     "client_id",
122///     "client_secret",
123///     "user@example",
124///     "password123",
125///     std::time::Duration::from_secs(10)));
126/// let file_uuid = "123e4567-e89b-12d3-a456-426614174000".to_string();
127/// let upload_status = Some(false);
128/// async {
129///     let res = register_uploaded_file(qc.clone(), &file_uuid, upload_status).await;
130///     match res {
131///         Ok(response) => println!("File registration successful"),
132///         Err(e) => println!("Error registering file: {}", e),
133///     }
134/// };
135/// ```
136pub async fn register_uploaded_file(
137    qc: Arc<QueryClient>,
138    uuid: &String,
139    upload_failed: Option<bool>,
140) -> Result<register_uploaded_file::ResponseData, reqwest::Error> {
141    let variables = register_uploaded_file::Variables {
142        uuid: uuid.to_string(),
143        upload_failed: Some(upload_failed.unwrap_or_default()),
144    };
145    post_query::<RegisterUploadedFile>(qc, variables).await
146}
147
148/// Streams a file to AWS S3 using a pre-signed URL.
149///
150/// # Arguments
151///
152/// * `qc` - An `Arc<QueryClient>` object that represents the connection to the Connect server.
153/// * `signed_url` - The pre-signed URL provided by AWS S3 for file upload.
154/// * `filename` - The local path to the file that needs to be uploaded.
155///
156/// # Returns
157///
158/// A `Result` containing either a `reqwest::Response` on success, or a `Box<dyn std::error::Error>` on failure.
159/// The successful response indicates that the file has been successfully uploaded to the specified S3 location.
160///
161/// # Errors
162///
163/// Returns an `Err` of type `Box<dyn std::error::Error>` if there's an error in reading the file, preparing the HTTP request,
164/// or if the server response status is not OK. Includes a `reqwest::Error` if the upload fails.
165///
166/// # Notes
167///
168/// Currently, this function has a known issue with streaming from disk to S3 (returns a 501 error).
169/// This needs investigation and fixing. It works correctly in the C# client implementation.
170///
171/// # Example
172///
173/// ```
174/// use discovery_connect::file::{create_file, stream_to_s3};
175/// use discovery_connect::{QueryClient};
176/// use std::sync::Arc;
177///
178/// let qc = Arc::new(QueryClient::new(
179///     "https://api.example.discovery.retinai.com",
180///     "client_id",
181///     "client_secret",
182///     "user@example",
183///     "password123",
184///     std::time::Duration::from_secs(10)));
185/// async {
186///     let filename = "path/to/local/file";
187///     let file = create_file(qc.clone(), "123e4567-e89b-12d3-a456-426614174000", &filename).await.unwrap();
188///     let binding = file.create_file.unwrap().signed_upload_url.unwrap();
189///     let signed_url = binding.as_str();
190///
191///     let res = stream_to_s3(qc.clone(), signed_url, &filename).await;
192///     match res {
193///         Ok(response) => println!("File uploaded successfully: {}", response.status()),
194///         Err(e) => println!("Error during file upload: {}", e),
195///     }
196/// };
197/// ```
198pub async fn stream_to_s3(
199    qc: Arc<QueryClient>,
200    signed_url: &str,
201    filename: &str,
202) -> Result<reqwest::Response, Box<dyn std::error::Error>> {
203    // TODO: streaming from disk to S3 returns a 501 error.
204    // Investigate and fix. This works fine in the C# client.
205    // let file = tokio::fs::File::open(filename).await?;
206    // let size = file.metadata().await?.len();
207    // let stream = tokio_util::codec::FramedRead::new(file, tokio_util::codec::BytesCodec::new());
208    let file = std::fs::read(filename)?;
209
210    match qc
211        .client
212        .put(signed_url.to_owned())
213        // .header("Content-Type", "application/octet-stream")
214        // .header("Content-Size", size.to_string())
215        // .body(reqwest::Body::wrap_stream(stream))
216        .body(file)
217        .send()
218        .await
219    {
220        Ok(response) => {
221            if response.status() != reqwest::StatusCode::OK {
222                let e: reqwest::Error = response.error_for_status().unwrap_err();
223                return Err(Box::new(e));
224            }
225            Ok(response)
226        }
227        Err(error) => {
228            eprintln!("  error: {:?}", error);
229            Err(Box::new(error))
230        }
231    }
232}