From 87bf3473f5e4f283cc338e040a49198f0a58b180 Mon Sep 17 00:00:00 2001 From: "Christopher L. Crutchfield" Date: Mon, 9 Dec 2024 17:36:57 -0800 Subject: [PATCH 1/2] feat: synology api for listing structure. --- src/subcommands/main_subcommand.rs | 79 +++++++++++++++++++---- src/synology_api/file_station.rs | 100 +++++++++++++++++++++++++++-- src/synology_api/responses.rs | 74 +++++++++++++++++++++ 3 files changed, 236 insertions(+), 17 deletions(-) diff --git a/src/subcommands/main_subcommand.rs b/src/subcommands/main_subcommand.rs index 999c243..b3bf72e 100644 --- a/src/subcommands/main_subcommand.rs +++ b/src/subcommands/main_subcommand.rs @@ -82,9 +82,8 @@ impl CustomTransferAgent for MainSubcommand { }?; self.file_station = Some(file_station); - - let path = configuration.path.as_str(); - match self.create_folder(path).await { + + match self.create_target_folder().await { Ok(_) => Ok(()), Err(error) => { error_init(1, error.to_string().as_str())?; @@ -117,6 +116,18 @@ impl CustomTransferAgent for MainSubcommand { }; let source_path = Path::new(source_path.as_str()); + let target_path = format!( + "{}/{}", + configuration.path, + event.oid.clone().context("OID should not be none.")? + ); + + if self.exists_on_remote(target_path.as_str()).await? { + info!("Object already exists on server."); + + return Ok(()) + } + let file_station = self.file_station.clone().context("File Station should not be null")?; file_station.upload(source_path, event.size.context("Size should not be null")?, configuration.path.as_str(), false, false, None, None, None, Some(progress_reporter)).await?; @@ -144,21 +155,67 @@ impl MainSubcommand { } #[tracing::instrument] - async fn create_folder(&self, path: &str) -> Result<()> { + fn get_parent_path(&self, path: &str) -> Result> { + if path == "/" || path == "" { + return Ok(None) + } + + let path_parts = path.split('/'); + let name = path_parts.last().context("Our path should have a name since it's not the root.")?; + // We remove one extra character so that we don't have a trailing '/'. + Ok(Some(path[..(path.len() - name.len() - 1)].to_string())) + } + + #[tracing::instrument] + fn get_name(&self, path: &str) -> Result { + if path == "/" || path == "" { + return Ok("".to_string()); // We are the root. We don't have a name. + } + + let path_parts = path.split('/'); + let name = path_parts.last().context("Our path should have a name since it's not the root.")?; + + Ok(name.to_string()) + } + + #[tracing::instrument] + async fn exists_on_remote(&self, path: &str) -> Result { + if path == "/" || path == "" { + info!("Path is root."); + + return Ok(true); // The root should always exist. Don't need to ask the server to confirm. + } + + let name = self.get_name(path)?; + let parent = self.get_parent_path(path)?.context("Path should not be root since we checked earlier.")?; + + let file_station = self.file_station.clone().context("File Station should not be null")?; + + // if parent is root, ask for list of shares then check if path is one of the shares + // else if parent is not root, ask for files in parent and check if path is one of the files + + //let list = file_station.list(path).await?; + + Ok(false) + } + + #[tracing::instrument] + async fn create_target_folder(&self) -> Result<()> { let configuration = Configuration::load()?; + if self.exists_on_remote(&configuration.path).await? { + return Ok(()); // Exit early, handle trying to create a folder over a share. + } + // This is a System wide, cross-process lock. - let lock = NamedLock::create("git-lfs-synology::MainSubcommand::create_folder")?; + let lock = NamedLock::create("git-lfs-synology::MainSubcommand::create_target_folder")?; let _guard = lock.lock()?; let file_station = self.file_station.clone().context("File Station should not be null.")?; - let path_parts = configuration.path.split('/'); - let name = path_parts.last().context("Our path should have a name")?; - // We remove one extra character so that we don't have a trailing '/'. - let folder_path_string = configuration.path[..(configuration.path.len() - name.len() - 1)].to_string(); - let folder_path = folder_path_string.as_str(); - let _folders = file_station.create_folder(folder_path, name, true).await?; + let name = self.get_name(&configuration.path)?; + let folder_path = self.get_parent_path(&configuration.path)?.context("Path should not be root.")?; + let _folders = file_station.create_folder(folder_path.as_str(), name.as_str(), true).await?; Ok(()) } diff --git a/src/synology_api/file_station.rs b/src/synology_api/file_station.rs index 29a164f..5093db1 100644 --- a/src/synology_api/file_station.rs +++ b/src/synology_api/file_station.rs @@ -9,7 +9,7 @@ use urlencoding::encode; use crate::credential_manager::Credential; -use super::{responses::{CreateFolderResponse, LoginError, LoginResponse, SynologyError, SynologyErrorStatus, SynologyResult, SynologyStatusCode}, ProgressReporter}; +use super::{responses::{CreateFolderResponse, ListResponse, LoginError, LoginResponse, SynologyError, SynologyErrorStatus, SynologyResult, SynologyStatusCode}, ProgressReporter}; #[derive(Clone, Debug)] pub struct SynologyFileStation { @@ -27,7 +27,7 @@ impl SynologyFileStation { } #[tracing::instrument] - async fn get(&self, api: &str, method: &str, version: u32, parameters: &HashMap<&str, &str>) -> Result { + async fn get(&self, api: &str, method: &str, version: u32, parameters: &HashMap<&str, String>) -> Result { match &self.sid { Some(sid) => { info!("Found sid, continuing."); @@ -130,10 +130,10 @@ impl SynologyFileStation { pub async fn create_folder(&self, folder_path: &str, name: &str, force_parent: bool) -> Result { let force_parent_string = force_parent.to_string(); - let mut parameters = HashMap::<&str, &str>::new(); - parameters.insert("folder_path", folder_path); - parameters.insert("name", name); - parameters.insert("force_parent", force_parent_string.as_str()); + let mut parameters = HashMap::<&str, String>::new(); + parameters.insert("folder_path", folder_path.to_string()); + parameters.insert("name", name.to_string()); + parameters.insert("force_parent", force_parent_string); self.get("SYNO.FileStation.CreateFolder", "create", 2, ¶meters).await } @@ -206,6 +206,94 @@ impl SynologyFileStation { } } + #[allow(clippy::too_many_arguments)] // Allow this so that we better match the Synology API. + #[tracing::instrument] + pub async fn list( + &self, + folder_path: &str, + offset: Option, + limit: Option, + sort_by: Option, + sort_direction: Option, + pattern: Option, + file_type: Option, + goto_path: Option, + include_real_path: bool, + include_size: bool, + include_owner: bool, + include_time: bool, + include_perm: bool, + include_mount_point_type: bool, + include_type: bool + ) -> Result { + let mut parameters = HashMap::<&str, String>::new(); + parameters.insert("folder_path", folder_path.to_string()); + + if let Some(offset) = offset { + parameters.insert("offset", offset.to_string()); + } + + if let Some(limit) = limit { + parameters.insert("limit", limit.to_string()); + } + + if let Some(sort_by) = sort_by { + parameters.insert("sort_by", sort_by); + } + + if let Some(sort_direction) = sort_direction { + parameters.insert("sort_direction", sort_direction); + } + + if let Some(pattern) = pattern { + parameters.insert("pattern", pattern); + } + + if let Some(file_type) = file_type { + parameters.insert("filetype", file_type); + } + + if let Some(goto_path) = goto_path { + parameters.insert("goto_path", goto_path); + } + + let mut additional: String = String::new(); + + if include_real_path { + additional = format!("{},real_path", additional); + } + + if include_size { + additional = format!("{},size", additional); + } + + if include_owner { + additional = format!("{},owner", additional); + } + + if include_time { + additional = format!("{},time", additional); + } + + if include_perm { + additional = format!("{},perm", additional); + } + + if include_mount_point_type { + additional = format!("{},mount_point_type", additional); + } + + if include_type { + additional = format!("{},type", additional); + } + + if additional.len() > 0 { + parameters.insert("additional", additional[1..].to_string()); + } + + self.get("SYNO.FileStation.List", "list", 2, ¶meters).await + } + #[tracing::instrument] pub async fn login(&mut self, credential: &Credential, enable_device_token: bool, totp: Option) -> Result { let device_name = format!( diff --git a/src/synology_api/responses.rs b/src/synology_api/responses.rs index b3c7d93..ae4f942 100644 --- a/src/synology_api/responses.rs +++ b/src/synology_api/responses.rs @@ -140,3 +140,77 @@ pub struct FolderModel { pub name: String, pub path: String } + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde[rename_all = "snake_case"]] +pub struct ListResponse { + pub total: u64, + pub offset: u64, + pub files: Vec +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde[rename_all = "snake_case"]] +pub struct File { + pub path: String, + pub name: String, + pub isdir: bool, + pub children: Option +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde[rename_all = "snake_case"]] +pub struct FileChildren { + total: u32, + offeset: i32, + files: Vec +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde[rename_all = "snake_case"]] +pub struct FileAdditional { + pub real_path: Option, + pub size: Option, + pub owner: Option, + pub time: Option, + pub perm: Option, + pub mount_point_time: Option, + #[serde(alias = "type")] + pub extension: Option +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde[rename_all = "snake_case"]] +pub struct FileOwner { + pub user: String, + pub group: String, + pub uid: i32, + pub gid: i32 +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde[rename_all = "snake_case"]] +pub struct FileTime { + pub atime: u64, + pub mtime: u64, + pub ctime: u64, + pub crtime: u64 +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde[rename_all = "snake_case"]] +pub struct FilePerm { + pub posix: u32, + pub is_acl_mode: bool, + pub acl: FileAcl +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde[rename_all = "snake_case"]] +pub struct FileAcl { + pub append: bool, + pub del: bool, + pub exec: bool, + pub read: bool, + pub write: bool +} \ No newline at end of file From 0cc5753eaee277e35d365b9cc50476649314a7c6 Mon Sep 17 00:00:00 2001 From: "Christopher L. Crutchfield" Date: Mon, 9 Dec 2024 21:11:45 -0800 Subject: [PATCH 2/2] feat: check that file exists before uploading it. --- src/subcommands/main_subcommand.rs | 27 ++++++++--- src/synology_api/file_station.rs | 78 +++++++++++++++++++++++++++++- src/synology_api/responses.rs | 68 +++++++++++++++++++++++--- 3 files changed, 159 insertions(+), 14 deletions(-) diff --git a/src/subcommands/main_subcommand.rs b/src/subcommands/main_subcommand.rs index b3bf72e..e4fb98f 100644 --- a/src/subcommands/main_subcommand.rs +++ b/src/subcommands/main_subcommand.rs @@ -154,9 +154,14 @@ impl MainSubcommand { } } + #[tracing::instrument] + fn is_path_root(&self, path: &str) -> bool { + path == "/" || path == "" + } + #[tracing::instrument] fn get_parent_path(&self, path: &str) -> Result> { - if path == "/" || path == "" { + if self.is_path_root(path) { return Ok(None) } @@ -168,7 +173,7 @@ impl MainSubcommand { #[tracing::instrument] fn get_name(&self, path: &str) -> Result { - if path == "/" || path == "" { + if self.is_path_root(path) { return Ok("".to_string()); // We are the root. We don't have a name. } @@ -180,7 +185,7 @@ impl MainSubcommand { #[tracing::instrument] async fn exists_on_remote(&self, path: &str) -> Result { - if path == "/" || path == "" { + if self.is_path_root(path) { info!("Path is root."); return Ok(true); // The root should always exist. Don't need to ask the server to confirm. @@ -191,12 +196,20 @@ impl MainSubcommand { let file_station = self.file_station.clone().context("File Station should not be null")?; - // if parent is root, ask for list of shares then check if path is one of the shares - // else if parent is not root, ask for files in parent and check if path is one of the files + if self.is_path_root(&parent) { + let shares = file_station.list_share( + None, None, None, None, None, + false, false, false, false, false, false, false).await?; - //let list = file_station.list(path).await?; + return Ok(shares.shares.iter().any(|share| share.name == name)); + } + else { + let files = file_station.list( + &parent, None, None, None, None, None, None, None, + false, false, false, false, false, false, false).await?; - Ok(false) + return Ok(files.files.iter().any(|file| file.name == name)); + } } #[tracing::instrument] diff --git a/src/synology_api/file_station.rs b/src/synology_api/file_station.rs index 5093db1..82a5a2c 100644 --- a/src/synology_api/file_station.rs +++ b/src/synology_api/file_station.rs @@ -9,7 +9,7 @@ use urlencoding::encode; use crate::credential_manager::Credential; -use super::{responses::{CreateFolderResponse, ListResponse, LoginError, LoginResponse, SynologyError, SynologyErrorStatus, SynologyResult, SynologyStatusCode}, ProgressReporter}; +use super::{responses::{CreateFolderResponse, ListResponse, ListShareResponse, LoginError, LoginResponse, SynologyError, SynologyErrorStatus, SynologyResult, SynologyStatusCode}, ProgressReporter}; #[derive(Clone, Debug)] pub struct SynologyFileStation { @@ -294,6 +294,82 @@ impl SynologyFileStation { self.get("SYNO.FileStation.List", "list", 2, ¶meters).await } + #[allow(clippy::too_many_arguments)] // Allow this so that we better match the Synology API. + #[tracing::instrument] + pub async fn list_share( + &self, + offset: Option, + limit: Option, + sort_by: Option, + sort_direction: Option, + only_writable: Option, + include_real_path: bool, + include_size: bool, + include_owner: bool, + include_time: bool, + include_perm: bool, + include_mount_point_type: bool, + include_volume_status: bool + ) -> Result { + let mut parameters = HashMap::<&str, String>::new(); + + if let Some(offset) = offset { + parameters.insert("offset", offset.to_string()); + } + + if let Some(limit) = limit { + parameters.insert("limit", limit.to_string()); + } + + if let Some(sort_by) = sort_by { + parameters.insert("sort_by", sort_by); + } + + if let Some(sort_direction) = sort_direction { + parameters.insert("sort_direction", sort_direction); + } + + if let Some(only_writable) = only_writable { + parameters.insert("only_writable", only_writable.to_string()); + } + + let mut additional: String = String::new(); + + if include_real_path { + additional = format!("{},real_path", additional); + } + + if include_size { + additional = format!("{},size", additional); + } + + if include_owner { + additional = format!("{},owner", additional); + } + + if include_time { + additional = format!("{},time", additional); + } + + if include_perm { + additional = format!("{},perm", additional); + } + + if include_mount_point_type { + additional = format!("{},mount_point_type", additional); + } + + if include_volume_status { + additional = format!("{},volume_status", additional); + } + + if additional.len() > 0 { + parameters.insert("additional", additional[1..].to_string()); + } + + self.get("SYNO.FileStation.List", "list_share", 2, ¶meters).await + } + #[tracing::instrument] pub async fn login(&mut self, credential: &Credential, enable_device_token: bool, totp: Option) -> Result { let device_name = format!( diff --git a/src/synology_api/responses.rs b/src/synology_api/responses.rs index ae4f942..cd3a425 100644 --- a/src/synology_api/responses.rs +++ b/src/synology_api/responses.rs @@ -171,8 +171,8 @@ pub struct FileChildren { pub struct FileAdditional { pub real_path: Option, pub size: Option, - pub owner: Option, - pub time: Option, + pub owner: Option, + pub time: Option