Files
AE_Anons/src/synology.rs

1349 lines
40 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#![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) {}
}