mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 06:25:41 +00:00
refactor: extract psysonic-syncfs crate (M4/7)
Moves all on-disk cache + device-sync code out of the top crate:
crates/psysonic-syncfs/ new lib crate
src/cache/{offline,hot,downloads,fs_utils} unchanged behaviour
src/sync/{batch,device} (no tray.rs — that's UI)
src/file_transfer.rs shared HTTP helpers
src/lib.rs + DownloadSemaphore type
+ sync_cancel_flags fn
The shell crate keeps `lib_commands/sync/tray.rs` (OS tray icon — UI
concern, will move to shell-tauri at M7) but drops the rest of
`lib_commands/cache/` and the syncfs sibling files.
Tauri command quirk surfaced and resolved: `#[tauri::command]` puts its
`__cmd__*` and `__tauri_command_name_*` helper macros at the *exact*
module of the function, and `pub use` doesn't carry them across module
boundaries. invoke_handler! in lib.rs now references each syncfs
command via its full deepest path
(`psysonic_syncfs::cache::offline::download_track_offline`, etc.) so
Tauri's macros resolve at the right scope. Same approach the audio
crate already uses (`audio::commands::audio_play`).
Cross-crate ref migrations applied via batch sed:
crate::audio::* → psysonic_audio::*
crate::analysis_runtime::* → psysonic_analysis::analysis_runtime::*
crate::analysis_cache::* → psysonic_analysis::analysis_cache::*
crate::subsonic_wire_user_agent → psysonic_core::user_agent::*
super::super::file_transfer:: → crate::file_transfer::
Behaviour preserving. Cargo check + clippy --workspace clean.
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Build a reqwest client with the standard Subsonic UA and a single overall timeout.
|
||||
/// For flows that need separate connect + read timeouts (long-running update/zip
|
||||
/// downloads with progress events), build the client inline.
|
||||
pub fn subsonic_http_client(timeout: Duration) -> Result<reqwest::Client, String> {
|
||||
reqwest::Client::builder()
|
||||
.user_agent(psysonic_core::user_agent::subsonic_wire_user_agent())
|
||||
.timeout(timeout)
|
||||
.build()
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Streams an HTTP response body to `dest_path` in chunks. Never buffers the full
|
||||
/// file in memory — keeps RAM flat regardless of file size.
|
||||
pub async fn stream_to_file(
|
||||
response: reqwest::Response,
|
||||
dest_path: &Path,
|
||||
) -> Result<(), String> {
|
||||
use futures_util::StreamExt;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
let mut file = tokio::fs::File::create(dest_path)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let mut stream = response.bytes_stream();
|
||||
while let Some(chunk) = stream.next().await {
|
||||
let chunk = chunk.map_err(|e| e.to_string())?;
|
||||
file.write_all(&chunk).await.map_err(|e| e.to_string())?;
|
||||
}
|
||||
file.flush().await.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Streams `response` to `part_path`, then renames `part_path` → `dest_path`.
|
||||
/// On any failure the partial `.part` file is best-effort removed so it does
|
||||
/// not linger on disk. Caller must ensure `dest_path.parent()` exists.
|
||||
///
|
||||
/// Note vs. previous inline implementations: the offline/device single-track
|
||||
/// flows used to leave a `.part` orphan if the final rename failed. This helper
|
||||
/// always cleans up, matching the batch-sync flow that already did.
|
||||
pub async fn finalize_streamed_download(
|
||||
response: reqwest::Response,
|
||||
dest_path: &Path,
|
||||
part_path: &Path,
|
||||
) -> Result<(), String> {
|
||||
if let Err(e) = stream_to_file(response, part_path).await {
|
||||
let _ = tokio::fs::remove_file(part_path).await;
|
||||
return Err(e);
|
||||
}
|
||||
if let Err(e) = tokio::fs::rename(part_path, dest_path).await {
|
||||
let _ = tokio::fs::remove_file(part_path).await;
|
||||
return Err(e.to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user