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}