td_client/transfer.rs
1//! Upload/download measurements and explicit downloads.
2//!
3//! Files are uploaded through tracked [message sends](crate::message), not a
4//! preliminary-upload method. [`Client::download`] handles a generated
5//! `downloadFile` request. All tracked operations accept the same optional
6//! borrowed `FnMut(usize, Progress) + Send` callback. Its index is the batch
7//! position for multiple messages, or zero for single sends and downloads.
8//!
9//! # Measurements are not completion
10//!
11//! [`Progress`] contains coalesced measurements, not a reliable event log or
12//! a smoothed percentage. Values may repeat or decrease, and size estimates may
13//! change. Zero total means indeterminate; it does not imply an empty file.
14//! There are no synthetic initial/final samples. Cached work, rapid completion,
15//! or coalescing can mean no callback at all, including for some album items.
16//!
17//! Only the operation's result establishes success. Even `current == total`
18//! does not establish server acceptance of a message.
19//!
20//! Callbacks run on the task polling the operation, outside internal locks.
21//! They may borrow local state and need not be `'static`, but must be `Send`,
22//! short-running, and non-panicking. Offload expensive work yourself. A panic
23//! unwinds the operation; it does not cancel native work or shut down its owner.
24//!
25//! # Download ranges
26//!
27//! Download progress is relative to the requested offset and limit. Only an
28//! available contiguous prefix beginning at that offset counts. Cached bytes
29//! outside the range and bytes beyond a hole do not increase its progress.
30//! A positive limit caps the range; zero means no explicit length limit.
31//! Estimates do not replace the final file state returned by `TDLib`.
32//!
33//! # Cancellation versus abandonment
34//!
35//! [`CancellationToken`] is the Tokio utility token, reexported for convenience.
36//! Calling `cancel()` requests cleanup when the operation is next polled.
37//! Dropping the operation only abandons local observation; it does not cancel
38//! `TDLib` work. Do not race cancellation against dropping the same future and
39//! expect cleanup to complete.
40//!
41//! Download cancellation is file-wide in `TDLib`. Concurrent download requests for
42//! one file can affect one another; they are not independent cancellable slices.
43//! Message-send cancellation has different native semantics; see [`crate::message`].
44
45/// A cooperative cancellation signal for tracked operations.
46///
47/// Reexported from `tokio-util`. Cancelling a token is sticky and affects every
48/// operation using that token. Dropping a token is not the same as calling
49/// `cancel()`; native cleanup still requires the operation future to be polled.
50pub use tokio_util::sync::CancellationToken;
51
52use td_types::enums::File;
53use td_types::{fns, types};
54
55use crate::client::Client;
56use crate::connection::tracking::{cancelled, with_progress};
57use crate::error::{Error, Result};
58
59/// A copy-only byte measurement for one tracked transfer.
60///
61/// Uploads report primary-file uploaded bytes. Downloads report the available
62/// prefix of the requested range. Values are not monotonic, and `total` may be
63/// an estimate. See the [module guide](crate::transfer#measurements-are-not-completion).
64///
65/// # Examples
66///
67/// Treat zero totals as indeterminate rather than dividing by zero:
68///
69/// ```
70/// use td_client::Progress;
71///
72/// fn display(progress: Progress) -> String {
73/// match progress.total {
74/// 1.. => format!("{} / {} bytes", progress.current, progress.total),
75/// _ => format!("{} bytes", progress.current),
76/// }
77/// }
78/// assert_eq!(display(Progress { current: 64, total: 0 }), "64 bytes");
79/// ```
80#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
81pub struct Progress {
82 /// Uploaded bytes, or downloaded bytes in the requested contiguous prefix.
83 pub current: i64,
84 /// Expected transfer size in bytes, or zero when indeterminate.
85 pub total: i64,
86}
87
88impl Client {
89 /// Downloads a file or byte range and returns `TDLib`'s final file state.
90 ///
91 /// The caller supplies the generated `downloadFile` request, including its
92 /// priority and range. `synchronous` must be `true`: `TDLib` retains the response
93 /// until the request finishes. This does not block the Rust task's thread.
94 ///
95 /// `offset` and `limit` must be valid nonnegative `TDLib` arguments whose range
96 /// arithmetic fits `i64`. The method does not repair invalid ranges. See the
97 /// [range contract](crate::transfer#download-ranges). With a callback, progress
98 /// uses item index zero; no callback is required for successful cached work.
99 ///
100 /// # Errors
101 ///
102 /// Returns direct-request errors as described on [`Client::send`]. When native
103 /// cancellation succeeds and the download returns a `TDLib` error, that error is
104 /// reported as [`Error::Cancelled`]. This is a cancellation interpretation,
105 /// not proof that no other native failure raced with cancellation. A successful
106 /// download response wins even if cancellation was also requested.
107 /// Cancellation-request failures are returned when no successful download won.
108 ///
109 /// # Panics
110 ///
111 /// Panics when `request.synchronous` is false. Caller callbacks must not panic.
112 ///
113 /// # Cancellation
114 ///
115 /// `TDLib` cancellation affects the entire file, including concurrent requests.
116 /// Dropping this future does not invoke it. A pre-cancelled token does not
117 /// guarantee the download was never submitted, and cached success may win.
118 /// Keep the future driven while token-triggered cleanup runs.
119 ///
120 /// # Examples
121 ///
122 /// ```no_run
123 /// # use td_client::Client;
124 /// # use td_client::Result;
125 /// # use td_client::Progress;
126 /// use td_types::fns;
127 ///
128 /// # async fn fetch(client: &Client, file_id: i32) -> Result {
129 /// let request = fns::downloadFile {
130 /// file_id, priority: 1, offset: 0, limit: 0, synchronous: true,
131 /// };
132 /// let mut observe = |_: usize, progress: Progress| {
133 /// println!("Available: {} bytes", progress.current);
134 /// };
135 /// let file = client.download(&request, None, Some(&mut observe)).await?;
136 /// println!("Local path: {}", file.local.path);
137 /// # Ok(())
138 /// # }
139 /// ```
140 pub async fn download(
141 &self,
142 request: &fns::downloadFile,
143 cancel: Option<&CancellationToken>,
144 progress: Option<&mut (dyn FnMut(usize, Progress) + Send)>,
145 ) -> Result<types::file> {
146 assert!(request.synchronous, "downloadFile.synchronous must be true");
147 let connection = self.connection()?;
148 let samples = progress.as_ref().map(|_| connection.observe_download(request));
149 let completion = async {
150 let response = connection.request(request);
151 tokio::pin!(response);
152 tokio::select! {
153 biased;
154 result = &mut response => result,
155 () = cancelled(cancel) => {
156 let request = fns::cancelDownloadFile { file_id: request.file_id, only_if_pending: false };
157 let cancellation = connection.request(&request).await;
158 match (cancellation, response.await) {
159 (_, result @ Ok(_)) => result,
160 (Ok(_), Err(Error::Td(_))) => Err(Error::Cancelled),
161 (Err(error), Err(_)) | (Ok(_), Err(error)) => Err(error),
162 }
163 }
164 }
165 };
166 let File::file(file) = with_progress(completion, samples, progress).await?;
167 Ok(file)
168 }
169}