From 1239acbe95dddb6199d5e26e741c7c8deb89be07 Mon Sep 17 00:00:00 2001 From: Kirill Date: Mon, 2 Mar 2026 16:30:30 +0300 Subject: [PATCH 01/15] fix(RcloneApp): run_command is publick now --- src/rclone_install/rclone_install_real.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rclone_install/rclone_install_real.rs b/src/rclone_install/rclone_install_real.rs index 2e7f90c..e487400 100644 --- a/src/rclone_install/rclone_install_real.rs +++ b/src/rclone_install/rclone_install_real.rs @@ -206,7 +206,7 @@ impl RcloneApp { }) } - fn run_command(&self, args: &[&str]) -> Result { + pub fn run_command(&self, args: &[&str]) -> Result { let output = Command::new(&self.rclone_path) .args(args) .output() From bb74d01b757ea851d16cf961b1401ff55170b14c Mon Sep 17 00:00:00 2001 From: Kirill Date: Mon, 2 Mar 2026 16:32:48 +0300 Subject: [PATCH 02/15] feat(remote): Added data struct for work with remote dist --- src/operations.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/operations.rs diff --git a/src/operations.rs b/src/operations.rs new file mode 100644 index 0000000..0b5e03d --- /dev/null +++ b/src/operations.rs @@ -0,0 +1,13 @@ +use std::collections::HashMap; + +/// === Модуль для работы с удаленными хранилищами === + +#[derive(Debug, Clone)] +pub struct Remote { + pub name: String, + pub r#type: String, + pub config: HashMap, +} + +pub mod remotes; + From 5d0c5b2b1e25d09dbedb6ca3ef99cfa1477276c3 Mon Sep 17 00:00:00 2001 From: Kirill Date: Mon, 2 Mar 2026 16:34:50 +0300 Subject: [PATCH 03/15] feat(files): Data strict for work with data and files --- src/operations.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/operations.rs b/src/operations.rs index 0b5e03d..1558ac7 100644 --- a/src/operations.rs +++ b/src/operations.rs @@ -11,3 +11,16 @@ pub struct Remote { pub mod remotes; +/// === Модуль для работы с файлами и директориями === + +#[derive(Debug, Clone)] +pub struct FileInfo { + pub name: String, + pub path: String, + pub size: u64, + pub is_dir: bool, + pub modified: Option, +} + +pub mod files; + From 237790651b1af042fcd26454936d16919fc495ee Mon Sep 17 00:00:00 2001 From: Kirill Date: Mon, 2 Mar 2026 16:37:03 +0300 Subject: [PATCH 04/15] feat(sync): Added modules for operations and sync --- src/operations.rs | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/operations.rs b/src/operations.rs index 1558ac7..9a3538e 100644 --- a/src/operations.rs +++ b/src/operations.rs @@ -24,3 +24,47 @@ pub struct FileInfo { pub mod files; +/// === Модуль для операций синхронизации === + +#[derive(Debug, Clone)] +pub struct SyncStats { + pub transferred: u64, + pub files: u64, + pub errors: u64, + pub checks: u64, + pub elapsed_time: f64, + pub transfer_speed: f64, // в байтах в секунду +} + +#[derive(Debug, Default)] +pub struct CopyOptions { + pub verbose: bool, + pub dry_run: bool, + pub bandwidth_limit: Option, + pub no_traverse: bool, +} + +#[derive(Debug, Default)] +pub struct SyncOptions { + pub verbose: bool, + pub dry_run: bool, + pub delete_excluded: bool, + pub bandwidth_limit: Option, + pub existing_only: bool, +} + +#[derive(Debug, Default)] +pub struct MoveOptions { + pub verbose: bool, + pub dry_run: bool, + pub delete_empty_src_dirs: bool, +} + +#[derive(Debug, Default)] +pub struct DeleteOptions { + pub verbose: bool, + pub dry_run: bool, + pub recursive: bool, +} + +pub mod sync; From b814314503311c85c4666475d4d7ee22eed567ad Mon Sep 17 00:00:00 2001 From: Kirill Date: Mon, 2 Mar 2026 16:38:14 +0300 Subject: [PATCH 05/15] feat(search): Added data strict for search --- src/operations.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/operations.rs b/src/operations.rs index 9a3538e..0481688 100644 --- a/src/operations.rs +++ b/src/operations.rs @@ -68,3 +68,12 @@ pub struct DeleteOptions { } pub mod sync; +/// === Модуль для поиска === + +#[derive(Debug, Default)] +pub struct FindOptions { + pub recursive: bool, + pub max_results: usize, +} + +pub mod search; From 1e1d452b662a85335844df5709ee0d8f1c4c86a8 Mon Sep 17 00:00:00 2001 From: Kirill Date: Mon, 2 Mar 2026 16:46:13 +0300 Subject: [PATCH 06/15] feat(file): Added implementation for strict to better data view in strict --- src/operations.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/operations.rs b/src/operations.rs index 0481688..f29084a 100644 --- a/src/operations.rs +++ b/src/operations.rs @@ -22,6 +22,25 @@ pub struct FileInfo { pub modified: Option, } +impl FileInfo { + pub fn format_size(&self) -> String { + let bytes = self.size; + if bytes < 1024 { + format!("{} B", bytes) + } else if bytes < 1024 * 1024 { + format!("{:.1} KB", bytes as f64 / 1024.0) + } else if bytes < 1024 * 1024 * 1024 { + format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0)) + } else { + format!("{:.2} GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0)) + } + } + + pub fn icon(&self) -> &'static str { + if self.is_dir { "📁" } else { "📄" } + } +} + pub mod files; /// === Модуль для операций синхронизации === @@ -68,6 +87,7 @@ pub struct DeleteOptions { } pub mod sync; + /// === Модуль для поиска === #[derive(Debug, Default)] From e42ab8469e342e59e29fff9bb7877b2749c94058 Mon Sep 17 00:00:00 2001 From: Kirill Date: Mon, 2 Mar 2026 16:48:21 +0300 Subject: [PATCH 07/15] feat(sync): Added implementation for better data view in strict --- src/operations.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/operations.rs b/src/operations.rs index f29084a..264deea 100644 --- a/src/operations.rs +++ b/src/operations.rs @@ -55,6 +55,21 @@ pub struct SyncStats { pub transfer_speed: f64, // в байтах в секунду } +impl SyncStats { + pub fn format_speed(&self) -> String { + let speed = self.transfer_speed; + if speed < 1024.0 { + format!("{:.0} B/s", speed) + } else if speed < 1024.0 * 1024.0 { + format!("{:.1} KB/s", speed / 1024.0) + } else if speed < 1024.0 * 1024.0 * 1024.0 { + format!("{:.1} MB/s", speed / (1024.0 * 1024.0)) + } else { + format!("{:.2} GB/s", speed / (1024.0 * 1024.0 * 1024.0)) + } + } +} + #[derive(Debug, Default)] pub struct CopyOptions { pub verbose: bool, From 4a6eb1b9b11ea6bc75a648d7c4d713129b172e0b Mon Sep 17 00:00:00 2001 From: Kirill Date: Mon, 2 Mar 2026 17:35:26 +0300 Subject: [PATCH 08/15] feat(remote): Added Functions: list - Get list of remote storage get_config - Get config of specific remote create - Create new storage update - Update configuration of remote delete - Remove remote --- src/operations/remotes.rs | 117 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 src/operations/remotes.rs diff --git a/src/operations/remotes.rs b/src/operations/remotes.rs new file mode 100644 index 0000000..f9a5767 --- /dev/null +++ b/src/operations/remotes.rs @@ -0,0 +1,117 @@ +use super::*; +use crate::rclone_install::RcloneApp; +use serde_json::Value; + +/// Получить список всех удаленных хранилищ +pub fn list(app: &RcloneApp) -> Result, String> { + let output = app.run_command(&["listremotes"])?; + + let mut remotes = Vec::new(); + for line in output.lines() { + let name = line.trim_end_matches(':'); + if !name.is_empty() { + // Получаем детальную информацию о каждом remote + if let Ok(config) = get_config(app, name) { + remotes.push(config); + } else { + // Если не удалось получить конфиг, добавляем базовую информацию + remotes.push(Remote { + name: name.to_string(), + r#type: "unknown".to_string(), + config: HashMap::new(), + }); + } + } + } + + Ok(remotes) +} + +/// Получить конфигурацию конкретного remote +pub fn get_config(app: &RcloneApp, remote_name: &str) -> Result { + let output = app.run_command(&["config", "dump"])?; + + let configs: Value = serde_json::from_str(&output) + .map_err(|e| format!("Ошибка парсинга конфигурации: {}", e))?; + + if let Some(remote_config) = configs.get(remote_name) { + let r#type = remote_config + .get("type") + .and_then(|v| v.as_str()) + .unwrap_or("unknown") + .to_string(); + + let mut config_map = HashMap::new(); + if let Some(obj) = remote_config.as_object() { + for (key, value) in obj { + if key != "type" { + if let Some(val_str) = value.as_str() { + config_map.insert(key.clone(), val_str.to_string()); + } else { + config_map.insert(key.clone(), value.to_string()); + } + } + } + } + + Ok(Remote { + name: remote_name.to_string(), + r#type, + config: config_map, + }) + } else { + Err(format!("Remote '{}' не найден", remote_name)) + } +} + +/// Создать новое удаленное хранилище +pub fn create( + app: &RcloneApp, + name: &str, + r#type: &str, + config: &HashMap, +) -> Result<(), String> { + let mut args = vec!["config", "create", name, r#type]; + + for (key, value) in config { + args.push(key.as_str()); + args.push(value.as_str()); + } + + app.run_command(&args)?; + Ok(()) +} + +/// Обновить конфигурацию remote +pub fn update(app: &RcloneApp, name: &str, config: &HashMap) -> Result<(), String> { + let mut args = vec!["config", "update", name]; + + for (key, value) in config { + args.push(key.as_str()); + args.push(value.as_str()); + } + + app.run_command(&args)?; + Ok(()) +} + +/// Удалить remote +pub fn delete(app: &RcloneApp, name: &str) -> Result<(), String> { + app.run_command(&["config", "delete", name])?; + Ok(()) +} + +/// Проверить доступность remote +pub fn check(app: &RcloneApp, remote_name: &str) -> Result { + let result = app.run_command(&["lsd", remote_name, "--max-depth", "1"]); + match result { + Ok(_) => Ok(true), + Err(e) => { + if e.contains("directory not found") { + Ok(true) // Remote существует, но пустой + } else { + Err(e) + } + } + } +} From addd1802ff98124c8dec771ffa287a1f4de77138 Mon Sep 17 00:00:00 2001 From: Kirill Date: Mon, 2 Mar 2026 17:39:13 +0300 Subject: [PATCH 09/15] feat(files): add functions: list - get storage data size - get storage data and file count about - get storage info --- src/operations/files.rs | 64 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 src/operations/files.rs diff --git a/src/operations/files.rs b/src/operations/files.rs new file mode 100644 index 0000000..dc580f1 --- /dev/null +++ b/src/operations/files.rs @@ -0,0 +1,64 @@ +use super::*; +use crate::rclone_install::RcloneApp; +use serde_json::Value; + +/// Получить содержимое директории +pub fn list(app: &RcloneApp, remote_path: &str) -> Result, String> { + let output = app.run_command(&["lsjson", remote_path])?; + + let files: Vec = serde_json::from_str(&output) + .map_err(|e| format!("Ошибка парсинга списка файлов: {}", e))?; + + let mut file_infos = Vec::new(); + for item in files { + let name = item["Name"].as_str().unwrap_or("").to_string(); + let path = item["Path"].as_str().unwrap_or("").to_string(); + let size = item["Size"].as_u64().unwrap_or(0); + let is_dir = item["IsDir"].as_bool().unwrap_or(false); + let modified = item["ModTime"].as_str().map(String::from); + + file_infos.push(FileInfo { + name, + path, + size, + is_dir, + modified, + }); + } + + Ok(file_infos) +} + +/// Получить размер и количество файлов +pub fn size(app: &RcloneApp, path: &str) -> Result<(u64, u64), String> { + let output = app.run_command(&["size", path, "--json"])?; + + let size_info: Value = + serde_json::from_str(&output).map_err(|e| format!("Ошибка парсинга размера: {}", e))?; + + let count = size_info["count"].as_u64().unwrap_or(0); + let bytes = size_info["bytes"].as_u64().unwrap_or(0); + + Ok((bytes, count)) +} + +/// Получить информацию о хранилище (общий объем, использовано, свободно) +pub fn about(app: &RcloneApp, remote: &str) -> Result, String> { + let output = app.run_command(&["about", remote, "--json"])?; + + let about_info: Value = serde_json::from_str(&output) + .map_err(|e| format!("Ошибка парсинга информации о хранилище: {}", e))?; + + let mut info = HashMap::new(); + if let Some(obj) = about_info.as_object() { + for (key, value) in obj { + if let Some(num) = value.as_u64() { + info.insert(key.clone(), num); + } else if let Some(num) = value.as_f64() { + info.insert(key.clone(), num as u64); + } + } + } + + Ok(info) +} From 7114a2cab32c737e9f9aaf2e5d39cfb650a57f0e Mon Sep 17 00:00:00 2001 From: Kirill Date: Mon, 2 Mar 2026 17:41:01 +0300 Subject: [PATCH 10/15] feat(sync): added functions: copy - copy file or directories sync - synchronize directories move_files - move files or directories delete - remove files or directories parse_sync_stats - parcing of synchronization statistics from output preview - get file list wich will be touched by synchronization --- src/operations/sync.rs | 144 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 src/operations/sync.rs diff --git a/src/operations/sync.rs b/src/operations/sync.rs new file mode 100644 index 0000000..70a8e8d --- /dev/null +++ b/src/operations/sync.rs @@ -0,0 +1,144 @@ +use super::*; +use crate::rclone_install::RcloneApp; +use serde_json::Value; + +/// Скопировать файлы/директории +pub fn copy( + app: &RcloneApp, + source: &str, + dest: &str, + options: &CopyOptions, +) -> Result { + let mut args = vec!["copy", source, dest, "--progress", "--stats-one-line"]; + + if options.verbose { + args.push("-v"); + } + if options.dry_run { + args.push("--dry-run"); + } + if let Some(bandwidth) = &options.bandwidth_limit { + args.push("--bwlimit"); + args.push(bandwidth.as_str()); + } + if options.no_traverse { + args.push("--no-traverse"); + } + + let output = app.run_command(&args)?; + parse_sync_stats(&output) +} + +/// Синхронизировать директории (dest станет идентичной source) +pub fn sync( + app: &RcloneApp, + source: &str, + dest: &str, + options: &SyncOptions, +) -> Result { + let mut args = vec!["sync", source, dest, "--progress", "--stats-one-line"]; + + if options.verbose { + args.push("-v"); + } + if options.dry_run { + args.push("--dry-run"); + } + if options.delete_excluded { + args.push("--delete-excluded"); + } + if let Some(bandwidth) = &options.bandwidth_limit { + args.push("--bwlimit"); + args.push(bandwidth.as_str()); + } + if options.existing_only { + args.push("--existing-only"); + } + + let output = app.run_command(&args)?; + parse_sync_stats(&output) +} + +/// Переместить файлы/директории +pub fn move_files( + app: &RcloneApp, + source: &str, + dest: &str, + options: &MoveOptions, +) -> Result { + let mut args = vec!["move", source, dest, "--progress", "--stats-one-line"]; + + if options.verbose { + args.push("-v"); + } + if options.dry_run { + args.push("--dry-run"); + } + if options.delete_empty_src_dirs { + args.push("--delete-empty-src-dirs"); + } + + let output = app.run_command(&args)?; + parse_sync_stats(&output) +} + +/// Удалить файлы/директории +pub fn delete(app: &RcloneApp, path: &str, options: &DeleteOptions) -> Result { + let command = if options.recursive { "purge" } else { "delete" }; + + let mut args = vec![command, path, "--progress"]; + + if options.verbose { + args.push("-v"); + } + if options.dry_run { + args.push("--dry-run"); + } + + let output = app.run_command(&args)?; + parse_sync_stats(&output) +} + +/// Парсинг статистики синхронизации из вывода +fn parse_sync_stats(output: &str) -> Result { + let mut stats = SyncStats { + transferred: 0, + files: 0, + errors: 0, + checks: 0, + elapsed_time: 0.0, + transfer_speed: 0.0, + }; + + // Парсим последнюю строку со статистикой + for line in output.lines().rev() { + if line.contains("Transferred:") { + // Пример: "Transferred: 5 / 5, 100%, 1.234 MB/s, ETA 0s" + if let Some(speed_part) = line.split(',').nth(2) { + if speed_part.contains("MB/s") { + let speed_str = speed_part.trim().replace("MB/s", "").trim().to_string(); + if let Ok(speed_mb) = speed_str.parse::() { + stats.transfer_speed = speed_mb * 1024.0 * 1024.0; + } + } + } + break; + } + } + + Ok(stats) +} + +/// Получить список файлов, которые будут затронуты при синхронизации (dry-run) +pub fn preview(app: &RcloneApp, source: &str, dest: &str) -> Result, String> { + let output = app.run_command(&["copy", source, dest, "--dry-run", "--progress"])?; + + let mut files = Vec::new(); + for line in output.lines() { + if line.contains(":") && !line.contains("Transferred:") && !line.contains("Errors:") { + files.push(line.to_string()); + } + } + + Ok(files) +} From 24c9d2e5a9571f586ce42c7b355a501b5b5c9ce3 Mon Sep 17 00:00:00 2001 From: Kirill Date: Mon, 2 Mar 2026 17:45:06 +0300 Subject: [PATCH 11/15] feat(search): Add functions: by_name - search file by name --- src/operations/search.rs | 45 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/operations/search.rs diff --git a/src/operations/search.rs b/src/operations/search.rs new file mode 100644 index 0000000..af8ed73 --- /dev/null +++ b/src/operations/search.rs @@ -0,0 +1,45 @@ +use super::*; +use crate::rclone_install::RcloneApp; +use serde_json::Value; + +/// Найти файлы по имени +pub fn by_name( + app: &RcloneApp, + path: &str, + pattern: &str, + options: &FindOptions, +) -> Result, String> { + let mut args = vec!["lsjson", path]; + + if options.recursive { + args.push("-R"); + } + + let output = app.run_command(&args)?; + let files: Vec = + serde_json::from_str(&output).map_err(|e| format!("Ошибка парсинга: {}", e))?; + + let pattern = pattern.to_lowercase(); + let mut results = Vec::new(); + + for item in files { + let name = item["Name"].as_str().unwrap_or(""); + let path = item["Path"].as_str().unwrap_or(""); + + if name.to_lowercase().contains(&pattern) || path.to_lowercase().contains(&pattern) { + results.push(FileInfo { + name: name.to_string(), + path: path.to_string(), + size: item["Size"].as_u64().unwrap_or(0), + is_dir: item["IsDir"].as_bool().unwrap_or(false), + modified: item["ModTime"].as_str().map(String::from), + }); + } + + if results.len() >= options.max_results { + break; + } + } + + Ok(results) +} From 559af7d594c4b4506086fb3d0de6fa1d830da61c Mon Sep 17 00:00:00 2001 From: Kirill Date: Fri, 27 Mar 2026 12:05:35 +0300 Subject: [PATCH 12/15] feat(checks): Add new finction diff - function which cheks differencies between 2 repo --- src/operations/check.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/operations/check.rs diff --git a/src/operations/check.rs b/src/operations/check.rs new file mode 100644 index 0000000..423b808 --- /dev/null +++ b/src/operations/check.rs @@ -0,0 +1,17 @@ +use super::*; +use crate::rclone_install::RcloneApp; +use serde_json::Value; + +/// Проверить разницу между двумя директориями +pub fn diff(app: &RcloneApp, source: &str, dest: &str) -> Result, String> { + let output = app.run_command(&["check", source, dest, "--combined", "-"])?; + + let mut differences = Vec::new(); + for line in output.lines() { + if !line.is_empty() { + differences.push(line.to_string()); + } + } + + Ok(differences) +} From 3bcfbe573eb87dc5ebef80b5c855090a745da9ae Mon Sep 17 00:00:00 2001 From: Kirill Date: Fri, 27 Mar 2026 12:06:48 +0300 Subject: [PATCH 13/15] feat(info): Add new functions version - send info about rclone version stats - check info about operation --- src/operations/info.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/operations/info.rs diff --git a/src/operations/info.rs b/src/operations/info.rs new file mode 100644 index 0000000..b9a3d83 --- /dev/null +++ b/src/operations/info.rs @@ -0,0 +1,26 @@ +use super::*; +use crate::rclone_install::RcloneApp; +use serde_json::Value; +use std::collections::HashMap; + +/// Получить версию в удобочитаемом формате +pub fn version(app: &RcloneApp) -> Result, String> { + let output = app.run_command(&["version"])?; + let mut version_info = HashMap::new(); + + for line in output.lines() { + if let Some((key, value)) = line.split_once(':') { + version_info.insert(key.trim().to_string(), value.trim().to_string()); + } + } + + Ok(version_info) +} + +/// Получить статистику текущих операций +pub fn stats(app: &RcloneApp) -> Result { + let output = app.run_command(&["core", "stats"])?; + let stats: Value = + serde_json::from_str(&output).map_err(|e| format!("Ошибка парсинга статистики: {}", e))?; + Ok(stats) +} From a07396c5767d11d6925f6376823fbe7ced6f869a Mon Sep 17 00:00:00 2001 From: Kirill Date: Fri, 27 Mar 2026 12:08:45 +0300 Subject: [PATCH 14/15] feat(operations): add header file --- src/operations.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/operations.rs b/src/operations.rs index 264deea..95ca596 100644 --- a/src/operations.rs +++ b/src/operations.rs @@ -112,3 +112,9 @@ pub struct FindOptions { } pub mod search; + +pub mod check; +pub mod info; + +#[cfg(test)] +mod operations_tests; From 53bc624b02c4f54fd03adff7ce3ad7c56bc7e591 Mon Sep 17 00:00:00 2001 From: Kirill Date: Fri, 27 Mar 2026 12:09:02 +0300 Subject: [PATCH 15/15] tests: Operation tests --- src/operations/operations_tests.rs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/operations/operations_tests.rs diff --git a/src/operations/operations_tests.rs b/src/operations/operations_tests.rs new file mode 100644 index 0000000..0ed7ecc --- /dev/null +++ b/src/operations/operations_tests.rs @@ -0,0 +1,29 @@ +use crate::operations::{FileInfo, SyncStats}; + +#[test] +fn test_file_info_formatting() { + let file = FileInfo { + name: "test.txt".to_string(), + path: "/path/test.txt".to_string(), + size: 1500, + is_dir: false, + modified: None, + }; + + assert_eq!(file.format_size(), "1.5 KB"); + assert_eq!(file.icon(), "📄"); +} + +#[test] +fn test_sync_stats_formatting() { + let stats = SyncStats { + transferred: 1024, + files: 10, + errors: 0, + checks: 100, + elapsed_time: 5.0, + transfer_speed: 2_500_000.0, // Добавлен .0 для f64 + }; + + assert_eq!(stats.format_speed(), "2.4 MB/s"); +}