Skip to main content

rusty_cat/
upload_trait.rs

1use bytes::Bytes;
2
3use crate::http_breakpoint::UploadResumeInfo;
4use crate::{MeowError, TransferTask};
5use async_trait::async_trait;
6
7/// Context for upload prepare stage.
8#[derive(Debug, Clone, Copy)]
9pub struct UploadPrepareCtx<'a> {
10    /// HTTP client used for requests.
11    pub client: &'a reqwest::Client,
12    /// Immutable task snapshot.
13    pub task: &'a TransferTask,
14    /// Locally confirmed uploaded offset in bytes.
15    ///
16    /// Range: `>= 0`.
17    pub local_offset: u64,
18}
19
20/// Context for upload chunk stage.
21///
22/// [`UploadChunkCtx::chunk`] is a [`bytes::Bytes`] handle. Cloning it is an
23/// O(1) refcount bump, and passing it into [`reqwest::Body`] never copies the
24/// underlying buffer. This lets protocol implementations send the same chunk
25/// multiple times (for example during retries) without re-allocating.
26#[derive(Debug, Clone)]
27pub struct UploadChunkCtx<'a> {
28    /// HTTP client used for requests.
29    pub client: &'a reqwest::Client,
30    /// Immutable task snapshot.
31    pub task: &'a TransferTask,
32    /// Raw bytes for the current chunk.
33    ///
34    /// `Bytes` is cheap to clone and can be converted into `reqwest::Body`
35    /// without copying. Avoid calling [`bytes::Bytes::to_vec`] on hot paths.
36    pub chunk: Bytes,
37    /// Start offset of this chunk in the full file.
38    ///
39    /// Range: `>= 0`.
40    pub offset: u64,
41}
42
43/// Custom breakpoint upload protocol.
44///
45/// Implementors are responsible for request construction and response parsing.
46/// The executor handles file I/O, chunking, retries, progress, and scheduling.
47///
48/// # Examples
49///
50/// ```no_run
51/// use async_trait::async_trait;
52/// use rusty_cat::api::{
53///     BreakpointUpload, MeowError, UploadChunkCtx, UploadPrepareCtx, UploadResumeInfo,
54/// };
55///
56/// struct MyUploadProtocol;
57///
58/// #[async_trait]
59/// impl BreakpointUpload for MyUploadProtocol {
60///     async fn prepare(&self, _ctx: UploadPrepareCtx<'_>) -> Result<UploadResumeInfo, MeowError> {
61///         Ok(UploadResumeInfo::default())
62///     }
63///
64///     async fn upload_chunk(&self, ctx: UploadChunkCtx<'_>) -> Result<UploadResumeInfo, MeowError> {
65///         let _ = (ctx.task.file_name(), ctx.offset);
66///         Ok(UploadResumeInfo {
67///             completed_file_id: None,
68///             next_byte: Some(ctx.offset + ctx.chunk.len() as u64),
69///             provider_upload_id: None,
70///         })
71///     }
72/// }
73/// ```
74///
75/// # Executor integration contract
76///
77/// - If [`UploadResumeInfo::completed_file_id`] is `Some`, the executor treats
78///   the upload as fully completed and stops sending further chunks.
79/// - [`UploadResumeInfo::next_byte`] is a server-suggested next offset; the
80///   executor merges it with local offset before continuing.
81/// - When computed next offset reaches `task.total_size()`, the executor calls
82///   [`BreakpointUpload::complete_upload`] unless completion was already
83///   indicated by `completed_file_id`.
84/// - When user cancels an upload task, executor calls
85///   [`BreakpointUpload::abort_upload`].
86#[async_trait]
87pub trait BreakpointUpload: Send + Sync {
88    /// Prepare stage before first chunk upload.
89    ///
90    /// Typical responsibilities include creating upload session and querying
91    /// already uploaded offset on remote side.
92    ///
93    /// # Parameters
94    ///
95    /// - `client`: Shared HTTP client used by executor.
96    /// - `task`: Upload task snapshot with URL/method/headers/file metadata.
97    /// - `local_offset`: Locally confirmed uploaded offset.
98    ///
99    /// # Returns
100    ///
101    /// - `Ok(info)`: Server resume info used by executor to compute next offset.
102    /// - `Err`: Prepare failed and task enters error path.
103    ///
104    /// # Errors
105    ///
106    /// Return [`MeowError`] when remote session creation/checkpoint probing
107    /// fails, request signing fails, or protocol payload parsing fails.
108    ///
109    /// # Examples
110    ///
111    /// ```no_run
112    /// use rusty_cat::api::UploadPrepareCtx;
113    ///
114    /// fn read_prepare_ctx(ctx: UploadPrepareCtx<'_>) {
115    ///     let _ = (ctx.task.url(), ctx.local_offset);
116    /// }
117    /// ```
118    async fn prepare(&self, ctx: UploadPrepareCtx<'_>) -> Result<UploadResumeInfo, MeowError>;
119
120    /// Uploads a single chunk.
121    ///
122    /// Executor guarantees chunk bounds are valid and chunk bytes match the
123    /// provided `offset`.
124    ///
125    /// # Errors
126    ///
127    /// Return [`MeowError`] when chunk request fails, server rejects the chunk,
128    /// or protocol response parsing fails.
129    ///
130    /// # Examples
131    ///
132    /// ```no_run
133    /// use rusty_cat::api::UploadChunkCtx;
134    ///
135    /// fn read_chunk_ctx(ctx: &UploadChunkCtx<'_>) {
136    ///     // `ctx.chunk` is a `bytes::Bytes`; cloning is a cheap refcount bump.
137    ///     let _ = (ctx.offset, ctx.chunk.len(), ctx.task.total_size());
138    /// }
139    /// ```
140    async fn upload_chunk(&self, ctx: UploadChunkCtx<'_>) -> Result<UploadResumeInfo, MeowError>;
141
142    /// Finalization step after all chunk bytes are uploaded.
143    ///
144    /// Typical use case: multipart-complete API calls. Return value is an
145    /// optional provider-defined payload that will be forwarded to
146    /// `MeowClient::try_enqueue` complete callback.
147    ///
148    /// Default implementation is a no-op (`Ok(None)`).
149    ///
150    /// # Errors
151    ///
152    /// Implementations should return [`MeowError`] if final commit/merge API
153    /// fails.
154    async fn complete_upload(
155        &self,
156        _client: &reqwest::Client,
157        _task: &TransferTask,
158    ) -> Result<Option<String>, MeowError> {
159        Ok(None)
160    }
161
162    /// Abort/cleanup hook called when user cancels an upload task.
163    ///
164    /// Typical use case: abort multipart session or remove temporary objects.
165    /// Default implementation is a no-op.
166    ///
167    /// # Errors
168    ///
169    /// Implementations should return [`MeowError`] when cleanup or abort API
170    /// calls fail.
171    async fn abort_upload(
172        &self,
173        _client: &reqwest::Client,
174        _task: &TransferTask,
175    ) -> Result<(), MeowError> {
176        Ok(())
177    }
178
179    /// Whether this protocol is safe to upload chunks of one file **concurrently
180    /// and out of order** (intra-file parallel parts).
181    ///
182    /// The default is `false`, which keeps every protocol on the strict-serial
183    /// upload path. Returning `true` is an **opt-in contract**: the executor may
184    /// then dispatch up to `max_parts_in_flight` chunks of the same file at once,
185    /// in any completion order. Only override this to `true` when ALL of the
186    /// following hold, or out-of-order uploads will silently corrupt data:
187    ///
188    /// - **Offset-derived part identity.** Each chunk's server-side identity
189    ///   (part number / block id) is a pure function of its `offset`, never an
190    ///   incrementing counter or arrival-order sequence.
191    /// - **No server single-cursor.** The protocol does not rely on a
192    ///   server-maintained "next expected byte" that assumes sequential arrival.
193    /// - **Idempotent re-upload.** Re-uploading a part at the same offset (which
194    ///   happens on resume for any part above the persisted contiguous prefix)
195    ///   overwrites rather than duplicates.
196    /// - **Completion gated by the executor.** [`Self::complete_upload`] is only
197    ///   invoked once, after the executor confirms the whole file arrived as a
198    ///   contiguous prefix and every in-flight part has joined; the protocol does
199    ///   not finalize off a single chunk.
200    ///
201    /// `&self` is shared across all concurrent part calls, so any per-part
202    /// bookkeeping the protocol keeps must already be interior-mutable and
203    /// thread-safe.
204    fn supports_parallel_parts(&self) -> bool {
205        false
206    }
207}