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}