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/// [`ObjectStore::put_multipart_opts`]: crate::ObjectStore::put_multipart_opts
43/// [`LocalFileSystem`]: crate::local::LocalFileSystem
44#[async_trait]
45pub trait MultipartStore: Send + Sync + 'static {
46 /// Creates a new multipart upload, returning the [`MultipartId`]
47 async fn create_multipart(&self, path: &Path) -> Result<MultipartId>;
48
49 /// Creates a new multipart upload with the given options, returning the [`MultipartId`]
50 ///
51 /// This allows callers using the low-level multipart API to provide object attributes,
52 /// tags, or implementation-specific extensions when initiating the upload.
53 async fn create_multipart_opts(
54 &self,
55 path: &Path,
56 opts: PutMultipartOptions,
57 ) -> Result<MultipartId> {
58 let PutMultipartOptions {
59 tags,
60 attributes,
61 extensions: _,
62 } = opts;
63
64 if !tags.is_empty() || !attributes.is_empty() {
65 return Err(Error::NotSupported {
66 source: "create_multipart_opts with non-default options".into(),
67 });
68 }
69
70 self.create_multipart(path).await
71 }
72
73 /// Uploads a new part with index `part_idx`
74 ///
75 /// `part_idx` should be an integer in the range `0..N` where `N` is the number of
76 /// parts in the upload. Parts may be uploaded concurrently and in any order.
77 ///
78 /// Most stores require that all parts excluding the last are at least 5 MiB, and some
79 /// further require that all parts excluding the last be the same size, e.g. [R2].
80 /// [`WriteMultipart`] performs writes in fixed size blocks of 5 MiB, and clients wanting
81 /// to maximise compatibility should look to do likewise.
82 ///
83 /// [R2]: https://developers.cloudflare.com/r2/objects/multipart-objects/#limitations
84 /// [`WriteMultipart`]: crate::upload::WriteMultipart
85 async fn put_part(
86 &self,
87 path: &Path,
88 id: &MultipartId,
89 part_idx: usize,
90 data: PutPayload,
91 ) -> Result<PartId>;
92
93 /// Completes a multipart upload
94 ///
95 /// The `i`'th value of `parts` must be a [`PartId`] returned by a call to [`Self::put_part`]
96 /// with a `part_idx` of `i`, and the same `path` and `id` as provided to this method. Calling
97 /// this method with out of sequence or repeated [`PartId`], or [`PartId`] returned for other
98 /// values of `path` or `id`, will result in implementation-defined behaviour
99 async fn complete_multipart(
100 &self,
101 path: &Path,
102 id: &MultipartId,
103 parts: Vec<PartId>,
104 ) -> Result<PutResult>;
105
106 /// Aborts a multipart upload
107 async fn abort_multipart(&self, path: &Path, id: &MultipartId) -> Result<()>;
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113 use crate::Extensions;
114
115 struct TestMultipartStore;
116
117 #[async_trait]
118 impl MultipartStore for TestMultipartStore {
119 async fn create_multipart(&self, _path: &Path) -> Result<MultipartId> {
120 Ok("test".into())
121 }
122
123 async fn put_part(
124 &self,
125 _path: &Path,
126 _id: &MultipartId,
127 _part_idx: usize,
128 _data: PutPayload,
129 ) -> Result<PartId> {
130 unreachable!()
131 }
132
133 async fn complete_multipart(
134 &self,
135 _path: &Path,
136 _id: &MultipartId,
137 _parts: Vec<PartId>,
138 ) -> Result<PutResult> {
139 unreachable!()
140 }
141
142 async fn abort_multipart(&self, _path: &Path, _id: &MultipartId) -> Result<()> {
143 unreachable!()
144 }
145 }
146
147 #[tokio::test]
148 async fn default_create_multipart_opts_ignores_extensions() {
149 let mut extensions = Extensions::new();
150 extensions.insert("extension");
151 let opts = PutMultipartOptions {
152 extensions,
153 ..Default::default()
154 };
155
156 let id = TestMultipartStore
157 .create_multipart_opts(&Path::from("test"), opts)
158 .await
159 .unwrap();
160
161 assert_eq!(id, "test");
162 }
163}