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..6c87eab 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,48 @@ pub fn move_file, D: AsRef>(source: S, destination: D) -> c Ok(()) } + +/// Set file/directory owner and permissions. +#[cfg(unix)] +pub fn set_file_permissions>( + source: S, + uid: Option, + gid: Option, + mode: Option, +) -> crate::Result<()> { + 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}.", + path.display() + ); + } + (Some(owner), None) => { + log::info!("Changed owner for {} to {owner}.", path.display()); + } + (None, Some(group)) => { + log::info!("Changed group for {} to {group}.", path.display()); + } + _ => (), + } + + if let Some(new_mode) = mode { + 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); + fs::set_permissions(path, permissions)?; // ← Works on paths (files & directories) + log::info!( + "Permission for {} changed from {old_mode:o} to {new_mode:o}.", + path.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"))]