diff --git a/src/operations.rs b/src/operations.rs new file mode 100644 index 0000000..95ca596 --- /dev/null +++ b/src/operations.rs @@ -0,0 +1,120 @@ +use std::collections::HashMap; + +/// === Модуль для работы с удаленными хранилищами === + +#[derive(Debug, Clone)] +pub struct Remote { + pub name: String, + pub r#type: String, + pub config: HashMap, +} + +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, +} + +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; + +/// === Модуль для операций синхронизации === + +#[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, // в байтах в секунду +} + +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, + 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; + +/// === Модуль для поиска === + +#[derive(Debug, Default)] +pub struct FindOptions { + pub recursive: bool, + pub max_results: usize, +} + +pub mod search; + +pub mod check; +pub mod info; + +#[cfg(test)] +mod operations_tests; 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) +} 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) +} 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) +} 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"); +} 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) + } + } + } +} 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) +} 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) +} 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()