Skip to main content

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}