Skip to main content

object_store/
multipart.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! Cloud Multipart Upload
19//!
20//! This crate provides an asynchronous interface for multipart file uploads to
21//! cloud storage services. It's designed to offer efficient, non-blocking operations,
22//! especially useful when dealing with large files or high-throughput systems.
23
24use async_trait::async_trait;
25
26use crate::path::Path;
27use crate::{Error, MultipartId, PutMultipartOptions, PutPayload, PutResult, Result};
28
29/// Represents a part of a file that has been successfully uploaded in a multipart upload process.
30#[derive(Debug, Clone)]
31pub struct PartId {
32    /// Id of this part
33    pub content_id: String,
34}
35
36/// A low-level interface for interacting with multipart upload APIs
37///
38/// Most use-cases should prefer [`ObjectStore::put_multipart_opts`] as this is supported by more
39/// backends, including [`LocalFileSystem`], and automatically handles uploading fixed
40/// size parts of sufficient size in parallel
41///
42/// See [`AmazonS3::create_multipart`] for a full example of this API.
43///
44/// [`ObjectStore::put_multipart_opts`]: crate::ObjectStore::put_multipart_opts
45/// [`LocalFileSystem`]: crate::local::LocalFileSystem
46/// [`AmazonS3::create_multipart`]: crate::aws::AmazonS3::create_multipart
47#[async_trait]
48pub trait MultipartStore: Send + Sync + 'static {
49    /// Creates a new multipart upload, returning the [`MultipartId`]
50    ///
51    /// See [`AmazonS3::create_multipart`] for a full example of this API.
52    ///
53    /// [`AmazonS3::create_multipart`]: crate::aws::AmazonS3::create_multipart
54    async fn create_multipart(&self, path: &Path) -> Result<MultipartId>;
55
56    /// Creates a new multipart upload with the given options, returning the [`MultipartId`]
57    ///
58    /// This allows callers using the low-level multipart API to provide object attributes,
59    /// tags, or implementation-specific extensions when initiating the upload.
60    ///
61    /// See [`AmazonS3::create_multipart`] for a full example of this API.
62    ///
63    /// [`AmazonS3::create_multipart`]: crate::aws::AmazonS3::create_multipart
64    async fn create_multipart_opts(
65        &self,
66        path: &Path,
67        opts: PutMultipartOptions,
68    ) -> Result<MultipartId> {
69        let PutMultipartOptions {
70            tags,
71            attributes,
72            extensions: _,
73        } = opts;
74
75        if !tags.is_empty() || !attributes.is_empty() {
76            return Err(Error::NotSupported {
77                source: "create_multipart_opts with non-default options".into(),
78            });
79        }
80
81        self.create_multipart(path).await
82    }
83
84    /// Uploads a new part with index `part_idx`
85    ///
86    /// `part_idx` should be an integer in the range `0..N` where `N` is the number of
87    /// parts in the upload. Parts may be uploaded concurrently and in any order.
88    ///
89    /// Most stores require that all parts excluding the last are at least 5 MiB, and some
90    /// further require that all parts excluding the last be the same size, e.g. [R2].
91    /// [`WriteMultipart`] performs writes in fixed size blocks of 5 MiB, and clients wanting
92    /// to maximise compatibility should look to do likewise.
93    ///
94    /// [R2]: https://developers.cloudflare.com/r2/objects/multipart-objects/#limitations
95    /// [`WriteMultipart`]: crate::upload::WriteMultipart
96    async fn put_part(
97        &self,
98        path: &Path,
99        id: &MultipartId,
100        part_idx: usize,
101        data: PutPayload,
102    ) -> Result<PartId>;
103
104    /// Completes a multipart upload
105    ///
106    /// The `i`'th value of `parts` must be a [`PartId`] returned by a call to [`Self::put_part`]
107    /// with a `part_idx` of `i`, and the same `path` and `id` as provided to this method. Calling
108    /// this method with out of sequence or repeated [`PartId`], or [`PartId`] returned for other
109    /// values of `path` or `id`, will result in implementation-defined behaviour
110    async fn complete_multipart(
111        &self,
112        path: &Path,
113        id: &MultipartId,
114        parts: Vec<PartId>,
115    ) -> Result<PutResult>;
116
117    /// Aborts a multipart upload
118    async fn abort_multipart(&self, path: &Path, id: &MultipartId) -> Result<()>;
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    use crate::Extensions;
125
126    struct TestMultipartStore;
127
128    #[async_trait]
129    impl MultipartStore for TestMultipartStore {
130        async fn create_multipart(&self, _path: &Path) -> Result<MultipartId> {
131            Ok("test".into())
132        }
133
134        async fn put_part(
135            &self,
136            _path: &Path,
137            _id: &MultipartId,
138            _part_idx: usize,
139            _data: PutPayload,
140        ) -> Result<PartId> {
141            unreachable!()
142        }
143
144        async fn complete_multipart(
145            &self,
146            _path: &Path,
147            _id: &MultipartId,
148            _parts: Vec<PartId>,
149        ) -> Result<PutResult> {
150            unreachable!()
151        }
152
153        async fn abort_multipart(&self, _path: &Path, _id: &MultipartId) -> Result<()> {
154            unreachable!()
155        }
156    }
157
158    #[tokio::test]
159    async fn default_create_multipart_opts_ignores_extensions() {
160        let mut extensions = Extensions::new();
161        extensions.insert("extension");
162        let opts = PutMultipartOptions {
163            extensions,
164            ..Default::default()
165        };
166
167        let id = TestMultipartStore
168            .create_multipart_opts(&Path::from("test"), opts)
169            .await
170            .unwrap();
171
172        assert_eq!(id, "test");
173    }
174}