From 83789709addddd67909f68da1abec2b5ef939f2d Mon Sep 17 00:00:00 2001 From: Elias Projahn Date: Sun, 30 Mar 2025 10:52:27 +0200 Subject: [PATCH] Cross platform track paths --- .../2025-03-30-065511_json_paths/down.sql | 1 + .../2025-03-30-065511_json_paths/up.sql | 1 + src/db/models.rs | 6 +- src/db/tables.rs | 71 ++++++++++++++++++- src/editor/tracks/track_row.rs | 2 +- src/library.rs | 34 ++++++--- src/player.rs | 4 +- 7 files changed, 102 insertions(+), 17 deletions(-) create mode 100644 migrations/2025-03-30-065511_json_paths/down.sql create mode 100644 migrations/2025-03-30-065511_json_paths/up.sql diff --git a/migrations/2025-03-30-065511_json_paths/down.sql b/migrations/2025-03-30-065511_json_paths/down.sql new file mode 100644 index 0000000..bda88ad --- /dev/null +++ b/migrations/2025-03-30-065511_json_paths/down.sql @@ -0,0 +1 @@ +UPDATE tracks SET path = (SELECT group_concat(value, '/') FROM json_each(tracks.path)); \ No newline at end of file diff --git a/migrations/2025-03-30-065511_json_paths/up.sql b/migrations/2025-03-30-065511_json_paths/up.sql new file mode 100644 index 0000000..8684474 --- /dev/null +++ b/migrations/2025-03-30-065511_json_paths/up.sql @@ -0,0 +1 @@ +UPDATE tracks SET path = '["' || replace(path, '/', '","') || '"]'; \ No newline at end of file diff --git a/src/db/models.rs b/src/db/models.rs index 26aec29..a33cc49 100644 --- a/src/db/models.rs +++ b/src/db/models.rs @@ -1,7 +1,7 @@ //! This module contains higher-level models combining information from //! multiple database tables. -use std::{collections::HashSet, fmt::Display}; +use std::{collections::HashSet, fmt::Display, path::PathBuf}; use anyhow::Result; use diesel::prelude::*; @@ -61,7 +61,7 @@ pub struct EnsemblePerformer { #[derive(Clone, Debug)] pub struct Track { pub track_id: String, - pub path: String, + pub path: PathBuf, pub works: Vec, } @@ -405,7 +405,7 @@ impl Track { Ok(Self { track_id: data.track_id, - path: data.path, + path: data.path.0, works, }) } diff --git a/src/db/tables.rs b/src/db/tables.rs index b697401..42bb8ec 100644 --- a/src/db/tables.rs +++ b/src/db/tables.rs @@ -1,8 +1,19 @@ //! This module contains structs that are one-to-one representations of the //! tables in the database schema. +use std::path::{Path, PathBuf}; + +use anyhow::{anyhow, Result}; use chrono::NaiveDateTime; -use diesel::{prelude::*, sqlite::Sqlite}; +use diesel::{ + backend::Backend, + deserialize::{FromSql, FromSqlRow}, + expression::AsExpression, + prelude::*, + serialize::{IsNull, Output, ToSql}, + sql_types::Text, + sqlite::Sqlite, +}; use gtk::glib::{self, Boxed}; use super::{schema::*, TranslatedString}; @@ -131,7 +142,7 @@ pub struct Track { pub recording_index: i32, pub medium_id: Option, pub medium_index: Option, - pub path: String, + pub path: PathBufWrapper, pub created_at: NaiveDateTime, pub edited_at: NaiveDateTime, pub last_used_at: NaiveDateTime, @@ -183,3 +194,59 @@ pub struct AlbumMedium { pub medium_id: String, pub sequence_number: i32, } + +#[derive(AsExpression, FromSqlRow, Clone, Debug)] +#[diesel(sql_type = Text)] +pub struct PathBufWrapper(pub PathBuf); + +impl ToSql for PathBufWrapper +where + String: ToSql, +{ + fn to_sql(&self, out: &mut Output) -> diesel::serialize::Result { + out.set_value(serde_json::to_string( + &self + .0 + .iter() + .map(|p| { + p.to_str() + .ok_or_else(|| anyhow!("Path contains invalid UTF-8")) + }) + .collect::>>()?, + )?); + + Ok(IsNull::No) + } +} + +impl FromSql for PathBufWrapper +where + DB: Backend, + String: FromSql, +{ + fn from_sql(bytes: DB::RawValue<'_>) -> diesel::deserialize::Result { + Ok(PathBufWrapper( + serde_json::from_str::>(&String::from_sql(bytes)?)? + .into_iter() + .collect(), + )) + } +} + +impl From for PathBufWrapper { + fn from(value: PathBuf) -> Self { + PathBufWrapper(value) + } +} + +impl From for PathBuf { + fn from(value: PathBufWrapper) -> Self { + value.0 + } +} + +impl AsRef for PathBufWrapper { + fn as_ref(&self) -> &Path { + self.0.as_ref() + } +} diff --git a/src/editor/tracks/track_row.rs b/src/editor/tracks/track_row.rs index 5aeb707..f675ec5 100644 --- a/src/editor/tracks/track_row.rs +++ b/src/editor/tracks/track_row.rs @@ -149,7 +149,7 @@ impl TracksEditorTrackRow { obj.set_subtitle(&match &track_data.location { TrackLocation::Undefined => String::new(), - TrackLocation::Library(track) => track.path.clone(), + TrackLocation::Library(track) => track.path.to_string_lossy().into_owned(), TrackLocation::System(path) => { let format_string = gettext("Import from {}"); let file_name = path.file_name().unwrap().to_str().unwrap(); diff --git a/src/library.rs b/src/library.rs index b56e895..8d709a8 100644 --- a/src/library.rs +++ b/src/library.rs @@ -1632,9 +1632,7 @@ impl Library { let mut to_path = PathBuf::from(self.folder()); to_path.push(&filename); - let library_path = filename - .into_string() - .or(Err(anyhow!("Filename contains invalid Unicode.")))?; + let library_path = PathBuf::from(filename); fs::copy(path, to_path)?; @@ -1644,7 +1642,7 @@ impl Library { recording_index, medium_id: None, medium_index: None, - path: library_path, + path: library_path.into(), created_at: now, edited_at: now, last_used_at: now, @@ -1822,7 +1820,7 @@ fn write_zip( // Include all tracks that are part of the library. for (index, track) in tracks.into_iter().enumerate() { - add_file_to_zip(&mut zip, &library_folder, &track.path)?; + add_file_to_zip(&mut zip, &library_folder, &path_to_zip(&track.path)?)?; // Ignore if the reveiver has been dropped. let _ = sender.send_blocking(ProcessMsg::Progress((index + 1) as f64 / n_tracks as f64)); @@ -1833,7 +1831,6 @@ fn write_zip( Ok(()) } -// TODO: Cross-platform paths? fn add_file_to_zip( zip: &mut ZipWriter>, library_folder: impl AsRef, @@ -2064,9 +2061,8 @@ fn import_from_zip( let n_tracks = tracks.len(); - // TODO: Cross-platform paths? for (index, track) in tracks.into_iter().enumerate() { - let library_track_file_path = library_folder.as_ref().join(Path::new(&track.path)); + let library_track_file_path = library_folder.as_ref().join(&track.path); // Skip tracks that are already present. if !fs::exists(&library_track_file_path)? { @@ -2074,7 +2070,7 @@ fn import_from_zip( fs::create_dir_all(parent)?; } - let archive_track_file = archive.by_name(&track.path)?; + let archive_track_file = archive.by_name(&path_to_zip(&track.path)?)?; let library_track_file = File::create(library_track_file_path)?; std::io::copy( @@ -2160,3 +2156,23 @@ async fn download_tmp_file( Ok(file) } + +/// Convert a path to a ZIP path. ZIP files use "/" as the path separator +/// regardless of the current platform. +fn path_to_zip(path: impl AsRef) -> Result { + Ok(path + .as_ref() + .iter() + .map(|p| { + p.to_str() + .ok_or_else(|| { + anyhow!( + "Path \"{}\"contains invalid UTF-8", + path.as_ref().to_string_lossy() + ) + }) + .map(|s| s.to_owned()) + }) + .collect::>>()? + .join("/")) +} diff --git a/src/player.rs b/src/player.rs index 3396a19..8f75d9a 100644 --- a/src/player.rs +++ b/src/player.rs @@ -1,6 +1,6 @@ use std::{ cell::{Cell, OnceCell, RefCell}, - path::PathBuf, + path::{Path, PathBuf}, }; use anyhow::{anyhow, Context, Result}; @@ -473,7 +473,7 @@ impl Player { self.append(playlist) } - fn library_path_to_file_path(&self, path: &str) -> String { + fn library_path_to_file_path(&self, path: impl AsRef) -> String { PathBuf::from(self.library().unwrap().folder()) .join(path) .to_str()