From 5614d20bedcc4dd268136d389ad796b404a69d2c Mon Sep 17 00:00:00 2001 From: llama Date: Thu, 20 Nov 2025 23:43:14 +0800 Subject: [PATCH] fix(Import): case-fold media filenames when checking uniqueness (#4435) * add wrapper struct with case-folding get impl * use wrapper struct * restrict case-folding to windows * Revert "restrict case-folding to windows" This reverts commit aad01d904f07b28466190d849141883e8a76e1c2. * case-fold filenames for newly added media * add test * fix incorrect comment --- .../package/apkg/import/media.rs | 3 ++- rslib/src/media/files.rs | 12 ++++++++++-- rslib/src/media/mod.rs | 6 +++--- rslib/src/sync/media/database/client/mod.rs | 19 +++++++++++++++++-- 4 files changed, 32 insertions(+), 8 deletions(-) diff --git a/rslib/src/import_export/package/apkg/import/media.rs b/rslib/src/import_export/package/apkg/import/media.rs index 32bf7c807..20543e074 100644 --- a/rslib/src/import_export/package/apkg/import/media.rs +++ b/rslib/src/import_export/package/apkg/import/media.rs @@ -17,6 +17,7 @@ use crate::import_export::package::media::SafeMediaEntry; use crate::import_export::ImportProgress; use crate::media::files::add_hash_suffix_to_file_stem; use crate::media::files::sha1_of_reader; +use crate::media::Checksums; use crate::prelude::*; use crate::progress::ThrottlingProgressHandler; @@ -75,7 +76,7 @@ impl Context<'_> { fn prepare_media( media_entries: Vec, archive: &mut ZipArchive, - existing_sha1s: &HashMap, + existing_sha1s: &Checksums, progress: &mut ThrottlingProgressHandler, ) -> Result { let mut media_map = MediaUseMap::default(); diff --git a/rslib/src/media/files.rs b/rslib/src/media/files.rs index ce17b40bb..b098eb19e 100644 --- a/rslib/src/media/files.rs +++ b/rslib/src/media/files.rs @@ -173,7 +173,9 @@ pub fn add_data_to_folder_uniquely<'a, P>( where P: AsRef, { - let normalized_name = normalize_filename(desired_name); + // force lowercase to account for case-insensitive filesystems + // but not within normalize_filename, for existing media refs + let normalized_name: Cow<_> = normalize_filename(desired_name).to_lowercase().into(); let mut target_path = folder.as_ref().join(normalized_name.as_ref()); @@ -496,8 +498,14 @@ mod test { "test.mp3" ); - // different contents + // different contents, filenames differ only by case let h2 = sha1_of_data(b"hello1"); + assert_eq!( + add_data_to_folder_uniquely(dpath, "Test.mp3", b"hello1", h2).unwrap(), + "test-88fdd585121a4ccb3d1540527aee53a77c77abb8.mp3" + ); + + // same contents, filenames differ only by case assert_eq!( add_data_to_folder_uniquely(dpath, "test.mp3", b"hello1", h2).unwrap(), "test-88fdd585121a4ccb3d1540527aee53a77c77abb8.mp3" diff --git a/rslib/src/media/mod.rs b/rslib/src/media/mod.rs index 259dd52f8..8a599fbec 100644 --- a/rslib/src/media/mod.rs +++ b/rslib/src/media/mod.rs @@ -6,7 +6,6 @@ pub mod files; mod service; use std::borrow::Cow; -use std::collections::HashMap; use std::path::Path; use std::path::PathBuf; @@ -22,6 +21,7 @@ use crate::progress::ThrottlingProgressHandler; use crate::sync::http_client::HttpSyncClient; use crate::sync::login::SyncAuth; use crate::sync::media::database::client::changetracker::ChangeTracker; +pub use crate::sync::media::database::client::Checksums; use crate::sync::media::database::client::MediaDatabase; use crate::sync::media::database::client::MediaEntry; use crate::sync::media::progress::MediaSyncProgress; @@ -157,7 +157,7 @@ impl MediaManager { pub fn all_checksums_after_checking( &self, progress: impl FnMut(usize) -> bool, - ) -> Result> { + ) -> Result { ChangeTracker::new(&self.media_folder, progress).register_changes(&self.db)?; self.db.all_registered_checksums() } @@ -176,7 +176,7 @@ impl MediaManager { /// All checksums without registering changes first. #[cfg(test)] - pub(crate) fn all_checksums_as_is(&self) -> HashMap { + pub(crate) fn all_checksums_as_is(&self) -> Checksums { self.db.all_registered_checksums().unwrap() } } diff --git a/rslib/src/sync/media/database/client/mod.rs b/rslib/src/sync/media/database/client/mod.rs index f9c6e5ed1..fe3e7c840 100644 --- a/rslib/src/sync/media/database/client/mod.rs +++ b/rslib/src/sync/media/database/client/mod.rs @@ -18,6 +18,20 @@ use crate::prelude::*; pub mod changetracker; +pub struct Checksums(HashMap); + +impl Checksums { + // case-fold filenames when checking files to be imported + // to account for case-insensitive filesystems + pub fn get(&self, key: impl AsRef) -> Option<&Sha1Hash> { + self.0.get(key.as_ref().to_lowercase().as_str()) + } + + pub fn contains_key(&self, key: impl AsRef) -> bool { + self.get(key).is_some() + } +} + #[derive(Debug, PartialEq, Eq)] pub struct MediaEntry { pub fname: String, @@ -175,11 +189,12 @@ delete from media where fname=?", } /// Returns all filenames and checksums, where the checksum is not null. - pub(crate) fn all_registered_checksums(&self) -> error::Result> { + pub(crate) fn all_registered_checksums(&self) -> error::Result { self.db .prepare("SELECT fname, csum FROM media WHERE csum IS NOT NULL")? .query_and_then([], row_to_name_and_checksum)? - .collect() + .collect::>() + .map(Checksums) } pub(crate) fn force_resync(&self) -> error::Result<()> {