55 lines
1.5 KiB
Rust
55 lines
1.5 KiB
Rust
|
|
use std::path::{Path, PathBuf};
|
||
|
|
|
||
|
|
/// Move a file from inbox to the permanent storage directory.
|
||
|
|
///
|
||
|
|
/// Creates the directory structure: `storage_dir/artist/album/filename`
|
||
|
|
/// Returns the full path of the moved file.
|
||
|
|
///
|
||
|
|
/// If `rename` fails (cross-device), falls back to copy + remove.
|
||
|
|
pub async fn move_to_storage(
|
||
|
|
storage_dir: &Path,
|
||
|
|
artist: &str,
|
||
|
|
album: &str,
|
||
|
|
filename: &str,
|
||
|
|
source: &Path,
|
||
|
|
) -> anyhow::Result<PathBuf> {
|
||
|
|
let artist_dir = sanitize_dir_name(artist);
|
||
|
|
let album_dir = sanitize_dir_name(album);
|
||
|
|
|
||
|
|
let dest_dir = storage_dir.join(&artist_dir).join(&album_dir);
|
||
|
|
tokio::fs::create_dir_all(&dest_dir).await?;
|
||
|
|
|
||
|
|
let dest = dest_dir.join(filename);
|
||
|
|
|
||
|
|
// Avoid overwriting existing files
|
||
|
|
if dest.exists() {
|
||
|
|
anyhow::bail!("Destination already exists: {:?}", dest);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Try atomic rename first (same filesystem)
|
||
|
|
match tokio::fs::rename(source, &dest).await {
|
||
|
|
Ok(()) => {}
|
||
|
|
Err(_) => {
|
||
|
|
// Cross-device: copy then remove
|
||
|
|
tokio::fs::copy(source, &dest).await?;
|
||
|
|
tokio::fs::remove_file(source).await?;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
tracing::info!(from = ?source, to = ?dest, "moved file to storage");
|
||
|
|
Ok(dest)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Remove characters that are unsafe for directory names.
|
||
|
|
fn sanitize_dir_name(name: &str) -> String {
|
||
|
|
name.chars()
|
||
|
|
.map(|c| match c {
|
||
|
|
'/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' | '\0' => '_',
|
||
|
|
_ => c,
|
||
|
|
})
|
||
|
|
.collect::<String>()
|
||
|
|
.trim()
|
||
|
|
.trim_matches('.')
|
||
|
|
.to_owned()
|
||
|
|
}
|