cognite/api/core/files.rs
1use std::collections::HashSet;
2
3use bytes::Bytes;
4use futures::TryStream;
5use serde::Serialize;
6
7use crate::api::resource::*;
8use crate::dto::core::files::*;
9use crate::dto::items::Items;
10use crate::error::Result;
11use crate::utils::lease::CleanResource;
12use crate::{
13 CondSend, Error, IdentityList, IdentityOrInstance, IdentityOrInstanceList, PartitionedFilter,
14};
15use crate::{Identity, ItemsVec, Patch};
16
17/// Files store documents, binary blobs, and other file data and relate it to assets.
18pub type Files = Resource<FileMetadata>;
19
20impl WithBasePath for Files {
21 const BASE_PATH: &'static str = "files";
22}
23
24impl FilterWithRequest<PartitionedFilter<FileFilter>, FileMetadata> for Files {}
25impl SearchItems<'_, FileFilter, FileSearch, FileMetadata> for Files {}
26impl<R> RetrieveWithIgnoreUnknownIds<IdentityOrInstanceList<R>, FileMetadata> for Files
27where
28 IdentityOrInstanceList<R>: Serialize,
29 R: Send + Sync,
30{
31}
32impl<R> DeleteWithIgnoreUnknownIds<IdentityList<R>> for Files
33where
34 IdentityList<R>: Serialize,
35 R: Send + Sync,
36{
37}
38impl Update<Patch<PatchFile>, FileMetadata> for Files {}
39
40/// Utility for uploading files in multiple parts.
41pub struct MultipartUploader<'a> {
42 resource: &'a Files,
43 id: IdentityOrInstance,
44 urls: MultiUploadUrls,
45}
46
47impl<'a> MultipartUploader<'a> {
48 /// Create a new multipart uploader.
49 ///
50 /// # Arguments
51 ///
52 /// * `resource` - Files resource.
53 /// * `id` - ID of the file to upload.
54 /// * `urls` - Upload URLs returned from `init_multipart_upload`.
55 pub fn new(resource: &'a Files, id: IdentityOrInstance, urls: MultiUploadUrls) -> Self {
56 Self { resource, id, urls }
57 }
58
59 /// Upload a part given by part index given by `part_no`. The part number
60 /// counts from zero, so with 5 parts you must upload with `part_no` 0, 1, 2, 3, and 4.
61 ///
62 /// # Arguments
63 ///
64 /// * `part_no` - Part number.
65 /// * `stream` - Stream to upload.
66 /// * `size` - Size of stream to upload.
67 pub async fn upload_part_stream<S>(&self, part_no: usize, stream: S, size: u64) -> Result<()>
68 where
69 S: futures::TryStream + CondSend + 'static,
70 S::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
71 bytes::Bytes: From<S::Ok>,
72 {
73 if part_no >= self.urls.upload_urls.len() {
74 return Err(Error::Other("Part number out of range".to_owned()));
75 }
76
77 self.resource
78 .upload_stream_known_size("", &self.urls.upload_urls[0], stream, size)
79 .await
80 }
81
82 #[cfg(not(target_arch = "wasm32"))]
83 /// Upload a part given by part index given by `part_no`. The part number
84 /// counts from zero, so with 5 parts you must upload with `part_no` 0, 1, 2, 3, and 4.
85 ///
86 /// # Arguments
87 ///
88 /// * `part_no` - Part number.
89 /// * `file` - File to upload.
90 pub async fn upload_part_file<S>(&self, part_no: usize, file: tokio::fs::File) -> Result<()> {
91 let size = file.metadata().await?.len();
92 let stream = tokio_util::codec::FramedRead::new(file, tokio_util::codec::BytesCodec::new());
93
94 self.upload_part_stream(part_no, stream, size).await
95 }
96
97 /// Upload a part given by part index given by `part_no`. The part number
98 /// counts from zero, so with 5 parts you must upload with `part_no` 0, 1, 2, 3, and 4.
99 ///
100 /// # Arguments
101 ///
102 /// * `part_no` - Part number.
103 /// * `part` - Binary data to upload.
104 pub async fn upload_part_blob(&self, part_no: usize, part: impl Into<Bytes>) -> Result<()> {
105 if part_no >= self.urls.upload_urls.len() {
106 return Err(Error::Other("Part number out of range".to_owned()));
107 }
108 self.resource
109 .upload_blob("", &self.urls.upload_urls[part_no], part)
110 .await
111 }
112
113 /// Complete the multipart upload process after all parts are uploaded.
114 pub async fn complete(self) -> Result<()> {
115 self.resource
116 .complete_multipart_upload(self.id, self.urls.upload_id)
117 .await
118 }
119}
120
121impl Files {
122 /// Upload a stream to a url, the url is received from `Files::upload`
123 ///
124 /// # Arguments
125 ///
126 /// * `mime_type` - Mime type of file to upload. For example `application/pdf`.
127 /// * `url` - URL to upload stream to.
128 /// * `stream` - Stream to upload.
129 /// * `stream_chunked` - Set this to `true` to use chunked streaming. Note that this is not supported for the
130 /// azure file backend. If this is set to `false`, the entire file is read into memory before uploading, which may
131 /// be very expensive. Use `upload_stream_known_size` if the size of the file is known.
132 ///
133 /// # Example
134 ///
135 /// ```ignore
136 /// use tokio_util::codec::{BytesCodec, FramedRead};
137 ///
138 /// let file = tokio::fs::File::open("my-file");
139 /// let stream = FramedRead::new(file, BytesCodec::new());
140 /// cognite_client.files.upload_stream(&file.mime_type.unwrap(), &file.upload_url, stream, true).await?;
141 /// ```
142 ///
143 /// Note that `stream_chunked` being true is in general more efficient, but it is not supported
144 /// for the azure file backend.
145 pub async fn upload_stream<S>(
146 &self,
147 mime_type: &str,
148 url: &str,
149 stream: S,
150 stream_chunked: bool,
151 ) -> Result<()>
152 where
153 S: futures::TryStream + CondSend + 'static,
154 S::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
155 bytes::Bytes: From<S::Ok>,
156 {
157 self.api_client
158 .put_stream(url, mime_type, stream, stream_chunked, None)
159 .await
160 }
161
162 /// Upload a stream to an url, the url is received from `Files::upload`
163 /// This method requires that the length of the stream in bytes is known before hand.
164 /// If the specified size is wrong, the request may fail or even hang.
165 ///
166 /// # Arguments
167 ///
168 /// * `mime_type` - Mime type of file to upload. For example `application/pdf`.
169 /// * `url` - URL to upload stream to.
170 /// * `stream` - Stream to upload.
171 /// * `size` - Known size of stream in bytes. Note: Do not use this method if the size is not
172 /// actually known!
173 ///
174 /// # Example
175 ///
176 /// ```ignore
177 /// use tokio_util::codec::{BytesCodec, FramedRead};
178 ///
179 /// let file = tokio::fs::File::open("my-file").await?;
180 /// let size = file.metadata().await?.len();
181 /// let stream = FramedRead::new(file, BytesCodec::new());
182 ///
183 /// cognite_client.files.upload_stream_known_size(&file_res.mime_type.unwrap(), &file_res.extra.upload_url, stream, size).await?;
184 /// ```
185 ///
186 /// Note that this will still stream the data from disk, so it should be as efficient as `upload_stream` with
187 /// `upload_chunked`, but not require the target to accept `content-encoding: chunked`.
188 pub async fn upload_stream_known_size<S>(
189 &self,
190 mime_type: &str,
191 url: &str,
192 stream: S,
193 size: u64,
194 ) -> Result<()>
195 where
196 S: futures::TryStream + CondSend + 'static,
197 S::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
198 bytes::Bytes: From<S::Ok>,
199 {
200 self.api_client
201 .put_stream(url, mime_type, stream, true, Some(size))
202 .await
203 }
204
205 #[cfg(not(target_arch = "wasm32"))]
206 /// Upload a file as a stream to CDF. `url` should be the upload URL returned from
207 /// `upload`.
208 ///
209 /// # Arguments
210 ///
211 /// * `mime_type` - Mime type of file to upload. For example `application/pdf`.
212 /// * `url` - URL to upload the file to.
213 /// * `file` - File to upload.
214 pub async fn upload_file(
215 &self,
216 mime_type: &str,
217 url: &str,
218 file: tokio::fs::File,
219 ) -> Result<()> {
220 let size = file.metadata().await?.len();
221 let stream = tokio_util::codec::FramedRead::new(file, tokio_util::codec::BytesCodec::new());
222
223 self.api_client
224 .put_stream(url, mime_type, stream, true, Some(size))
225 .await
226 }
227
228 /// Upload a binary vector to `url`.
229 ///
230 /// # Arguments
231 ///
232 /// * `mime_type` - Mime type of file to upload. For example `application/pdf`.
233 /// * `url` - URL to upload blob to.
234 /// * `blob` - File to upload, as bytes.
235 pub async fn upload_blob(
236 &self,
237 mime_type: &str,
238 url: &str,
239 blob: impl Into<Bytes>,
240 ) -> Result<()> {
241 self.api_client.put_blob(url, mime_type, blob).await
242 }
243
244 /// Create a file, optionally overwriting an existing file.
245 ///
246 /// The result will contain an upload URL that can be used to upload a file.
247 ///
248 /// # Arguments
249 ///
250 /// * `overwrite` - Set this to `true` to overwrite existing files with the same `external_id`.
251 /// If this is `false`, and a file with the given `external_id` already exists, the request will fail.
252 /// * `item` - The file to upload.
253 pub async fn upload(
254 &self,
255 overwrite: bool,
256 item: &AddFile,
257 ) -> Result<FileUploadResult<UploadUrl>> {
258 self.api_client
259 .post_with_query("files", item, Some(FileUploadQuery::new(overwrite)))
260 .await
261 }
262
263 /// Get an upload link for a file with given identity.
264 ///
265 /// # Arguments
266 ///
267 /// `id` - Identity of file metadata or data models file.
268 pub async fn get_upload_link(
269 &self,
270 id: &IdentityOrInstance,
271 ) -> Result<FileUploadResult<UploadUrl>> {
272 let mut res = self
273 .api_client
274 .post::<Items<Vec<FileUploadResult<UploadUrl>>>, _>(
275 "files/uploadlink",
276 &Items::new([id]),
277 )
278 .await?;
279 if res.items.is_empty() {
280 Err(Error::Other(
281 "File with given identity not found.".to_string(),
282 ))
283 } else {
284 Ok(res.items.remove(0))
285 }
286 }
287
288 /// Get multipart upload link for an existing file metadata or data models file.
289 ///
290 /// # Arguments
291 ///
292 /// * `id` - Identity of file metadata or data models file.
293 /// * `parts` - Number of parts to be uploaded.
294 pub async fn get_multipart_upload_link(
295 &self,
296 id: &IdentityOrInstance,
297 parts: u32,
298 ) -> Result<FileUploadResult<MultiUploadUrls>> {
299 let mut res = self
300 .api_client
301 .post_with_query::<Items<Vec<FileUploadResult<MultiUploadUrls>>>, _, _>(
302 "files/multiuploadlink",
303 &Items::new([id]),
304 Some(MultipartGetUploadLinkQuery::new(parts)),
305 )
306 .await?;
307 if res.items.is_empty() {
308 Err(Error::Other(
309 "File with given identity not found.".to_string(),
310 ))
311 } else {
312 Ok(res.items.remove(0))
313 }
314 }
315
316 /// Create a file, specifying that it should be uploaded in multiple parts.
317 ///
318 /// This returns a `MultipartUploader`, which wraps the upload process.
319 ///
320 /// # Arguments
321 ///
322 /// * `overwrite` - Set this to `true` to overwrite existing files with the same `external_id`.
323 /// If this is `false`, and a file with the given `external_id` already exists, the request will fail.
324 /// * `parts` - The number of parts to upload, should be a number between 1 and 250.
325 /// * `item` - The file to upload.
326 pub async fn multipart_upload<'a>(
327 &'a self,
328 overwrite: bool,
329 parts: u32,
330 item: &AddFile,
331 ) -> Result<(MultipartUploader<'a>, FileMetadata)> {
332 let res = self.init_multipart_upload(overwrite, parts, item).await?;
333 self.create_multipart_upload(res)
334 }
335
336 /// Upload files for an existing file metadata or data models file.
337 ///
338 /// This returns a `MultipartUploader`, which wraps the upload process.
339 ///
340 /// # Arguments
341 ///
342 /// * `parts` - The number of parts to upload, should be a number between 1 and 250.
343 /// * `id` - Identity of file metadata or data models file.
344 pub async fn multipart_upload_existing<'a>(
345 &'a self,
346 id: &IdentityOrInstance,
347 parts: u32,
348 ) -> Result<(MultipartUploader<'a>, FileMetadata)> {
349 let res = self.get_multipart_upload_link(id, parts).await?;
350 self.create_multipart_upload(res)
351 }
352
353 fn create_multipart_upload(
354 &self,
355 res: FileUploadResult<MultiUploadUrls>,
356 ) -> Result<(MultipartUploader<'_>, FileMetadata)> {
357 Ok((
358 MultipartUploader::new(
359 self,
360 IdentityOrInstance::Identity(Identity::Id {
361 id: res.metadata.id,
362 }),
363 res.extra,
364 ),
365 res.metadata,
366 ))
367 }
368
369 /// Create a file, specifying that it should be uploaded in multiple parts.
370 ///
371 /// # Arguments
372 ///
373 /// * `overwrite` - Set this to `true` to overwrite existing files with the same `external_id`.
374 /// If this is `false`, and a file with the given `external_id` already exists, the request will fail.
375 /// * `parts` - The number of parts to upload, should be a number between 1 and 250.
376 /// * `item` - The file to upload.
377 pub async fn init_multipart_upload(
378 &self,
379 overwrite: bool,
380 parts: u32,
381 item: &AddFile,
382 ) -> Result<FileUploadResult<MultiUploadUrls>> {
383 self.api_client
384 .post_with_query(
385 "files/initmultipartupload",
386 item,
387 Some(MultipartFileUploadQuery::new(overwrite, parts)),
388 )
389 .await
390 }
391
392 /// Complete a multipart upload. This endpoint must be called after all parts of a multipart file
393 /// upload have been uploaded.
394 ///
395 /// # Arguments
396 ///
397 /// * `id` - ID of the file that was uploaded.
398 /// * `upload_id` - `upload_id` returned by `init_multipart_upload`.
399 pub async fn complete_multipart_upload(
400 &self,
401 id: IdentityOrInstance,
402 upload_id: String,
403 ) -> Result<()> {
404 self.api_client
405 .post::<serde_json::Value, _>(
406 "files/completemultipartupload",
407 &CompleteMultipartUpload { id, upload_id },
408 )
409 .await?;
410 Ok(())
411 }
412
413 /// Get download links for a list of files.
414 ///
415 /// # Arguments
416 ///
417 /// * `ids` - List of file IDs or external IDs.
418 pub async fn download_link(&self, ids: &[IdentityOrInstance]) -> Result<Vec<FileDownloadUrl>> {
419 let items = Items::new(ids);
420 let file_links_response: ItemsVec<FileDownloadUrl> =
421 self.api_client.post("files/downloadlink", &items).await?;
422 Ok(file_links_response.items)
423 }
424
425 /// Stream a file from `url`.
426 ///
427 /// # Arguments
428 ///
429 /// * `url` - URL to download from.
430 pub async fn download(
431 &self,
432 url: &str,
433 ) -> Result<impl TryStream<Ok = bytes::Bytes, Error = reqwest::Error>> {
434 self.api_client.get_stream(url).await
435 }
436
437 /// Stream a file given by `id`.
438 ///
439 /// # Arguments
440 ///
441 /// * `id` - ID or external ID of file to download.
442 pub async fn download_file(
443 &self,
444 id: IdentityOrInstance,
445 ) -> Result<impl TryStream<Ok = bytes::Bytes, Error = reqwest::Error>> {
446 let items = vec![id];
447 let links = self.download_link(&items).await?;
448 let link = links.first().unwrap();
449 self.download(&link.download_url).await
450 }
451}
452
453impl CleanResource<FileMetadata> for Files {
454 async fn clean_resource(
455 &self,
456 resources: Vec<FileMetadata>,
457 ) -> std::result::Result<(), crate::Error> {
458 let ids = resources
459 .iter()
460 .map(|a| Identity::from(a.id))
461 .collect::<HashSet<Identity>>();
462 self.delete(&ids.into_iter().collect::<Vec<_>>(), true)
463 .await
464 }
465}