diff --git a/Cargo.lock b/Cargo.lock index 6b17a19..9c2c123 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -89,9 +89,9 @@ checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" [[package]] name = "autocfg" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "backtrace" @@ -911,9 +911,9 @@ checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "num-traits" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] diff --git a/src/cli.rs b/src/cli.rs index ae8149d..788c7e4 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -75,7 +75,11 @@ pub enum Command { /// Album name; can be a person name in "People" auto-album album_name: String, - /// Folder name in user's Personal Space (must exist) + /// Create target folder if it does not exist in Personal Space + #[arg(long)] + create: bool, + + /// Folder name in user's Personal Space folder_path: String, }, diff --git a/src/commands/export/api_client.rs b/src/commands/export/api_client.rs index b0b72ac..08c5d26 100644 --- a/src/commands/export/api_client.rs +++ b/src/commands/export/api_client.rs @@ -8,13 +8,13 @@ use syno_api::foto::background_task::file::dto::TaskInfo; use syno_api::foto::{self, browse::folder::dto::Folder}; use syno_api::foto_team; +#[derive(Debug, Deserialize)] +struct FolderContainer { + folder: Folder, +} + impl<'a, C: ApiClient> SessionClient<'a, C> { pub async fn get_folder_by_name(&self, name: &str) -> Result { - #[derive(Debug, Deserialize)] - struct FolderContainer { - folder: Folder, - } - let folder: FolderContainer = self .client .get( @@ -26,6 +26,21 @@ impl<'a, C: ApiClient> SessionClient<'a, C> { Ok(folder.folder) } + pub async fn create_folder(&self, name: &str, parent_id: u32) -> Result { + let folder: FolderContainer = self + .client + .post( + self.dsm_url.clone(), + ApiParams::new(foto::browse::folder::API, "create", 1), + &[ + ("name", name), + ("target_id", parent_id.to_string().as_str()), + ], + ) + .await?; + Ok(folder.folder) + } + pub async fn copy_photos( &self, photo_ids: &[u32], diff --git a/src/commands/export/mod.rs b/src/commands/export/mod.rs index 4f6de95..9c06833 100644 --- a/src/commands/export/mod.rs +++ b/src/commands/export/mod.rs @@ -22,6 +22,7 @@ mod api_client; pub async fn handle( album_name: &str, target_folder_path: &str, + create_folder: bool, conf: &Conf, client: &C, io: &mut I, @@ -37,7 +38,7 @@ pub async fn handle( } let folder_path = format!("/{}", target_folder_path.trim().trim_matches('/')); - log::info!("Target folder: {folder_path}"); + log::info!("target folder: {folder_path}"); let folder_future = client.get_folder_by_name(folder_path.as_str()); let find_album_future = find_album(album_name, &user_settings, &client); @@ -45,6 +46,9 @@ pub async fn handle( let folder = match folder_future.await { Ok(folder) => folder, Err(error) => match error.downcast::()? { + DsmError::Photo(PhotoError::NoAccessOrNotFound) if create_folder => { + create_folder_path(folder_path.as_str(), &client).await? + } DsmError::Photo(PhotoError::NoAccessOrNotFound) => { bail!("folder '{target_folder_path}' does not exist in Personal Space") } @@ -64,6 +68,38 @@ pub async fn handle( } } +async fn create_folder_path( + folder_path: &str, + client: &SessionClient<'_, C>, +) -> Result { + let path_segments: Vec<_> = folder_path.split('/').filter(|s| !s.is_empty()).collect(); + if path_segments.iter().any(|s| s.trim().is_empty()) { + bail!("{folder_path} is not valid folder path"); + } + let mut result_folder = client.get_folder_by_name("/").await?; + let mut path_so_far = String::new(); + let mut exists = true; + for segment in path_segments { + path_so_far.push('/'); + path_so_far.push_str(segment); + if exists { + let folder_result = client.get_folder_by_name(path_so_far.as_str()).await; + match folder_result { + Ok(folder) => result_folder = folder, + Err(error) => match error.downcast::()? { + DsmError::Photo(PhotoError::NoAccessOrNotFound) => exists = false, + other => bail!(other), + }, + } + } + if !exists { + result_folder = client.create_folder(segment, result_folder.id).await?; + log::info!("created {path_so_far} folder") + } + } + Ok(result_folder) +} + async fn export( (album, target_folder, user_settings): (Album, Folder, UserSettings), client: &SessionClient<'_, C>, diff --git a/src/commands/login/mod.rs b/src/commands/login/mod.rs index fa699d6..116ade2 100644 --- a/src/commands/login/mod.rs +++ b/src/commands/login/mod.rs @@ -66,7 +66,7 @@ fn unwrap_or_read_dsm_url(url: Option, conf: &Conf, io: &mut I) -> R dsm_url .set_port(Some(port)) .expect("DSM URL address should be valid"); - log::info!("Using DSM address: {dsm_url}") + log::info!("using DSM address: {dsm_url}") } Ok(dsm_url) } diff --git a/src/lib.rs b/src/lib.rs index da0c25c..d3e2385 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -56,11 +56,13 @@ pub async fn run( } Command::Export { album_name, + create, folder_path, } => { export::handle( album_name.as_str(), folder_path.as_str(), + create, &conf, &client.client, io,