Skip to main content

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}