cloudpub_sdk/
builder.rs

1use anyhow::{bail, Context, Result};
2use cloudpub_client::client::run_client;
3pub use cloudpub_client::config::{ClientConfig, ClientOpts};
4use cloudpub_common::config::MaskedString;
5use cloudpub_common::logging::init_log;
6use dirs::cache_dir;
7use futures::future::FutureExt;
8use parking_lot::RwLock;
9use std::path::{Path, PathBuf};
10use std::sync::Arc;
11use std::time::Duration;
12use tokio::sync::mpsc;
13use tracing::warn;
14
15use crate::connection::{CheckSignalFn, Connection, ConnectionEvent};
16
17/// Строитель для создания и настройки экземпляров `Connection`.
18///
19/// `ConnectionBuilder` предоставляет удобный интерфейс для настройки
20/// параметров соединения перед установлением соединения с сервером CloudPub.
21///
22/// # Пример
23///
24/// ```no_run
25/// # async fn example() -> anyhow::Result<()> {
26/// use cloudpub_sdk::Connection;
27/// use std::time::Duration;
28///
29/// let conn = Connection::builder()
30///     .config_path("/custom/config.toml")  // Пользовательский файл конфигурации
31///     .log_level("debug")                  // Установка уровня логирования
32///     .verbose(true)                       // Включить вывод в консоль
33///     .credentials("user@example.com", "password")  // Учетные данные для аутентификации
34///     .timeout(Duration::from_secs(30))    // Таймаут операций
35///     .build()
36///     .await?;
37/// # Ok(())
38/// # }
39/// ```
40///
41/// # Методы аутентификации
42///
43/// Строитель поддерживает два метода аутентификации:
44///
45/// 1. **На основе токена**: Используйте `token()` для аутентификации с существующим токеном
46/// 2. **Учетные данные**: Используйте `credentials()` или `email()`/`password()` для аутентификации по имени/паролю
47///
48/// # Значения по умолчанию
49///
50/// - Уровень логирования: "info"
51/// - Подробный вывод: false
52/// - Таймаут: 10 секунд
53/// - Путь к конфигурации: Системное расположение по умолчанию (~/.config/cloudpub/client.toml)
54pub struct ConnectionBuilder {
55    config_path: Option<PathBuf>,
56    log_level: String,
57    verbose: bool,
58    token: Option<String>,
59    email: Option<String>,
60    password: Option<String>,
61    timeout: Duration,
62    check_signal_fn: Option<CheckSignalFn>,
63}
64
65impl Default for ConnectionBuilder {
66    fn default() -> Self {
67        Self {
68            config_path: None,
69            log_level: "info".to_string(),
70            verbose: false,
71            token: None,
72            email: None,
73            password: None,
74            timeout: Duration::from_secs(10),
75            check_signal_fn: None,
76        }
77    }
78}
79
80impl ConnectionBuilder {
81    /// Создает новый builder с настройками по умолчанию.
82    ///
83    /// # Пример
84    ///
85    /// ```no_run
86    /// use cloudpub_sdk::ConnectionBuilder;
87    ///
88    /// let builder = ConnectionBuilder::new();
89    /// ```
90    pub fn new() -> Self {
91        Self::default()
92    }
93
94    /// Устанавливает путь к файлу конфигурации.
95    ///
96    /// По умолчанию SDK ищет файл конфигурации в стандартном системном расположении.
97    /// Используйте этот метод для указания пользовательского расположения.
98    ///
99    /// # Аргументы
100    ///
101    /// * `path` - Путь к файлу конфигурации
102    ///
103    /// # Пример
104    ///
105    /// ```no_run
106    /// # async fn example() -> anyhow::Result<()> {
107    /// use cloudpub_sdk::Connection;
108    /// use std::path::Path;
109    ///
110    /// let conn = Connection::builder()
111    ///     .config_path(Path::new("/etc/cloudpub/config.toml"))
112    ///     .build()
113    ///     .await?;
114    /// # Ok(())
115    /// # }
116    /// ```
117    pub fn config_path<P: AsRef<Path>>(mut self, path: P) -> Self {
118        self.config_path = Some(path.as_ref().to_path_buf());
119        self
120    }
121
122    /// Устанавливает уровень логирования для SDK.
123    ///
124    /// # Аргументы
125    ///
126    /// * `level` - Уровень логирования: "trace", "debug", "info", "warn", "error"
127    ///
128    /// # Пример
129    ///
130    /// ```no_run
131    /// # async fn example() -> anyhow::Result<()> {
132    /// use cloudpub_sdk::Connection;
133    ///
134    /// // Включить отладочное логирование
135    /// let conn = Connection::builder()
136    ///     .log_level("debug")
137    ///     .verbose(true)  // Также выводить в консоль
138    ///     .build()
139    ///     .await?;
140    /// # Ok(())
141    /// # }
142    /// ```
143    pub fn log_level<S: Into<String>>(mut self, level: S) -> Self {
144        self.log_level = level.into();
145        self
146    }
147
148    /// Включает или отключает подробное логирование в консоль.
149    ///
150    /// При включении сообщения логов выводятся в stderr в дополнение к файлу логов.
151    /// Это полезно для отладки и разработки.
152    ///
153    /// # Аргументы
154    ///
155    /// * `verbose` - true для включения вывода в консоль, false для отключения
156    ///
157    /// # Пример
158    ///
159    /// ```no_run
160    /// # async fn example() -> anyhow::Result<()> {
161    /// use cloudpub_sdk::Connection;
162    ///
163    /// let conn = Connection::builder()
164    ///     .verbose(true)  // Включить вывод в консоль
165    ///     .build()
166    ///     .await?;
167    /// # Ok(())
168    /// # }
169    /// ```
170    pub fn verbose(mut self, verbose: bool) -> Self {
171        self.verbose = verbose;
172        self
173    }
174
175    /// Устанавливает токен аутентификации.
176    ///
177    /// Используйте этот метод для аутентификации на основе токена. Это взаимоисключающе
178    /// с аутентификацией на основе учетных данных.
179    ///
180    /// # Аргументы
181    ///
182    /// * `token` - Токен аутентификации, полученный при предыдущем входе
183    ///
184    /// # Пример
185    ///
186    /// ```no_run
187    /// # async fn example() -> anyhow::Result<()> {
188    /// use cloudpub_sdk::Connection;
189    ///
190    /// let conn = Connection::builder()
191    ///     .token("your-auth-token-here")
192    ///     .build()
193    ///     .await?;
194    /// # Ok(())
195    /// # }
196    /// ```
197    pub fn token<S: Into<String>>(mut self, token: S) -> Self {
198        self.token = Some(token.into());
199        self.email = None;
200        self.password = None;
201        self
202    }
203
204    /// Устанавливает email и пароль для аутентификации.
205    ///
206    /// Используйте этот метод для аутентификации на основе учетных данных. Это взаимоисключающе
207    /// с аутентификацией на основе токена.
208    ///
209    /// # Аргументы
210    ///
211    /// * `email` - Email адрес пользователя
212    /// * `password` - Пароль пользователя
213    ///
214    /// # Пример
215    ///
216    /// ```no_run
217    /// # async fn example() -> anyhow::Result<()> {
218    /// use cloudpub_sdk::Connection;
219    ///
220    /// let conn = Connection::builder()
221    ///     .credentials("user@example.com", "secure-password")
222    ///     .build()
223    ///     .await?;
224    /// # Ok(())
225    /// # }
226    /// ```
227    pub fn credentials<S: Into<String>>(mut self, email: S, password: S) -> Self {
228        self.email = Some(email.into());
229        self.password = Some(password.into());
230        self.token = None;
231        self
232    }
233
234    /// Устанавливает только email адрес.
235    ///
236    /// Должен использоваться в сочетании с `password()`. Это полезно, когда
237    /// учетные данные получаются отдельно.
238    ///
239    /// # Аргументы
240    ///
241    /// * `email` - Email адрес пользователя
242    ///
243    /// # Пример
244    ///
245    /// ```no_run
246    /// # async fn example() -> anyhow::Result<()> {
247    /// use cloudpub_sdk::Connection;
248    ///
249    /// let conn = Connection::builder()
250    ///     .email("user@example.com")
251    ///     .password("secure-password")
252    ///     .build()
253    ///     .await?;
254    /// # Ok(())
255    /// # }
256    /// ```
257    pub fn email<S: Into<String>>(mut self, email: S) -> Self {
258        self.email = Some(email.into());
259        self.token = None;
260        self
261    }
262
263    /// Устанавливает только пароль.
264    ///
265    /// Должен использоваться в сочетании с `email()`. Это полезно, когда
266    /// учетные данные получаются отдельно.
267    ///
268    /// # Аргументы
269    ///
270    /// * `password` - Пароль пользователя
271    ///
272    /// # Пример
273    ///
274    /// ```no_run
275    /// # async fn example() -> anyhow::Result<()> {
276    /// use cloudpub_sdk::Connection;
277    ///
278    /// let email = std::env::var("CLOUDPUB_EMAIL")?;
279    /// let password = std::env::var("CLOUDPUB_PASSWORD")?;
280    ///
281    /// let conn = Connection::builder()
282    ///     .email(email)
283    ///     .password(password)
284    ///     .build()
285    ///     .await?;
286    /// # Ok(())
287    /// # }
288    /// ```
289    pub fn password<S: Into<String>>(mut self, password: S) -> Self {
290        self.password = Some(password.into());
291        self.token = None;
292        self
293    }
294
295    /// Устанавливает таймаут для операций.
296    ///
297    /// Этот таймаут применяется ко всем асинхронным операциям, таким как register, publish, ls и т.д.
298    /// По умолчанию 10 секунд.
299    ///
300    /// # Аргументы
301    ///
302    /// * `timeout` - Продолжительность таймаута операции
303    ///
304    /// # Пример
305    ///
306    /// ```no_run
307    /// # async fn example() -> anyhow::Result<()> {
308    /// use cloudpub_sdk::Connection;
309    /// use std::time::Duration;
310    ///
311    /// let conn = Connection::builder()
312    ///     .timeout(Duration::from_secs(60))  // Таймаут 1 минута
313    ///     .build()
314    ///     .await?;
315    /// # Ok(())
316    /// # }
317    /// ```
318    pub fn timeout(mut self, timeout: Duration) -> Self {
319        self.timeout = timeout;
320        self
321    }
322
323    /// Устанавливает таймаут в секундах.
324    ///
325    /// Удобный метод для установки таймаута в секундах вместо Duration.
326    ///
327    /// # Аргументы
328    ///
329    /// * `secs` - Таймаут в секундах
330    ///
331    /// # Пример
332    ///
333    /// ```no_run
334    /// # async fn example() -> anyhow::Result<()> {
335    /// use cloudpub_sdk::Connection;
336    ///
337    /// let conn = Connection::builder()
338    ///     .timeout_secs(30)  // Таймаут 30 секунд
339    ///     .build()
340    ///     .await?;
341    /// # Ok(())
342    /// # }
343    /// ```
344    pub fn timeout_secs(mut self, secs: u64) -> Self {
345        self.timeout = Duration::from_secs(secs);
346        self
347    }
348
349    /// Устанавливает функцию для проверки сигналов прерывания.
350    ///
351    /// Это в основном используется языковыми обертками (например, Python) для проверки
352    /// сигналов, таких как Ctrl+C, во время долговременных операций.
353    ///
354    /// # Аргументы
355    ///
356    /// * `check_fn` - Функция, которая возвращает ошибку, если операция должна быть прервана
357    ///
358    /// # Пример
359    ///
360    /// ```no_run
361    /// # async fn example() -> anyhow::Result<()> {
362    /// use cloudpub_sdk::{Connection, CheckSignalFn};
363    /// use std::sync::Arc;
364    /// use std::sync::atomic::{AtomicBool, Ordering};
365    ///
366    /// let interrupted = Arc::new(AtomicBool::new(false));
367    /// let interrupted_clone = interrupted.clone();
368    ///
369    /// let check_signal: CheckSignalFn = Arc::new(move || {
370    ///     if interrupted_clone.load(Ordering::Relaxed) {
371    ///         anyhow::bail!("Operation interrupted")
372    ///     }
373    ///     Ok(())
374    /// });
375    ///
376    /// let conn = Connection::builder()
377    ///     .check_signal_fn(check_signal)
378    ///     .build()
379    ///     .await?;
380    ///
381    /// # Ok(())
382    /// # }
383    /// ```
384    pub fn check_signal_fn(mut self, check_fn: CheckSignalFn) -> Self {
385        self.check_signal_fn = Some(check_fn);
386        self
387    }
388
389    /// Создает и устанавливает соединение с сервером CloudPub.
390    ///
391    /// Этот метод:
392    /// 1. Проверяет конфигурацию
393    /// 2. Инициализирует логирование
394    /// 3. Загружает или создает файл конфигурации
395    /// 4. Аутентифицируется на сервере (если предоставлены учетные данные)
396    /// 5. Устанавливает соединение
397    /// 6. Ожидает готовности соединения
398    ///
399    /// # Возвращает
400    ///
401    /// Возвращает экземпляр `Connection` при успехе, или ошибку, если:
402    /// - Неверная конфигурация (например, email без пароля)
403    /// - Неудачная аутентификация
404    /// - Не удается установить соединение
405    /// - Происходит таймаут
406    ///
407    /// # Пример
408    ///
409    /// ```no_run
410    /// # async fn example() -> anyhow::Result<()> {
411    /// use cloudpub_sdk::Connection;
412    ///
413    /// // Создание с настройками по умолчанию
414    /// let conn = Connection::builder().build().await?;
415    ///
416    /// // Создание с пользовательской конфигурацией
417    /// let conn = Connection::builder()
418    ///     .credentials("user@example.com", "password")
419    ///     .log_level("debug")
420    ///     .verbose(true)
421    ///     .timeout_secs(30)
422    ///     .build()
423    ///     .await?;
424    /// # Ok(())
425    /// # }
426    /// ```
427    ///
428    /// # Ошибки
429    ///
430    /// Этот метод вернет ошибку, если:
431    /// - Email предоставлен без пароля или наоборот
432    /// - Не удалось инициализировать логирование
433    /// - Не удалось загрузить или создать файл конфигурации
434    /// - Неудачная аутентификация
435    /// - Неудачное соединение с сервером
436    /// - Таймаут ожидания соединения
437    pub async fn build(self) -> Result<Connection> {
438        // Проверяем аутентификацию, если она предоставлена
439        if self.email.is_some() && self.password.is_none() {
440            bail!("Password is required when email is provided");
441        }
442        if self.password.is_some() && self.email.is_none() {
443            bail!("Email is required when password is provided");
444        }
445
446        // Увеличиваем лимит `nofile` на Linux и Mac
447        if let Err(err) = fdlimit::raise_fd_limit() {
448            warn!("Failed to raise file descriptor limit: {}", err);
449        }
450
451        // Создаем директорию для логов
452        let log_dir = cache_dir().context("Can't get cache dir")?.join("cloudpub");
453        std::fs::create_dir_all(&log_dir).context("Can't create log dir")?;
454
455        let log_file = log_dir.join("client.log");
456
457        // Инициализируем логирование
458        let _guard = init_log(
459            &self.log_level,
460            &log_file,
461            self.verbose,
462            10 * 1024 * 1024,
463            2,
464        )
465        .context("Failed to initialize logging")?;
466
467        // Загружаем конфигурацию
468        let mut config = if let Some(ref path) = self.config_path {
469            ClientConfig::from_file(path, false)?
470        } else {
471            ClientConfig::load("client.toml", true, false)?
472        };
473
474        // Обрабатываем аутентификацию по токену
475        if let Some(token_value) = self.token {
476            config.token = Some(MaskedString(token_value));
477        }
478
479        let config = Arc::new(RwLock::new(config));
480
481        // Настраиваем каналы
482        let (command_tx, command_rx) = mpsc::channel(1024);
483        let (result_tx, result_rx) = mpsc::channel(1024);
484
485        // Определяем опции на основе аутентификации
486        let opts = if let Some(email_value) = self.email {
487            if let Some(password_value) = self.password {
488                ClientOpts {
489                    credentials: Some((email_value, password_value)),
490                    ..Default::default()
491                }
492            } else {
493                // Это не должно произойти из-за проверки выше
494                ClientOpts::default()
495            }
496        } else {
497            ClientOpts::default()
498        };
499
500        // Клонируем то, что нам нужно для задачи клиента
501        let config_clone = config.clone();
502
503        // Запускаем задачу клиента
504        let client_handle = tokio::spawn(async move {
505            if let Err(err) = run_client(config_clone, opts, command_rx, result_tx)
506                .boxed()
507                .await
508            {
509                tracing::error!("Client exited with error: {:?}, restarting in 5 sec..", err);
510            }
511        });
512
513        // Создаем соединение со встроенным управлением событиями
514        let connection = Connection::new(
515            config,
516            command_tx,
517            result_rx,
518            self.timeout,
519            self.check_signal_fn,
520            _guard,
521            Some(client_handle),
522        );
523
524        // Ожидаем установления соединения
525        connection
526            .wait_for_event(|event| matches!(event, ConnectionEvent::Connected))
527            .await?;
528
529        Ok(connection)
530    }
531}