Merge pull request 'dev/stricts' (#2) from dev/stricts into main

Reviewed-on: https://git.rustprogersteam.ru/Black_Raven/UIrclone/pulls/2
This commit is contained in:
Black_Raven committed 2026-03-27 09:21:22 +00:00
commit 51758c21bf
9 files changed
+563 -1

No files matched your search

+120
View File
@@ -0,0 +1,120 @@
use std::collections::HashMap;
/// === Модуль для работы с удаленными хранилищами ===
#[derive(Debug, Clone)]
pub struct Remote {
pub name: String,
pub r#type: String,
pub config: HashMap<String, String>,
}
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<String>,
}
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<String>,
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<String>,
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;
+17
View File
@@ -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<Vec<String>, 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)
}
+64
View File
@@ -0,0 +1,64 @@
use super::*;
use crate::rclone_install::RcloneApp;
use serde_json::Value;
/// Получить содержимое директории
pub fn list(app: &RcloneApp, remote_path: &str) -> Result<Vec<FileInfo>, String> {
let output = app.run_command(&["lsjson", remote_path])?;
let files: Vec<Value> = 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<HashMap<String, u64>, 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)
}
+26
View File
@@ -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<HashMap<String, String>, 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<Value, String> {
let output = app.run_command(&["core", "stats"])?;
let stats: Value =
serde_json::from_str(&output).map_err(|e| format!("Ошибка парсинга статистики: {}", e))?;
Ok(stats)
}
+29
View File
@@ -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");
}
+117
View File
@@ -0,0 +1,117 @@
use super::*;
use crate::rclone_install::RcloneApp;
use serde_json::Value;
/// Получить список всех удаленных хранилищ
pub fn list(app: &RcloneApp) -> Result<Vec<Remote>, 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<Remote, String> {
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<String, String>,
) -> 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<String, String>) -> 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<bool, String> {
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)
}
}
}
}
+45
View File
@@ -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<Vec<FileInfo>, String> {
let mut args = vec!["lsjson", path];
if options.recursive {
args.push("-R");
}
let output = app.run_command(&args)?;
let files: Vec<Value> =
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)
}
+144
View File
@@ -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<SyncStats, String> {
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<SyncStats, String> {
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<SyncStats, String> {
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<SyncStats, String> {
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<SyncStats, String> {
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::<f64>() {
stats.transfer_speed = speed_mb * 1024.0 * 1024.0;
}
}
}
break;
}
}
Ok(stats)
}
/// Получить список файлов, которые будут затронуты при синхронизации (dry-run)
pub fn preview(app: &RcloneApp, source: &str, dest: &str) -> Result<Vec<String>, 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)
}
+1 -1
View File
@@ -206,7 +206,7 @@ impl RcloneApp {
}) })
} }
fn run_command(&self, args: &[&str]) -> Result<String, String> { pub fn run_command(&self, args: &[&str]) -> Result<String, String> {
let output = Command::new(&self.rclone_path) let output = Command::new(&self.rclone_path)
.args(args) .args(args)
.output() .output()