From a6c2194a8ff32f7a122c1ebe27686d03c707ab6d Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Sat, 4 Jul 2026 00:51:25 +0200 Subject: [PATCH 1/2] feat(import): Add support for setting permissions --- src/cli/import.rs | 5 ++++ src/config.rs | 18 +++++++++++++ src/taggedfilecollection.rs | 23 +++++++++++++++++ src/util/fs.rs | 51 ++++++++++++++++++++++++++++++++++++- src/util/mod.rs | 2 ++ 5 files changed, 98 insertions(+), 1 deletion(-) diff --git a/src/cli/import.rs b/src/cli/import.rs index 5a541ab..0829612 100644 --- a/src/cli/import.rs +++ b/src/cli/import.rs @@ -232,6 +232,11 @@ pub async fn run(config: &Config, cache: Option<&Cache>, args: Args) -> crate::R if let Err(err) = track_collection.write_tags() { log::error!("Failed to write tags: {err}"); } + + #[cfg(unix)] + if let Err(err) = track_collection.set_permissions(&cloned_config) { + log::error!("Failed to set permissions: {err}"); + } } }); diff --git a/src/config.rs b/src/config.rs index d02062b..f9012d6 100644 --- a/src/config.rs +++ b/src/config.rs @@ -379,6 +379,20 @@ pub struct PathTemplateConfig { pub compilation_format: String, } +/// The import configuration struct. +#[cfg(unix)] +#[expect(missing_copy_implementations)] +#[expect(clippy::struct_field_names)] +#[derive(Debug, Default, Clone, Deserialize, Serialize)] +pub struct ImportConfig { + /// User ID to set. + pub set_uid: Option, + /// Group ID to set. + pub set_gid: Option, + /// File mode to set. + pub set_mode: Option, +} + /// The main configuration struct. #[derive(Debug, Clone, Deserialize, Serialize)] pub struct Config { @@ -392,6 +406,10 @@ pub struct Config { pub weights: DistanceWeights, /// UI configuration. pub user_interface: UiConfig, + /// Import configuration. + #[cfg(unix)] + #[serde(default)] + pub import: ImportConfig, } impl Default for Config { diff --git a/src/taggedfilecollection.rs b/src/taggedfilecollection.rs index 1286e8a..3dbe403 100644 --- a/src/taggedfilecollection.rs +++ b/src/taggedfilecollection.rs @@ -294,6 +294,29 @@ impl TaggedFileCollection { Ok(()) } + + /// Set permission for all tracks in this collection. + /// + /// # Errors + /// + /// Returns an error if any of the underlying file system operations fail. + #[cfg(unix)] + pub fn set_permissions(&mut self, config: &Config) -> crate::Result<()> { + for track in &mut self + .media + .iter_mut() + .flat_map(|media| media.tracks.iter_mut()) + { + util::set_file_permissions( + &track.path, + config.import.set_uid, + config.import.set_gid, + config.import.set_mode, + )?; + } + + Ok(()) + } } impl IntoIterator for TaggedFileCollection { diff --git a/src/util/fs.rs b/src/util/fs.rs index 47e5622..641ceb2 100644 --- a/src/util/fs.rs +++ b/src/util/fs.rs @@ -12,11 +12,11 @@ use std::collections::BinaryHeap; use std::ffi::OsStr; use std::fs; use std::io; +use std::os::unix::{self, fs::PermissionsExt}; use std::path::{Path, PathBuf}; /// An iterator that recursively walks through a directory structure and yields a tuple `(path, /// dirs, files)` for each directory it visits. -/// /// This struct is created by [`walk_dir`]. See its documentation for more. pub struct DirWalk { /// Queued paths that will be visited next. @@ -117,3 +117,52 @@ pub fn move_file, D: AsRef>(source: S, destination: D) -> c Ok(()) } + +/// Set file owner. +#[cfg(unix)] +pub fn set_file_permissions>( + source: S, + uid: Option, + gid: Option, + mode: Option, +) -> crate::Result<()> { + unix::fs::chown(&source, uid, gid)?; + match (uid, gid) { + (Some(owner), Some(group)) => { + log::info!( + "Changed owner/group for {} to {owner}:{group}.", + source.as_ref().display() + ); + } + (Some(owner), None) => { + log::info!( + "Changed owner for {} to {owner}.", + source.as_ref().display() + ); + } + (None, Some(group)) => { + log::info!( + "Changed group for {} to {group}.", + source.as_ref().display() + ); + } + _ => (), + } + + if let Some(new_mode) = mode { + let file = fs::File::open(&source)?; + let permissions = file.metadata()?.permissions(); + let old_mode = permissions.mode(); + + if permissions.mode() != new_mode { + let permissions = fs::Permissions::from_mode(new_mode); + file.set_permissions(permissions)?; + log::info!( + "Permission for {} changed from {old_mode:o} to {new_mode:o}.", + source.as_ref().display() + ); + } + } + + Ok(()) +} diff --git a/src/util/mod.rs b/src/util/mod.rs index 99befeb..2875606 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -14,6 +14,8 @@ mod keyed_binheap; mod testing; mod time; +#[cfg(unix)] +pub use fs::set_file_permissions; pub use fs::{move_file, walk_dir}; pub use keyed_binheap::KeyedBinaryHeap; #[cfg(any(test, feature = "dev"))] From 97baaa4dbc2cb447acd53be637208ce066b3bcd8 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Sun, 12 Jul 2026 10:05:01 +0200 Subject: [PATCH 2/2] chore(util): Make `set_file_permissions` work for directories as well --- src/util/fs.rs | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/util/fs.rs b/src/util/fs.rs index 641ceb2..6c87eab 100644 --- a/src/util/fs.rs +++ b/src/util/fs.rs @@ -118,7 +118,7 @@ pub fn move_file, D: AsRef>(source: S, destination: D) -> c Ok(()) } -/// Set file owner. +/// Set file/directory owner and permissions. #[cfg(unix)] pub fn set_file_permissions>( source: S, @@ -126,40 +126,36 @@ pub fn set_file_permissions>( gid: Option, mode: Option, ) -> crate::Result<()> { - unix::fs::chown(&source, uid, gid)?; + let path = source.as_ref(); + + unix::fs::chown(path, uid, gid)?; match (uid, gid) { (Some(owner), Some(group)) => { log::info!( "Changed owner/group for {} to {owner}:{group}.", - source.as_ref().display() + path.display() ); } (Some(owner), None) => { - log::info!( - "Changed owner for {} to {owner}.", - source.as_ref().display() - ); + log::info!("Changed owner for {} to {owner}.", path.display()); } (None, Some(group)) => { - log::info!( - "Changed group for {} to {group}.", - source.as_ref().display() - ); + log::info!("Changed group for {} to {group}.", path.display()); } _ => (), } if let Some(new_mode) = mode { - let file = fs::File::open(&source)?; - let permissions = file.metadata()?.permissions(); + let metadata = fs::metadata(path)?; + let permissions = metadata.permissions(); let old_mode = permissions.mode(); if permissions.mode() != new_mode { let permissions = fs::Permissions::from_mode(new_mode); - file.set_permissions(permissions)?; + fs::set_permissions(path, permissions)?; // ← Works on paths (files & directories) log::info!( "Permission for {} changed from {old_mode:o} to {new_mode:o}.", - source.as_ref().display() + path.display() ); } }