Merge pull request 'dev/install_rclone' (#1) from dev/install_rclone into main
Reviewed-on: https://git.rustprogersteam.ru/Black_Raven/UIrclone/pulls/1
This commit is contained in:
commit
b61706fcc7
7 files changed
+2800
-2
No files matched your search
@@ -0,0 +1,43 @@
|
|||||||
|
name: Rust CI
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Rust
|
||||||
|
run: |
|
||||||
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal
|
||||||
|
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
|
- name: Verify Rust installation
|
||||||
|
run: |
|
||||||
|
rustc --version
|
||||||
|
cargo --version
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: cargo build --verbose
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: cargo test --verbose
|
||||||
|
|
||||||
|
- name: Build documentation
|
||||||
|
run: cargo doc --no-deps --document-private-items
|
||||||
|
|
||||||
|
- name: Create docs directory
|
||||||
|
run: mkdir -p ./docs-output && cp -r target/doc/* ./docs-output/
|
||||||
|
|
||||||
|
- name: Upload documentation as artifact
|
||||||
|
uses: actions/upload-artifact@v3 # Понижена версия для совместимости с GHES
|
||||||
|
with:
|
||||||
|
name: rust-documentation
|
||||||
|
path: ./docs-output/
|
||||||
|
retention-days: 30
|
||||||
|
if-no-files-found: error
|
||||||
Generated
+2302
File diff suppressed because it is too large.
Load diff
+14
@@ -4,3 +4,17 @@ version = "0.1.0"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
tokio = { version = "1.0", features = ["full"] }
|
||||||
|
reqwest = { version = "0.11", default-features = false, features = ["rustls-tls", "json", "stream"] }
|
||||||
|
futures = "0.3"
|
||||||
|
flate2 = "1.0"
|
||||||
|
tar = "0.4"
|
||||||
|
zip = "0.6"
|
||||||
|
dirs = "5.0"
|
||||||
|
which = "6.0"
|
||||||
|
serde_json = "1.0"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = "3.8"
|
||||||
|
serial_test = "2.0"
|
||||||
|
mockito = "1.0"
|
||||||
+19
-2
@@ -1,3 +1,20 @@
|
|||||||
fn main() {
|
#[cfg(test)]
|
||||||
println!("Hello, world!");
|
mod tests;
|
||||||
|
|
||||||
|
mod rcloneInstall;
|
||||||
|
|
||||||
|
use tokio::runtime::Runtime;
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let rt = Runtime::new()?;
|
||||||
|
|
||||||
|
let rclone = rt.block_on(async {
|
||||||
|
let rclone = rcloneInstall::RcloneApp::new().await?;
|
||||||
|
println!("rclone готов к использованию");
|
||||||
|
Ok::<_, Box<dyn std::error::Error>>(rclone)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let v = rclone.version().unwrap().to_string();
|
||||||
|
println!("Rclone: {}", v);
|
||||||
|
Ok::<_, Box<dyn std::error::Error>>(())
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
use flate2::read::GzDecoder;
|
||||||
|
use reqwest;
|
||||||
|
use std::fs;
|
||||||
|
use std::io::Cursor;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process::Command;
|
||||||
|
use tar::Archive;
|
||||||
|
use tokio;
|
||||||
|
|
||||||
|
pub struct RcloneApp {
|
||||||
|
rclone_path: PathBuf,
|
||||||
|
using_system_rclone: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RcloneApp {
|
||||||
|
pub async fn new() -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
|
// Сначала проверяем системный rclone
|
||||||
|
if let Some(system_rclone) = Self::check_system_rclone() {
|
||||||
|
println!("Найден системный rclone: {:?}", system_rclone);
|
||||||
|
return Ok(Self {
|
||||||
|
rclone_path: system_rclone,
|
||||||
|
using_system_rclone: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем в директории приложения
|
||||||
|
let app_dir = dirs::data_dir()
|
||||||
|
.ok_or("Не удалось найти папку для данных")?
|
||||||
|
.join("rclone-ui");
|
||||||
|
|
||||||
|
let rclone_path = if cfg!(windows) {
|
||||||
|
app_dir.join("rclone.exe")
|
||||||
|
} else {
|
||||||
|
app_dir.join("rclone")
|
||||||
|
};
|
||||||
|
|
||||||
|
if rclone_path.exists() {
|
||||||
|
println!("Найден rclone в директории приложения: {:?}", rclone_path);
|
||||||
|
return Ok(Self {
|
||||||
|
rclone_path,
|
||||||
|
using_system_rclone: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если ничего не найдено - устанавливаем
|
||||||
|
println!("rclone не найден. Начинаю установку...");
|
||||||
|
Self::install_latest_rclone().await
|
||||||
|
}
|
||||||
|
|
||||||
|
// Публичный конструктор для тестов и создания экземпляра с заданными параметрами
|
||||||
|
pub fn new_with_path(rclone_path: PathBuf, using_system_rclone: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
rclone_path,
|
||||||
|
using_system_rclone,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Геттеры
|
||||||
|
pub fn get_rclone_path(&self) -> &PathBuf {
|
||||||
|
&self.rclone_path
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_using_system_rclone(&self) -> bool {
|
||||||
|
self.using_system_rclone
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сеттеры
|
||||||
|
pub fn set_rclone_path(&mut self, path: PathBuf) {
|
||||||
|
self.rclone_path = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_using_system_rclone(&mut self, value: bool) {
|
||||||
|
self.using_system_rclone = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_system_rclone() -> Option<PathBuf> {
|
||||||
|
if let Ok(path) = which::which("rclone") {
|
||||||
|
return Some(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
if let Ok(path) = which::which("rclone.exe") {
|
||||||
|
return Some(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn install_latest_rclone() -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
|
println!("Начинаю установку последней версии rclone...");
|
||||||
|
|
||||||
|
// Определяем ОС и архитектуру
|
||||||
|
let os = if cfg!(target_os = "linux") {
|
||||||
|
"linux"
|
||||||
|
} else if cfg!(target_os = "macos") {
|
||||||
|
"osx"
|
||||||
|
} else if cfg!(target_os = "windows") {
|
||||||
|
"windows"
|
||||||
|
} else {
|
||||||
|
return Err("Неподдерживаемая операционная система".into());
|
||||||
|
};
|
||||||
|
|
||||||
|
let arch = if cfg!(target_arch = "x86_64") {
|
||||||
|
"amd64"
|
||||||
|
} else if cfg!(target_arch = "aarch64") {
|
||||||
|
"arm64"
|
||||||
|
} else if cfg!(target_arch = "arm") {
|
||||||
|
"arm"
|
||||||
|
} else {
|
||||||
|
return Err("Неподдерживаемая архитектура".into());
|
||||||
|
};
|
||||||
|
|
||||||
|
// Получаем информацию о последнем релизе
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let latest_release: serde_json::Value = client
|
||||||
|
.get("https://api.github.com/repos/rclone/rclone/releases/latest")
|
||||||
|
.header("User-Agent", "rclone-ui-installer")
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.json()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let version = latest_release["tag_name"]
|
||||||
|
.as_str()
|
||||||
|
.ok_or("Не удалось получить версию")?
|
||||||
|
.trim_start_matches('v');
|
||||||
|
|
||||||
|
// Формируем URL для скачивания
|
||||||
|
let download_url = format!(
|
||||||
|
"https://github.com/rclone/rclone/releases/download/v{}/rclone-v{}-{}-{}.zip",
|
||||||
|
version, version, os, arch
|
||||||
|
);
|
||||||
|
|
||||||
|
println!("Скачиваю rclone версии {} с: {}", version, download_url);
|
||||||
|
|
||||||
|
// Скачиваем архив
|
||||||
|
let response = client.get(&download_url).send().await?;
|
||||||
|
if !response.status().is_success() {
|
||||||
|
return Err(format!("Ошибка скачивания: HTTP {}", response.status()).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let bytes = response.bytes().await?;
|
||||||
|
|
||||||
|
// Создаем директорию приложения
|
||||||
|
let app_dir = dirs::data_dir()
|
||||||
|
.ok_or("Не удалось найти папку для данных")?
|
||||||
|
.join("rclone-ui");
|
||||||
|
fs::create_dir_all(&app_dir)?;
|
||||||
|
|
||||||
|
// Определяем имя исполняемого файла
|
||||||
|
let rclone_filename = if cfg!(windows) {
|
||||||
|
"rclone.exe"
|
||||||
|
} else {
|
||||||
|
"rclone"
|
||||||
|
};
|
||||||
|
let rclone_path = app_dir.join(rclone_filename);
|
||||||
|
|
||||||
|
// Распаковываем архив
|
||||||
|
use zip::read::ZipArchive;
|
||||||
|
let cursor = Cursor::new(bytes);
|
||||||
|
let mut archive = ZipArchive::new(cursor)?;
|
||||||
|
|
||||||
|
let mut extracted = false;
|
||||||
|
for i in 0..archive.len() {
|
||||||
|
let mut file = archive.by_index(i)?;
|
||||||
|
let file_name = file.name().to_string();
|
||||||
|
|
||||||
|
if file_name.ends_with("rclone") || file_name.ends_with("rclone.exe") {
|
||||||
|
let mut contents = Vec::new();
|
||||||
|
std::io::copy(&mut file, &mut contents)?;
|
||||||
|
fs::write(&rclone_path, contents)?;
|
||||||
|
extracted = true;
|
||||||
|
println!("Найден и распакован: {}", file_name);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !extracted {
|
||||||
|
return Err("Не удалось найти исполняемый файл rclone в архиве".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем права на выполнение для Unix систем
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
let mut perms = fs::metadata(&rclone_path)?.permissions();
|
||||||
|
perms.set_mode(0o755);
|
||||||
|
fs::set_permissions(&rclone_path, perms)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("rclone успешно установлен в {:?}", rclone_path);
|
||||||
|
|
||||||
|
// Проверяем установку
|
||||||
|
let output = Command::new(&rclone_path)
|
||||||
|
.arg("version")
|
||||||
|
.output()
|
||||||
|
.map_err(|e| format!("Ошибка проверки установки: {}", e))?;
|
||||||
|
|
||||||
|
if output.status.success() {
|
||||||
|
let version_output = String::from_utf8_lossy(&output.stdout);
|
||||||
|
println!("Установленная версия rclone:\n{}", version_output);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
rclone_path,
|
||||||
|
using_system_rclone: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_command(&self, args: &[&str]) -> Result<String, String> {
|
||||||
|
let output = Command::new(&self.rclone_path)
|
||||||
|
.args(args)
|
||||||
|
.output()
|
||||||
|
.map_err(|e| format!("Ошибка запуска: {}", e))?;
|
||||||
|
|
||||||
|
if output.status.success() {
|
||||||
|
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||||
|
} else {
|
||||||
|
Err(String::from_utf8_lossy(&output.stderr).to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn version(&self) -> Result<String, String> {
|
||||||
|
self.run_command(&["version"])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod rclone_install;
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::rcloneInstall::RcloneApp;
|
||||||
|
use std::fs;
|
||||||
|
use std::env;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
use serial_test::serial;
|
||||||
|
|
||||||
|
fn setup_test_dir() -> tempfile::TempDir {
|
||||||
|
tempdir().expect("Failed to create temp dir")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_new_with_path_constructor() {
|
||||||
|
let temp_dir = setup_test_dir();
|
||||||
|
let rclone_path = temp_dir.path().join("rclone");
|
||||||
|
|
||||||
|
let app = RcloneApp::new_with_path(rclone_path.clone(), true);
|
||||||
|
|
||||||
|
assert_eq!(app.get_rclone_path(), &rclone_path);
|
||||||
|
assert!(app.is_using_system_rclone());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_getters() {
|
||||||
|
let temp_dir = setup_test_dir();
|
||||||
|
let rclone_path = temp_dir.path().join("rclone");
|
||||||
|
|
||||||
|
let app = RcloneApp::new_with_path(rclone_path.clone(), true);
|
||||||
|
|
||||||
|
assert_eq!(app.get_rclone_path(), &rclone_path);
|
||||||
|
assert!(app.is_using_system_rclone());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_setters() {
|
||||||
|
let temp_dir = setup_test_dir();
|
||||||
|
let rclone_path1 = temp_dir.path().join("rclone1");
|
||||||
|
let rclone_path2 = temp_dir.path().join("rclone2");
|
||||||
|
|
||||||
|
let mut app = RcloneApp::new_with_path(rclone_path1, false);
|
||||||
|
|
||||||
|
assert!(!app.is_using_system_rclone());
|
||||||
|
|
||||||
|
app.set_using_system_rclone(true);
|
||||||
|
assert!(app.is_using_system_rclone());
|
||||||
|
|
||||||
|
app.set_rclone_path(rclone_path2.clone());
|
||||||
|
assert_eq!(app.get_rclone_path(), &rclone_path2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_version_method_with_nonexistent_rclone() {
|
||||||
|
let temp_dir = setup_test_dir();
|
||||||
|
let rclone_path = if cfg!(windows) {
|
||||||
|
temp_dir.path().join("rclone.exe")
|
||||||
|
} else {
|
||||||
|
temp_dir.path().join("rclone")
|
||||||
|
};
|
||||||
|
|
||||||
|
let app = RcloneApp::new_with_path(rclone_path, false);
|
||||||
|
|
||||||
|
let result = app.version();
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_version_method_with_mock_rclone() {
|
||||||
|
let temp_dir = setup_test_dir();
|
||||||
|
let rclone_path = if cfg!(windows) {
|
||||||
|
temp_dir.path().join("rclone.exe")
|
||||||
|
} else {
|
||||||
|
temp_dir.path().join("rclone")
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
fs::write(&rclone_path, "#!/bin/sh\necho 'rclone v1.60.0'").unwrap();
|
||||||
|
let mut perms = fs::metadata(&rclone_path).unwrap().permissions();
|
||||||
|
perms.set_mode(0o755);
|
||||||
|
fs::set_permissions(&rclone_path, perms).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
fs::write(&rclone_path, "").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let app = RcloneApp::new_with_path(rclone_path, false);
|
||||||
|
|
||||||
|
if cfg!(not(windows)) {
|
||||||
|
let result = app.version();
|
||||||
|
println!("Version result: {:?}", result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_using_system_rclone_flag() {
|
||||||
|
let temp_dir = setup_test_dir();
|
||||||
|
let rclone_path = temp_dir.path().join("rclone");
|
||||||
|
|
||||||
|
let app = RcloneApp::new_with_path(rclone_path.clone(), true);
|
||||||
|
assert!(app.is_using_system_rclone());
|
||||||
|
|
||||||
|
let app2 = RcloneApp::new_with_path(rclone_path, false);
|
||||||
|
assert!(!app2.is_using_system_rclone());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_multiple_instances() {
|
||||||
|
let temp_dir = setup_test_dir();
|
||||||
|
|
||||||
|
let app1 = RcloneApp::new_with_path(temp_dir.path().join("rclone1"), true);
|
||||||
|
let app2 = RcloneApp::new_with_path(temp_dir.path().join("rclone2"), false);
|
||||||
|
let app3 = RcloneApp::new_with_path(temp_dir.path().join("rclone3"), true);
|
||||||
|
|
||||||
|
assert!(app1.is_using_system_rclone());
|
||||||
|
assert!(!app2.is_using_system_rclone());
|
||||||
|
assert!(app3.is_using_system_rclone());
|
||||||
|
|
||||||
|
assert_ne!(app1.get_rclone_path(), app2.get_rclone_path());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn test_check_system_rclone() {
|
||||||
|
let result = RcloneApp::check_system_rclone();
|
||||||
|
println!("System rclone found: {:?}", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_path_manipulation() {
|
||||||
|
let temp_dir = setup_test_dir();
|
||||||
|
let original_path = temp_dir.path().join("original_rclone");
|
||||||
|
let new_path = temp_dir.path().join("new_rclone");
|
||||||
|
|
||||||
|
let mut app = RcloneApp::new_with_path(original_path.clone(), false);
|
||||||
|
assert_eq!(app.get_rclone_path(), &original_path);
|
||||||
|
|
||||||
|
app.set_rclone_path(new_path.clone());
|
||||||
|
assert_eq!(app.get_rclone_path(), &new_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_os_arch_detection() {
|
||||||
|
let os = if cfg!(target_os = "linux") {
|
||||||
|
"linux"
|
||||||
|
} else if cfg!(target_os = "macos") {
|
||||||
|
"osx"
|
||||||
|
} else if cfg!(target_os = "windows") {
|
||||||
|
"windows"
|
||||||
|
} else {
|
||||||
|
"unknown"
|
||||||
|
};
|
||||||
|
|
||||||
|
let arch = if cfg!(target_arch = "x86_64") {
|
||||||
|
"amd64"
|
||||||
|
} else if cfg!(target_arch = "aarch64") {
|
||||||
|
"arm64"
|
||||||
|
} else if cfg!(target_arch = "arm") {
|
||||||
|
"arm"
|
||||||
|
} else {
|
||||||
|
"unknown"
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_ne!(os, "unknown", "ОС должна быть определена");
|
||||||
|
assert_ne!(arch, "unknown", "Архитектура должна быть определена");
|
||||||
|
|
||||||
|
println!("OS: {}, ARCH: {}", os, arch);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[ignore]
|
||||||
|
async fn test_actual_rclone_installation() {
|
||||||
|
match RcloneApp::new().await {
|
||||||
|
Ok(app) => {
|
||||||
|
assert!(app.get_rclone_path().exists());
|
||||||
|
let version = app.version();
|
||||||
|
assert!(version.is_ok());
|
||||||
|
println!("Установлен rclone по пути: {:?}", app.get_rclone_path());
|
||||||
|
println!("Версия: {:?}", version);
|
||||||
|
println!("Используется системный rclone: {}", app.is_using_system_rclone());
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
panic!("Не удалось установить rclone: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user