#![allow(dead_code)] use log; use reqwest::{ multipart::{Form, Part}, Client, ClientBuilder, }; use serde::de::DeserializeOwned; use serde::Deserialize; use serde_json::Value; use std::collections::HashMap; use std::time::Duration; use urlencoding; /// Основная ошибка библиотеки #[derive(Debug, thiserror::Error)] pub enum SynologyError { #[error("HTTP error: {0}")] Http(#[from] reqwest::Error), #[error("API error: code {code} - {message:?}")] Api { code: i64, message: Option }, #[error("Invalid response format: {0}")] InvalidResponseFormat(String), #[error("Login required")] LoginRequired, #[error("Not logged in")] NotLoggedIn, #[error("Invalid parameter: {0}")] InvalidParameter(String), #[error("MIME error: {0}")] Mime(#[from] reqwest::header::InvalidHeaderValue), #[error("IO error: {0}")] Io(#[from] std::io::Error), #[error("CSV error: {0}")] Csv(String), } /// Результат операций pub type Result = std::result::Result; /// Базовый ответ API #[derive(Debug, Deserialize)] struct ApiResponse { success: bool, #[serde(default)] data: Option, #[serde(default)] error: Option, } /// Пустой тип для ответов без данных #[derive(Debug, Default, Deserialize)] struct EmptyData; #[derive(Debug, Deserialize)] struct ApiError { code: i64, #[serde(default)] #[allow(dead_code)] errors: Option>, } /// Информация о доступных API #[derive(Debug, Clone, Default, Deserialize)] pub struct ApiInfo { pub path: String, pub min_version: i32, pub max_version: i32, } /// Информация о виртуальных файловых системах #[derive(Debug, Default, Deserialize)] pub struct VirtualSupport { #[serde(default)] pub enable_iso_mount: bool, #[serde(default)] pub enable_remote_mount: bool, } /// Информация о File Station #[derive(Debug, Default, Deserialize)] pub struct FileStationInfo { pub is_manager: bool, pub hostname: String, pub support_sharing: bool, #[serde(default)] pub allow_heic_original: bool, #[serde(default)] pub allow_normal_disable_html: bool, #[serde(default)] pub enable_list_usergrp: bool, #[serde(default)] pub enable_send_email_attachment: bool, #[serde(default)] pub enable_view_google: bool, #[serde(default)] pub enable_view_microsoft: bool, #[serde(default)] pub support_file_request: bool, #[serde(default)] pub support_vfs: bool, #[serde(default)] pub support_virtual: Option, #[serde(default)] pub support_virtual_protocol: Vec, #[serde(default)] pub system_codepage: String, #[serde(default)] pub uid: u64, } /// Объект файла/папки #[derive(Debug, Clone, Default, Deserialize)] pub struct FileInfo { pub path: String, pub name: String, pub isdir: bool, #[serde(default)] pub additional: Option, } /// Дополнительная информация о файле #[derive(Debug, Clone, Default, Deserialize)] pub struct FileAdditional { pub real_path: Option, pub size: Option, #[serde(default)] pub owner: Option, #[serde(default)] pub time: Option, #[serde(default)] pub perm: Option, pub r#type: Option, } /// Информация о владельце #[derive(Debug, Clone, Default, Deserialize)] pub struct OwnerInfo { pub user: String, pub group: String, pub uid: u32, pub gid: u32, } /// Информация о времени #[derive(Debug, Clone, Default, Deserialize)] pub struct TimeInfo { pub atime: u64, pub mtime: u64, pub ctime: u64, pub crtime: u64, } /// Информация о правах доступа #[derive(Debug, Clone, Default, Deserialize)] pub struct PermInfo { pub posix: u32, pub is_acl_mode: bool, #[serde(default)] pub acl: Option, } #[derive(Debug, Clone, Default, Deserialize)] pub struct AclInfo { pub append: bool, pub del: bool, pub exec: bool, pub read: bool, pub write: bool, } /// Общая папка #[derive(Debug, Default, Deserialize)] pub struct Share { pub path: String, pub name: String, #[serde(default)] pub additional: Option, } /// Список файлов #[derive(Debug, Default, Deserialize)] pub struct FileList { pub total: i32, pub offset: i32, pub files: Vec, } /// Список общих папок #[derive(Debug, Default, Deserialize)] pub struct ShareList { pub total: i32, pub offset: i32, pub shares: Vec, } /// Информация о задаче (для асинхронных операций) #[derive(Debug, Default, Deserialize)] pub struct TaskInfo { pub taskid: String, } /// Статус задачи #[derive(Debug, Default, Deserialize)] pub struct TaskStatus { pub finished: bool, #[serde(default)] pub progress: Option, #[serde(default)] pub total: Option, #[serde(default)] pub processed_size: Option, #[serde(default)] pub processed_num: Option, #[serde(default)] pub path: Option, #[serde(default)] pub processing_path: Option, #[serde(default)] pub dest_folder_path: Option, } /// Результат поиска #[derive(Debug, Default, Deserialize)] pub struct SearchResult { pub total: i32, pub offset: i32, pub finished: bool, pub files: Vec, } /// Информация о шаринге #[derive(Debug, Clone, Default, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SharingLink { pub id: String, pub url: String, pub link_owner: String, pub path: String, pub is_folder: bool, pub has_password: bool, pub date_expired: String, pub date_available: String, pub status: String, } /// Созданная ссылка #[derive(Debug, Clone, Default, Deserialize)] pub struct CreatedLink { pub path: String, pub url: String, pub id: String, pub qrcode: Option, pub error: i32, } /// Параметры сортировки #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SortBy { Name, Size, User, Group, MTime, ATime, CTime, Crtime, Posix, Type, } impl SortBy { fn as_str(&self) -> &'static str { match self { SortBy::Name => "name", SortBy::Size => "size", SortBy::User => "user", SortBy::Group => "group", SortBy::MTime => "mtime", SortBy::ATime => "atime", SortBy::CTime => "ctime", SortBy::Crtime => "crtime", SortBy::Posix => "posix", SortBy::Type => "type", } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SortDirection { Asc, Desc, } impl SortDirection { fn as_str(&self) -> &'static str { match self { SortDirection::Asc => "asc", SortDirection::Desc => "desc", } } } /// Параметры для list_files #[derive(Default)] pub struct ListFilesOptions { pub offset: Option, pub limit: Option, pub sort_by: Option, pub sort_direction: Option, pub pattern: Option, pub file_type: Option, pub additional: Option>, } impl ListFilesOptions { pub fn new() -> Self { Self::default() } pub fn offset(mut self, offset: i32) -> Self { self.offset = Some(offset); self } pub fn limit(mut self, limit: i32) -> Self { self.limit = Some(limit); self } pub fn sort_by(mut self, sort_by: SortBy) -> Self { self.sort_by = Some(sort_by); self } pub fn sort_direction(mut self, sort_direction: SortDirection) -> Self { self.sort_direction = Some(sort_direction); self } pub fn pattern(mut self, pattern: &str) -> Self { self.pattern = Some(pattern.to_string()); self } pub fn file_type(mut self, file_type: &str) -> Self { self.file_type = Some(file_type.to_string()); self } pub fn additional(mut self, additional: &[&str]) -> Self { self.additional = Some(additional.iter().map(|&s| s.to_string()).collect()); self } } /// Параметры для list_shares #[derive(Default)] pub struct ListSharesOptions { pub offset: Option, pub limit: Option, pub sort_by: Option, pub sort_direction: Option, pub only_writable: Option, pub additional: Option>, } impl ListSharesOptions { pub fn new() -> Self { Self::default() } pub fn offset(mut self, offset: i32) -> Self { self.offset = Some(offset); self } pub fn limit(mut self, limit: i32) -> Self { self.limit = Some(limit); self } pub fn sort_by(mut self, sort_by: SortBy) -> Self { self.sort_by = Some(sort_by); self } pub fn sort_direction(mut self, sort_direction: SortDirection) -> Self { self.sort_direction = Some(sort_direction); self } pub fn only_writable(mut self, only_writable: bool) -> Self { self.only_writable = Some(only_writable); self } pub fn additional(mut self, additional: &[&str]) -> Self { self.additional = Some(additional.iter().map(|&s| s.to_string()).collect()); self } } /// Клиент для работы с Synology API pub struct SynologyClient { client: Client, base_url: String, sid: Option, session_name: String, } impl SynologyClient { /// Создание нового клиента pub fn new(base_url: &str) -> Self { let client = ClientBuilder::new() .timeout(Duration::from_secs(30)) .build() .expect("Failed to create HTTP client"); Self { client, base_url: base_url.trim_end_matches('/').to_string(), sid: None, session_name: "FileStation".to_string(), } } /// Установка имени сессии pub fn with_session_name(mut self, name: &str) -> Self { self.session_name = name.to_string(); self } /// Проверка авторизации pub fn is_authenticated(&self) -> bool { self.sid.is_some() } /// Выполнение запроса к API (с возвратом данных) async fn request( &self, path: &str, params: &[(&str, String)], ) -> Result { let mut url = format!("{}/webapi/{}", self.base_url, path); let mut param_parts: Vec = Vec::new(); for (key, value) in params { param_parts.push(format!("{}={}", key, urlencoding::encode(value))); } if !param_parts.is_empty() { url = format!("{}?{}", url, param_parts.join("&")); } log::debug!("Request URL: {}", url); let response = self.client.get(&url).send().await?; let status = response.status(); if !status.is_success() { let error_text = response.text().await?; log::debug!("Error response: {}", error_text); return Err(SynologyError::Api { code: status.as_u16() as i64, message: Some(error_text), }); } let response_text = response.text().await?; log::debug!("Response: {}", response_text); let api_response: ApiResponse = serde_json::from_str(&response_text) .map_err(|e| SynologyError::InvalidResponseFormat(e.to_string()))?; if api_response.success { if std::any::type_name::() == std::any::type_name::() { Ok(T::default()) } else { api_response.data.ok_or_else(|| { SynologyError::InvalidResponseFormat("Missing data field".to_string()) }) } } else { let code = api_response.error.as_ref().map(|e| e.code).unwrap_or(-1); Err(SynologyError::Api { code, message: None, }) } } /// Выполнение запроса с SID async fn request_auth( &self, path: &str, params: Vec<(&str, String)>, ) -> Result { let mut all_params = params; if let Some(sid) = &self.sid { all_params.push(("_sid", sid.clone())); } self.request(path, &all_params).await } /// Выполнение запроса без возврата данных (только проверка success) async fn request_auth_void(&self, path: &str, params: Vec<(&str, String)>) -> Result<()> { let _: EmptyData = self.request_auth(path, params).await?; Ok(()) } /// Получение информации о доступных API pub async fn get_api_info(&self, apis: &[&str]) -> Result> { let query = if apis.is_empty() { "all".to_string() } else { apis.join(",") }; let response: Value = self .request( "query.cgi", &[ ("api", "SYNO.API.Info".to_string()), ("version", "1".to_string()), ("method", "query".to_string()), ("query", query), ], ) .await?; let result: HashMap = serde_json::from_value(response) .map_err(|e| SynologyError::InvalidResponseFormat(e.to_string()))?; Ok(result) } /// Вход в систему pub async fn login(&mut self, account: &str, password: &str) -> Result { let response: HashMap = self .request( "auth.cgi", &[ ("api", "SYNO.API.Auth".to_string()), ("version", "3".to_string()), ("method", "login".to_string()), ("account", account.to_string()), ("passwd", password.to_string()), ("session", self.session_name.clone()), ("format", "sid".to_string()), ], ) .await?; let sid = response .get("sid") .ok_or_else(|| { SynologyError::InvalidResponseFormat("Missing sid in response".to_string()) })? .clone(); self.sid = Some(sid.clone()); Ok(sid) } /// Выход из системы pub async fn logout(&mut self) -> Result<()> { if self.sid.is_none() { return Ok(()); } self.request_auth_void( "auth.cgi", vec![ ("api", "SYNO.API.Auth".to_string()), ("version", "1".to_string()), ("method", "logout".to_string()), ("session", self.session_name.clone()), ], ) .await?; self.sid = None; Ok(()) } /// Получение информации о File Station pub async fn get_info(&self) -> Result { self.request_auth( "entry.cgi", vec![ ("api", "SYNO.FileStation.Info".to_string()), ("version", "2".to_string()), ("method", "get".to_string()), ], ) .await } /// Получение списка общих папок pub async fn list_shares(&self, options: ListSharesOptions) -> Result { let mut params = vec![ ("api", "SYNO.FileStation.List".to_string()), ("version", "2".to_string()), ("method", "list_share".to_string()), ]; if let Some(offset) = options.offset { params.push(("offset", offset.to_string())); } if let Some(limit) = options.limit { params.push(("limit", limit.to_string())); } if let Some(sort_by) = options.sort_by { params.push(("sort_by", sort_by.as_str().to_string())); } if let Some(sort_direction) = options.sort_direction { params.push(("sort_direction", sort_direction.as_str().to_string())); } if let Some(only_writable) = options.only_writable { params.push(("onlywritable", only_writable.to_string())); } if let Some(additional) = options.additional { let additional_str = additional .iter() .map(|s| format!("\"{}\"", s)) .collect::>() .join(","); params.push(("additional", format!("[{}]", additional_str))); } self.request_auth("entry.cgi", params).await } /// Получение списка файлов в папке pub async fn list_files( &self, folder_path: &str, options: ListFilesOptions, ) -> Result { let mut params = vec![ ("api", "SYNO.FileStation.List".to_string()), ("version", "2".to_string()), ("method", "list".to_string()), ("folder_path", folder_path.to_string()), ]; if let Some(offset) = options.offset { params.push(("offset", offset.to_string())); } if let Some(limit) = options.limit { params.push(("limit", limit.to_string())); } if let Some(sort_by) = options.sort_by { params.push(("sort_by", sort_by.as_str().to_string())); } if let Some(sort_direction) = options.sort_direction { params.push(("sort_direction", sort_direction.as_str().to_string())); } if let Some(pattern) = options.pattern { params.push(("pattern", pattern)); } if let Some(file_type) = options.file_type { params.push(("filetype", file_type)); } if let Some(additional) = options.additional { let additional_str = additional .iter() .map(|s| format!("\"{}\"", s)) .collect::>() .join(","); params.push(("additional", format!("[{}]", additional_str))); } self.request_auth("entry.cgi", params).await } /// Создание папки pub async fn create_folder( &self, folder_path: &str, name: &str, force_parent: Option, ) -> Result> { let mut params = vec![ ("api", "SYNO.FileStation.CreateFolder".to_string()), ("version", "2".to_string()), ("method", "create".to_string()), ("folder_path", format!("[\"{}\"]", folder_path)), ("name", format!("[\"{}\"]", name)), ]; if let Some(force_parent) = force_parent { params.push(("force_parent", force_parent.to_string())); } let response: HashMap> = self.request_auth("entry.cgi", params).await?; Ok(response.get("folders").cloned().unwrap_or_default()) } /// Переименование файла/папки pub async fn rename(&self, path: &str, new_name: &str) -> Result> { let response: HashMap> = self .request_auth( "entry.cgi", vec![ ("api", "SYNO.FileStation.Rename".to_string()), ("version", "2".to_string()), ("method", "rename".to_string()), ("path", format!("[\"{}\"]", path)), ("name", format!("[\"{}\"]", new_name)), ], ) .await?; Ok(response.get("files").cloned().unwrap_or_default()) } /// Копирование файлов/папок (асинхронное) pub async fn copy_start( &self, paths: &[&str], dest_folder_path: &str, overwrite: Option, ) -> Result { let paths_str = paths .iter() .map(|&p| format!("\"{}\"", p)) .collect::>() .join(","); let mut params = vec![ ("api", "SYNO.FileStation.CopyMove".to_string()), ("version", "3".to_string()), ("method", "start".to_string()), ("path", format!("[{}]", paths_str)), ("dest_folder_path", dest_folder_path.to_string()), ("remove_src", "false".to_string()), ]; if let Some(overwrite) = overwrite { params.push(("overwrite", overwrite.to_string())); } let response: TaskInfo = self.request_auth("entry.cgi", params).await?; Ok(response.taskid) } /// Перемещение файлов/папок (асинхронное) pub async fn move_start( &self, paths: &[&str], dest_folder_path: &str, overwrite: Option, ) -> Result { let paths_str = paths .iter() .map(|&p| format!("\"{}\"", p)) .collect::>() .join(","); let mut params = vec![ ("api", "SYNO.FileStation.CopyMove".to_string()), ("version", "3".to_string()), ("method", "start".to_string()), ("path", format!("[{}]", paths_str)), ("dest_folder_path", dest_folder_path.to_string()), ("remove_src", "true".to_string()), ]; if let Some(overwrite) = overwrite { params.push(("overwrite", overwrite.to_string())); } let response: TaskInfo = self.request_auth("entry.cgi", params).await?; Ok(response.taskid) } /// Получение статуса задачи копирования/перемещения pub async fn get_task_status(&self, task_id: &str) -> Result { self.request_auth( "entry.cgi", vec![ ("api", "SYNO.FileStation.CopyMove".to_string()), ("version", "3".to_string()), ("method", "status".to_string()), ("taskid", task_id.to_string()), ], ) .await } /// Удаление файлов/папок (блокирующее) pub async fn delete(&self, paths: &[&str], recursive: Option) -> Result<()> { let paths_str = paths .iter() .map(|&p| format!("\"{}\"", p)) .collect::>() .join(","); let mut params = vec![ ("api", "SYNO.FileStation.Delete".to_string()), ("version", "2".to_string()), ("method", "delete".to_string()), ("path", format!("[{}]", paths_str)), ]; if let Some(recursive) = recursive { params.push(("recursive", recursive.to_string())); } self.request_auth_void("entry.cgi", params).await?; Ok(()) } /// Удаление файлов/папок (асинхронное) pub async fn delete_start(&self, paths: &[&str], recursive: Option) -> Result { let paths_str = paths .iter() .map(|&p| format!("\"{}\"", p)) .collect::>() .join(","); let mut params = vec![ ("api", "SYNO.FileStation.Delete".to_string()), ("version", "2".to_string()), ("method", "start".to_string()), ("path", format!("[{}]", paths_str)), ]; if let Some(recursive) = recursive { params.push(("recursive", recursive.to_string())); } let response: TaskInfo = self.request_auth("entry.cgi", params).await?; Ok(response.taskid) } /// Получение статуса удаления pub async fn get_delete_status(&self, task_id: &str) -> Result { self.request_auth( "entry.cgi", vec![ ("api", "SYNO.FileStation.Delete".to_string()), ("version", "2".to_string()), ("method", "status".to_string()), ("taskid", task_id.to_string()), ], ) .await } /// Поиск файлов pub async fn search_start( &self, folder_paths: &[&str], recursive: Option, pattern: Option<&str>, file_type: Option<&str>, extension: Option<&str>, ) -> Result { let paths_str = folder_paths .iter() .map(|&p| format!("\"{}\"", p)) .collect::>() .join(","); let mut params = vec![ ("api", "SYNO.FileStation.Search".to_string()), ("version", "2".to_string()), ("method", "start".to_string()), ("folder_path", format!("[{}]", paths_str)), ]; if let Some(recursive) = recursive { params.push(("recursive", recursive.to_string())); } if let Some(pattern) = pattern { params.push(("pattern", pattern.to_string())); } if let Some(file_type) = file_type { params.push(("filetype", file_type.to_string())); } if let Some(extension) = extension { params.push(("extension", extension.to_string())); } let response: TaskInfo = self.request_auth("entry.cgi", params).await?; Ok(response.taskid) } /// Получение результатов поиска pub async fn search_list( &self, task_id: &str, offset: Option, limit: Option, additional: Option<&[&str]>, ) -> Result { let mut params = vec![ ("api", "SYNO.FileStation.Search".to_string()), ("version", "2".to_string()), ("method", "list".to_string()), ("taskid", task_id.to_string()), ]; if let Some(offset) = offset { params.push(("offset", offset.to_string())); } if let Some(limit) = limit { params.push(("limit", limit.to_string())); } if let Some(additional) = additional { let additional_str = additional .iter() .map(|&s| format!("\"{}\"", s)) .collect::>() .join(","); params.push(("additional", format!("[{}]", additional_str))); } self.request_auth("entry.cgi", params).await } /// Остановка поиска pub async fn search_stop(&self, task_id: &str) -> Result<()> { self.request_auth_void( "entry.cgi", vec![ ("api", "SYNO.FileStation.Search".to_string()), ("version", "2".to_string()), ("method", "stop".to_string()), ("taskid", task_id.to_string()), ], ) .await?; Ok(()) } /// Очистка результатов поиска pub async fn search_clean(&self, task_id: &str) -> Result<()> { self.request_auth_void( "entry.cgi", vec![ ("api", "SYNO.FileStation.Search".to_string()), ("version", "2".to_string()), ("method", "clean".to_string()), ("taskid", task_id.to_string()), ], ) .await?; Ok(()) } /// Создание ссылки для скачивания pub async fn create_sharing_link( &self, path: &str, password: Option<&str>, date_expired: Option<&str>, date_available: Option<&str>, ) -> Result { let mut params = vec![ ("api", "SYNO.FileStation.Sharing".to_string()), ("version", "3".to_string()), ("method", "create".to_string()), ("path", path.to_string()), ]; if let Some(password) = password { params.push(("password", password.to_string())); } if let Some(date_expired) = date_expired { params.push(("date_expired", date_expired.to_string())); } if let Some(date_available) = date_available { params.push(("date_available", date_available.to_string())); } let response: HashMap> = self.request_auth("entry.cgi", params).await?; response .get("links") .and_then(|links| links.first().cloned()) .ok_or_else(|| { SynologyError::InvalidResponseFormat("Missing links in response".to_string()) }) } /// Получение списка ссылок pub async fn list_sharing_links( &self, offset: Option, limit: Option, ) -> Result> { let mut params = vec![ ("api", "SYNO.FileStation.Sharing".to_string()), ("version", "3".to_string()), ("method", "list".to_string()), ]; if let Some(offset) = offset { params.push(("offset", offset.to_string())); } if let Some(limit) = limit { params.push(("limit", limit.to_string())); } let response: HashMap = self.request_auth("entry.cgi", params).await?; let links = response .get("links") .and_then(|v| serde_json::from_value(v.clone()).ok()) .unwrap_or_default(); Ok(links) } /// Удаление ссылки pub async fn delete_sharing_link(&self, id: &str) -> Result<()> { self.request_auth_void( "entry.cgi", vec![ ("api", "SYNO.FileStation.Sharing".to_string()), ("version", "3".to_string()), ("method", "delete".to_string()), ("id", id.to_string()), ], ) .await?; Ok(()) } /// Получение размера папки (асинхронное) pub async fn get_dir_size_start(&self, paths: &[&str]) -> Result { let paths_str = paths .iter() .map(|&p| format!("\"{}\"", p)) .collect::>() .join(","); let response: TaskInfo = self .request_auth( "entry.cgi", vec![ ("api", "SYNO.FileStation.DirSize".to_string()), ("version", "2".to_string()), ("method", "start".to_string()), ("path", format!("[{}]", paths_str)), ], ) .await?; Ok(response.taskid) } /// Получение статуса вычисления размера pub async fn get_dir_size_status(&self, task_id: &str) -> Result { self.request_auth( "entry.cgi", vec![ ("api", "SYNO.FileStation.DirSize".to_string()), ("version", "2".to_string()), ("method", "status".to_string()), ("taskid", task_id.to_string()), ], ) .await } /// Архивирование файлов pub async fn compress_start( &self, paths: &[&str], dest_file_path: &str, format: Option<&str>, password: Option<&str>, ) -> Result { let paths_str = paths .iter() .map(|&p| format!("\"{}\"", p)) .collect::>() .join(","); let mut params = vec![ ("api", "SYNO.FileStation.Compress".to_string()), ("version", "3".to_string()), ("method", "start".to_string()), ("path", format!("[{}]", paths_str)), ("dest_file_path", dest_file_path.to_string()), ]; if let Some(format) = format { params.push(("format", format.to_string())); } if let Some(password) = password { params.push(("password", password.to_string())); } let response: TaskInfo = self.request_auth("entry.cgi", params).await?; Ok(response.taskid) } /// Получение статуса архивации pub async fn get_compress_status(&self, task_id: &str) -> Result { self.request_auth( "entry.cgi", vec![ ("api", "SYNO.FileStation.Compress".to_string()), ("version", "3".to_string()), ("method", "status".to_string()), ("taskid", task_id.to_string()), ], ) .await } /// Извлечение архива pub async fn extract_start( &self, file_path: &str, dest_folder_path: &str, overwrite: Option, password: Option<&str>, ) -> Result { let mut params = vec![ ("api", "SYNO.FileStation.Extract".to_string()), ("version", "2".to_string()), ("method", "start".to_string()), ("file_path", file_path.to_string()), ("dest_folder_path", dest_folder_path.to_string()), ]; if let Some(overwrite) = overwrite { params.push(("overwrite", overwrite.to_string())); } if let Some(password) = password { params.push(("password", password.to_string())); } let response: TaskInfo = self.request_auth("entry.cgi", params).await?; Ok(response.taskid) } /// Получение статуса извлечения pub async fn get_extract_status(&self, task_id: &str) -> Result { self.request_auth( "entry.cgi", vec![ ("api", "SYNO.FileStation.Extract".to_string()), ("version", "2".to_string()), ("method", "status".to_string()), ("taskid", task_id.to_string()), ], ) .await } /// Скачивание файла (возвращает бинарные данные) pub async fn download(&self, path: &str) -> Result> { let mut url = format!( "{}/webapi/entry.cgi?api=SYNO.FileStation.Download&version=2&method=download&path={}&mode=open", self.base_url, path ); if let Some(sid) = &self.sid { url = format!("{}&_sid={}", url, sid); } let response = self.client.get(&url).send().await?; let status = response.status(); if !status.is_success() { let error_text = response.text().await?; return Err(SynologyError::Api { code: status.as_u16() as i64, message: Some(error_text), }); } Ok(response.bytes().await?.to_vec()) } /// Загрузка файла (multipart) pub async fn upload( &self, folder_path: &str, filename: &str, data: Vec, create_parents: Option, overwrite: Option, ) -> Result<()> { let mut form = Form::new() .text("api", "SYNO.FileStation.Upload".to_string()) .text("version", "2".to_string()) .text("method", "upload".to_string()) .text("path", folder_path.to_string()); if let Some(create_parents) = create_parents { form = form.text("create_parents", create_parents.to_string()); } if let Some(overwrite) = overwrite { form = form.text("overwrite", overwrite.to_string()); } let file_part = Part::bytes(data) .file_name(filename.to_string()) .mime_str("application/octet-stream")?; form = form.part("file", file_part); let mut url = format!("{}/webapi/entry.cgi", self.base_url); if let Some(sid) = &self.sid { url = format!("{}?_sid={}", url, sid); } let response = self.client.post(&url).multipart(form).send().await?; let api_response: ApiResponse = response.json().await?; if api_response.success { Ok(()) } else { let code = api_response.error.as_ref().map(|e| e.code).unwrap_or(-1); Err(SynologyError::Api { code, message: None, }) } } /// Поиск файла по имени и пути pub async fn search_file_by_name_and_path(&self, name: &str, path: &str) -> Result { let url = format!("{}/webapi/entry.cgi", self.base_url); let data = format!( "api=SYNO.SynologyDrive.Files&method=search&version=2&keyword={}&path={}&_sid={}", urlencoding::encode(name), urlencoding::encode(path), self.sid.as_ref().unwrap_or(&String::new()) ); log::debug!("Searching for file: {} in path: {}", name, path); let response = self .client .post(&url) .header("Content-Type", "application/x-www-form-urlencoded") .body(data) .send() .await?; let response_text = response.text().await?; let json: Value = serde_json::from_str(&response_text) .map_err(|e| SynologyError::InvalidResponseFormat(e.to_string()))?; if json.get("success").and_then(|s| s.as_bool()) != Some(true) { let code = json["error"]["code"].as_i64().unwrap_or(-1); return Err(SynologyError::Api { code, message: Some(format!("{:?}", json.get("error"))), }); } Ok(json) } /// Поиск файла по имени (без ограничения по пути) pub async fn search_file_by_name(&self, name: &str) -> Result { let url = format!("{}/webapi/entry.cgi", self.base_url); let data = format!( "api=SYNO.SynologyDrive.Files&method=search&version=2&keyword={}&_sid={}", urlencoding::encode(name), self.sid.as_ref().unwrap_or(&String::new()) ); log::debug!("Searching for file: {}", name); let response = self .client .post(&url) .header("Content-Type", "application/x-www-form-urlencoded") .body(data) .send() .await?; let response_text = response.text().await?; let json: Value = serde_json::from_str(&response_text) .map_err(|e| SynologyError::InvalidResponseFormat(e.to_string()))?; if json.get("success").and_then(|s| s.as_bool()) != Some(true) { let code = json["error"]["code"].as_i64().unwrap_or(-1); return Err(SynologyError::Api { code, message: Some(format!("{:?}", json.get("error"))), }); } Ok(json) } /// Экспорт файла Synology Office по ID pub async fn export_by_file_id(&self, file_id: &str, file_name: &str) -> Result> { let export_endpoint = file_name.replace(".osheet", ".xlsx"); let endpoint = format!("entry.cgi/{}", export_endpoint); let params = format!( "api=SYNO.Office.Export&method=download&version=1&path=id:{}&_sid={}", file_id, self.sid.as_ref().unwrap_or(&String::new()) ); let url = format!("{}/webapi/{}", self.base_url, endpoint); log::debug!("Export URL: {}", url); let response = self .client .post(&url) .header("Content-Type", "application/x-www-form-urlencoded") .body(params) .send() .await?; let status = response.status(); if !status.is_success() { let error_text = response.text().await?; return Err(SynologyError::Api { code: status.as_u16() as i64, message: Some(error_text), }); } let bytes = response.bytes().await?; log::debug!("Received {} bytes", bytes.len()); Ok(bytes.to_vec()) } } impl Drop for SynologyClient { fn drop(&mut self) {} }