1349 lines
40 KiB
Rust
1349 lines
40 KiB
Rust
#![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<String> },
|
||
|
||
#[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<T> = std::result::Result<T, SynologyError>;
|
||
|
||
/// Базовый ответ API
|
||
#[derive(Debug, Deserialize)]
|
||
struct ApiResponse<T> {
|
||
success: bool,
|
||
#[serde(default)]
|
||
data: Option<T>,
|
||
#[serde(default)]
|
||
error: Option<ApiError>,
|
||
}
|
||
|
||
/// Пустой тип для ответов без данных
|
||
#[derive(Debug, Default, Deserialize)]
|
||
struct EmptyData;
|
||
|
||
#[derive(Debug, Deserialize)]
|
||
struct ApiError {
|
||
code: i64,
|
||
#[serde(default)]
|
||
#[allow(dead_code)]
|
||
errors: Option<Vec<Value>>,
|
||
}
|
||
|
||
/// Информация о доступных 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<VirtualSupport>,
|
||
#[serde(default)]
|
||
pub support_virtual_protocol: Vec<String>,
|
||
#[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<FileAdditional>,
|
||
}
|
||
|
||
/// Дополнительная информация о файле
|
||
#[derive(Debug, Clone, Default, Deserialize)]
|
||
pub struct FileAdditional {
|
||
pub real_path: Option<String>,
|
||
pub size: Option<u64>,
|
||
#[serde(default)]
|
||
pub owner: Option<OwnerInfo>,
|
||
#[serde(default)]
|
||
pub time: Option<TimeInfo>,
|
||
#[serde(default)]
|
||
pub perm: Option<PermInfo>,
|
||
pub r#type: Option<String>,
|
||
}
|
||
|
||
/// Информация о владельце
|
||
#[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<AclInfo>,
|
||
}
|
||
|
||
#[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<FileAdditional>,
|
||
}
|
||
|
||
/// Список файлов
|
||
#[derive(Debug, Default, Deserialize)]
|
||
pub struct FileList {
|
||
pub total: i32,
|
||
pub offset: i32,
|
||
pub files: Vec<FileInfo>,
|
||
}
|
||
|
||
/// Список общих папок
|
||
#[derive(Debug, Default, Deserialize)]
|
||
pub struct ShareList {
|
||
pub total: i32,
|
||
pub offset: i32,
|
||
pub shares: Vec<Share>,
|
||
}
|
||
|
||
/// Информация о задаче (для асинхронных операций)
|
||
#[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<f64>,
|
||
#[serde(default)]
|
||
pub total: Option<i64>,
|
||
#[serde(default)]
|
||
pub processed_size: Option<u64>,
|
||
#[serde(default)]
|
||
pub processed_num: Option<u32>,
|
||
#[serde(default)]
|
||
pub path: Option<String>,
|
||
#[serde(default)]
|
||
pub processing_path: Option<String>,
|
||
#[serde(default)]
|
||
pub dest_folder_path: Option<String>,
|
||
}
|
||
|
||
/// Результат поиска
|
||
#[derive(Debug, Default, Deserialize)]
|
||
pub struct SearchResult {
|
||
pub total: i32,
|
||
pub offset: i32,
|
||
pub finished: bool,
|
||
pub files: Vec<FileInfo>,
|
||
}
|
||
|
||
/// Информация о шаринге
|
||
#[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<String>,
|
||
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<i32>,
|
||
pub limit: Option<i32>,
|
||
pub sort_by: Option<SortBy>,
|
||
pub sort_direction: Option<SortDirection>,
|
||
pub pattern: Option<String>,
|
||
pub file_type: Option<String>,
|
||
pub additional: Option<Vec<String>>,
|
||
}
|
||
|
||
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<i32>,
|
||
pub limit: Option<i32>,
|
||
pub sort_by: Option<SortBy>,
|
||
pub sort_direction: Option<SortDirection>,
|
||
pub only_writable: Option<bool>,
|
||
pub additional: Option<Vec<String>>,
|
||
}
|
||
|
||
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<String>,
|
||
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<T: DeserializeOwned + Default>(
|
||
&self,
|
||
path: &str,
|
||
params: &[(&str, String)],
|
||
) -> Result<T> {
|
||
let mut url = format!("{}/webapi/{}", self.base_url, path);
|
||
|
||
let mut param_parts: Vec<String> = 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<T> = serde_json::from_str(&response_text)
|
||
.map_err(|e| SynologyError::InvalidResponseFormat(e.to_string()))?;
|
||
|
||
if api_response.success {
|
||
if std::any::type_name::<T>() == std::any::type_name::<EmptyData>() {
|
||
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<T: DeserializeOwned + Default>(
|
||
&self,
|
||
path: &str,
|
||
params: Vec<(&str, String)>,
|
||
) -> Result<T> {
|
||
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<HashMap<String, ApiInfo>> {
|
||
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<String, ApiInfo> = 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<String> {
|
||
let response: HashMap<String, String> = 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<FileStationInfo> {
|
||
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<ShareList> {
|
||
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::<Vec<_>>()
|
||
.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<FileList> {
|
||
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::<Vec<_>>()
|
||
.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<bool>,
|
||
) -> Result<Vec<FileInfo>> {
|
||
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<String, Vec<FileInfo>> =
|
||
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<Vec<FileInfo>> {
|
||
let response: HashMap<String, Vec<FileInfo>> = 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<bool>,
|
||
) -> Result<String> {
|
||
let paths_str = paths
|
||
.iter()
|
||
.map(|&p| format!("\"{}\"", p))
|
||
.collect::<Vec<_>>()
|
||
.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<bool>,
|
||
) -> Result<String> {
|
||
let paths_str = paths
|
||
.iter()
|
||
.map(|&p| format!("\"{}\"", p))
|
||
.collect::<Vec<_>>()
|
||
.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<TaskStatus> {
|
||
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<bool>) -> Result<()> {
|
||
let paths_str = paths
|
||
.iter()
|
||
.map(|&p| format!("\"{}\"", p))
|
||
.collect::<Vec<_>>()
|
||
.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<bool>) -> Result<String> {
|
||
let paths_str = paths
|
||
.iter()
|
||
.map(|&p| format!("\"{}\"", p))
|
||
.collect::<Vec<_>>()
|
||
.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<TaskStatus> {
|
||
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<bool>,
|
||
pattern: Option<&str>,
|
||
file_type: Option<&str>,
|
||
extension: Option<&str>,
|
||
) -> Result<String> {
|
||
let paths_str = folder_paths
|
||
.iter()
|
||
.map(|&p| format!("\"{}\"", p))
|
||
.collect::<Vec<_>>()
|
||
.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<i32>,
|
||
limit: Option<i32>,
|
||
additional: Option<&[&str]>,
|
||
) -> Result<SearchResult> {
|
||
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::<Vec<_>>()
|
||
.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<CreatedLink> {
|
||
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<String, Vec<CreatedLink>> =
|
||
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<i32>,
|
||
limit: Option<i32>,
|
||
) -> Result<Vec<SharingLink>> {
|
||
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<String, Value> = 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<String> {
|
||
let paths_str = paths
|
||
.iter()
|
||
.map(|&p| format!("\"{}\"", p))
|
||
.collect::<Vec<_>>()
|
||
.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<TaskStatus> {
|
||
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<String> {
|
||
let paths_str = paths
|
||
.iter()
|
||
.map(|&p| format!("\"{}\"", p))
|
||
.collect::<Vec<_>>()
|
||
.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<TaskStatus> {
|
||
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<bool>,
|
||
password: Option<&str>,
|
||
) -> Result<String> {
|
||
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<TaskStatus> {
|
||
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<Vec<u8>> {
|
||
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<u8>,
|
||
create_parents: Option<bool>,
|
||
overwrite: Option<bool>,
|
||
) -> 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<EmptyData> = 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<Value> {
|
||
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<Value> {
|
||
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<Vec<u8>> {
|
||
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) {}
|
||
}
|