diff --git a/index.html b/index.html index 04bd70d..dc3cb47 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,7 @@ - Packer + Prospack
diff --git a/package.json b/package.json index 21aa962..2d42f19 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "ps5-packer", + "name": "prospack", "private": true, "version": "0.1.0", "type": "module", @@ -21,5 +21,6 @@ "@tauri-apps/api": "^2.12.2", "@tauri-apps/plugin-dialog": "^2.8.1", "@tauri-apps/plugin-opener": "^2.7.0" - } + }, + "license": "GPL-3.0-only" } diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index e2ccca1..01eadfb 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2685,7 +2685,7 @@ dependencies = [ ] [[package]] -name = "ps5-packer" +name = "prospack" version = "0.1.0" dependencies = [ "base64 0.22.1", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index c8835e0..f596395 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,11 +1,13 @@ [package] -name = "ps5-packer" +name = "prospack" version = "0.1.0" -description = "Compress, verify and upload PS5 game dumps" +description = "Pack, verify and send PS5 dumps as .ffpfsc, from your PC" +license = "GPL-3.0-only" +repository = "https://github.com/unicxrn/prospack" edition = "2021" [lib] -name = "ps5_packer_lib" +name = "prospack_lib" crate-type = ["staticlib", "cdylib", "rlib"] [build-dependencies] diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png index a6846ca..ff73400 100644 Binary files a/src-tauri/icons/128x128.png and b/src-tauri/icons/128x128.png differ diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png index bf0011a..757dc72 100644 Binary files a/src-tauri/icons/128x128@2x.png and b/src-tauri/icons/128x128@2x.png differ diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png index fcbf043..c6439f1 100644 Binary files a/src-tauri/icons/32x32.png and b/src-tauri/icons/32x32.png differ diff --git a/src-tauri/icons/64x64.png b/src-tauri/icons/64x64.png index 0fcb065..28d403a 100644 Binary files a/src-tauri/icons/64x64.png and b/src-tauri/icons/64x64.png differ diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns index 0bbe880..ff84722 100644 Binary files a/src-tauri/icons/icon.icns and b/src-tauri/icons/icon.icns differ diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico index e02d9aa..0aae53a 100644 Binary files a/src-tauri/icons/icon.ico and b/src-tauri/icons/icon.ico differ diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png index 0b39f24..0716c9a 100644 Binary files a/src-tauri/icons/icon.png and b/src-tauri/icons/icon.png differ diff --git a/src-tauri/icons/prospack.svg b/src-tauri/icons/prospack.svg new file mode 100644 index 0000000..c5e8bd2 --- /dev/null +++ b/src-tauri/icons/prospack.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src-tauri/src/ftp.rs b/src-tauri/src/ftp.rs index 503b552..e06a902 100644 --- a/src-tauri/src/ftp.rs +++ b/src-tauri/src/ftp.rs @@ -264,7 +264,7 @@ impl Ftp { /// Writes a small file atomically: upload to a hidden temp name, then rename over. pub fn put_atomic(&mut self, path: &str, bytes: &[u8]) -> Result<(), String> { let (dir, name) = path.rsplit_once('/').ok_or("bad path")?; - let tmp = format!("{dir}/.{name}.packer-tmp"); + let tmp = format!("{dir}/.{name}.prospack-tmp"); // ftpsrv's STOR doesn't truncate before writing, so never write over a leftover temp file. if self.size(&tmp)?.is_some() { self.delete(&tmp)?; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 7a5947f..c3cf6a6 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,6 +1,7 @@ mod dump; mod engine; mod ftp; +mod paths; mod pipeline; mod ps5; mod settings; @@ -112,6 +113,8 @@ pub fn run() { .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_opener::init()) .setup(|app| { + paths::init(app.path().app_config_dir()?, app.path().app_data_dir()?); + settings::migrate_legacy(); let handle = app.handle().clone(); let emitter: Emitter = Arc::new(move |event, payload| { let _ = handle.emit(event, payload); @@ -146,5 +149,5 @@ pub fn run() { ps5_status ]) .run(tauri::generate_context!()) - .expect("error while running Packer"); + .expect("error while running Prospack"); } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index c927564..230bf51 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -5,5 +5,5 @@ fn main() { if std::env::var_os("WEBKIT_DISABLE_DMABUF_RENDERER").is_none() { std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); } - ps5_packer_lib::run() + prospack_lib::run() } diff --git a/src-tauri/src/paths.rs b/src-tauri/src/paths.rs new file mode 100644 index 0000000..46e4a26 --- /dev/null +++ b/src-tauri/src/paths.rs @@ -0,0 +1,43 @@ +//! Where Prospack keeps its config and data. Tauri supplies the platform dirs at startup; +//! tests and CLI runs fall back to $PROSPACK_CONFIG_DIR / $PROSPACK_DATA_DIR. + +use std::path::PathBuf; +use std::sync::OnceLock; + +static CONFIG: OnceLock = OnceLock::new(); +static DATA: OnceLock = OnceLock::new(); + +pub fn init(config: PathBuf, data: PathBuf) { + let _ = CONFIG.set(config); + let _ = DATA.set(data); +} + +pub fn home() -> PathBuf { + std::env::var_os("HOME") + .or_else(|| std::env::var_os("USERPROFILE")) + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from(".")) +} + +pub fn config_dir() -> PathBuf { + std::env::var_os("PROSPACK_CONFIG_DIR") + .map(PathBuf::from) + .or_else(|| CONFIG.get().cloned()) + .unwrap_or_else(|| home().join(".config/prospack")) +} + +pub fn data_dir() -> PathBuf { + std::env::var_os("PROSPACK_DATA_DIR") + .map(PathBuf::from) + .or_else(|| DATA.get().cloned()) + .unwrap_or_else(|| home().join(".local/share/prospack")) +} + +/// Where the app lived while it was called Packer (Linux only). +pub fn legacy_config_dir() -> PathBuf { + home().join(".config/ps5-packer") +} + +pub fn legacy_data_dir() -> PathBuf { + home().join(".local/share/ps5-packer") +} diff --git a/src-tauri/src/pipeline.rs b/src-tauri/src/pipeline.rs index 2ed975b..0664720 100644 --- a/src-tauri/src/pipeline.rs +++ b/src-tauri/src/pipeline.rs @@ -190,7 +190,7 @@ impl Queue { engine, }; let worker = q.clone(); - std::thread::Builder::new().name("packer-worker".into()).spawn(move || worker.run()).unwrap(); + std::thread::Builder::new().name("prospack-worker".into()).spawn(move || worker.run()).unwrap(); q } @@ -595,7 +595,7 @@ impl Queue { let local_size = std::fs::metadata(&image).map_err(|e| format!("Image missing: {e}"))?.len(); let dir = s.remote_dir.trim_end_matches('/').to_string(); let final_remote = format!("{dir}/{title}.ffpfsc"); - let tmp_remote = format!("{dir}/.{title}.ffpfsc.packer-part"); + let tmp_remote = format!("{dir}/.{title}.ffpfsc.prospack-part"); let mut ftp = ps5::connect(s)?; let others = ps5::competitors(&mut ftp, &dir, &title).unwrap_or_default(); @@ -619,7 +619,7 @@ impl Queue { return Ok(()); } return Err(format!( - "The PS5 already has a different {title}.ffpfsc ({}). Delete or rename it first; Packer never overwrites.", + "The PS5 already has a different {title}.ffpfsc ({}). Delete or rename it first; Prospack never overwrites.", gb(existing) )); } @@ -895,9 +895,9 @@ mod e2e { fn e2e_local_fake_ps5() { let scratch = PathBuf::from(std::env::var("PACKER_E2E_SCRATCH").expect("PACKER_E2E_SCRATCH")); let tmp = tempfile::tempdir().unwrap(); - std::env::set_var("XDG_CONFIG_HOME", tmp.path().join("config")); - std::env::set_var("XDG_DATA_HOME", tmp.path().join("data")); - let engine_dir = tmp.path().join("data/ps5-packer/engine"); + std::env::set_var("PROSPACK_CONFIG_DIR", tmp.path().join("config")); + std::env::set_var("PROSPACK_DATA_DIR", tmp.path().join("data")); + let engine_dir = tmp.path().join("data/engine"); std::fs::create_dir_all(&engine_dir).unwrap(); std::os::unix::fs::symlink(scratch.join("mkvenv"), engine_dir.join("venv")).unwrap(); @@ -950,7 +950,7 @@ mod e2e { let local = std::fs::metadata(j.image_path.as_ref().unwrap()).unwrap().len(); let remote = ps5_root.join("data/homebrew/PPSA00001.ffpfsc"); assert_eq!(std::fs::metadata(&remote).unwrap().len(), local); - assert!(!ps5_root.join("data/homebrew/.PPSA00001.ffpfsc.packer-part").exists()); + assert!(!ps5_root.join("data/homebrew/.PPSA00001.ffpfsc.prospack-part").exists()); let cfg = std::fs::read_to_string(ps5_root.join("data/shadowmount/config.ini")).unwrap(); assert!(cfg.contains("image_ro=OTHER.exfat") && cfg.contains("image_ro=PPSA00001.exfat"), "{cfg}"); let at = std::fs::read_to_string(ps5_root.join("data/shadowmount/autotune.ini")).unwrap(); @@ -1003,7 +1003,7 @@ mod e2e { q.remove(id3).unwrap(); let good = std::fs::read(j4.image_path.as_ref().unwrap()).unwrap(); std::fs::remove_file(&remote).unwrap(); - let part = ps5_root.join("data/homebrew/.PPSA00001.ffpfsc.packer-part"); + let part = ps5_root.join("data/homebrew/.PPSA00001.ffpfsc.prospack-part"); std::fs::write(&part, &good[..good.len() / 2]).unwrap(); let id5 = q.add(crate::dump::inspect(&dump).unwrap()).unwrap(); let j5 = wait_until(&q, id5, 300); diff --git a/src-tauri/src/ps5.rs b/src-tauri/src/ps5.rs index 371ef40..fa6bbfe 100644 --- a/src-tauri/src/ps5.rs +++ b/src-tauri/src/ps5.rs @@ -160,7 +160,7 @@ pub fn write_hints(ftp: &mut Ftp, title_id: &str, backup_dir: &Path) -> Result String { "aprIndexed": r.apr_indexed, "amprHotSwapOptimized": false, // Any status other than bad-blocks-found*, compression-stats or not-mounted reads as validated. - "status": "verified-by-packer", + "status": "verified-by-prospack", "nestedName": format!("{}.exfat", r.title_id), "repairedBlocks": 0, - "hashMode": "packer-file-compare", + "hashMode": "prospack-file-compare", "hashCheckedBlocks": 0, "hashMatchedBlocks": 0, "hashMismatchedBlocks": 0, - "verifiedBy": "Packer", + "verifiedBy": "Prospack", "verifiedFiles": r.files_verified, "updatedAt": now, }); diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index 8d8dccb..dfa9fa7 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -1,5 +1,6 @@ +use crate::paths; use serde::{Deserialize, Serialize}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(default, rename_all = "camelCase")] @@ -24,12 +25,14 @@ pub struct Settings { pub mark_gc_validated: bool, /// Delete the local image once it's confirmed on the PS5. pub delete_local_after_upload: bool, + /// The first-run wizard has been completed (or skipped). + pub setup_done: bool, } impl Default for Settings { fn default() -> Self { Self { - ps5_host: "192.168.0.50".into(), + ps5_host: String::new(), ftp_port: 2121, web_port: 8888, remote_dir: "/data/homebrew".into(), @@ -41,30 +44,21 @@ impl Default for Settings { write_smp_hints: true, mark_gc_validated: true, delete_local_after_upload: false, + setup_done: false, } } } -fn home() -> PathBuf { - std::env::var_os("HOME").map(PathBuf::from).unwrap_or_else(|| PathBuf::from("/tmp")) -} - fn default_output_dir() -> PathBuf { - home().join("PS5 Images") + paths::home().join("Prospack Images") } pub fn config_dir() -> PathBuf { - std::env::var_os("XDG_CONFIG_HOME") - .map(PathBuf::from) - .unwrap_or_else(|| home().join(".config")) - .join("ps5-packer") + paths::config_dir() } pub fn data_dir() -> PathBuf { - std::env::var_os("XDG_DATA_HOME") - .map(PathBuf::from) - .unwrap_or_else(|| home().join(".local/share")) - .join("ps5-packer") + paths::data_dir() } fn settings_path() -> PathBuf { @@ -85,3 +79,94 @@ pub fn save(s: &Settings) -> Result<(), String> { std::fs::write(&tmp, serde_json::to_vec_pretty(s).unwrap()).map_err(|e| e.to_string())?; std::fs::rename(&tmp, &path).map_err(|e| e.to_string()) } + +/// Carries settings and backups over from the app's earlier name ("Packer", ps5-packer dirs). +/// Returns true when settings were written. Never touches the old folders. +pub fn migrate_from(old_cfg: &Path, old_data: &Path, new_cfg: &Path, new_data: &Path) -> Result { + let new_settings = new_cfg.join("settings.json"); + let mut wrote = false; + if !new_settings.exists() { + let old_settings = old_cfg.join("settings.json"); + let migrated = if let Ok(raw) = std::fs::read_to_string(&old_settings) { + // They configured the old app on purpose, so the wizard isn't needed. + let mut s: Settings = serde_json::from_str(&raw).map_err(|e| e.to_string())?; + s.setup_done = true; + Some(s) + } else if old_data.is_dir() { + // Used the old app with its built-in defaults: keep those, but show the wizard. + let old_images = paths::home().join("PS5 Images"); + Some(Settings { + ps5_host: "192.168.0.50".into(), + output_dir: if old_images.is_dir() { + old_images.to_string_lossy().into_owned() + } else { + default_output_dir().to_string_lossy().into_owned() + }, + ..Settings::default() + }) + } else { + None + }; + if let Some(s) = migrated { + std::fs::create_dir_all(new_cfg).map_err(|e| e.to_string())?; + std::fs::write(&new_settings, serde_json::to_vec_pretty(&s).unwrap()).map_err(|e| e.to_string())?; + wrote = true; + } + } + let (old_b, new_b) = (old_data.join("backups"), new_data.join("backups")); + if old_b.is_dir() && !new_b.exists() { + std::fs::create_dir_all(&new_b).map_err(|e| e.to_string())?; + for e in std::fs::read_dir(&old_b).map_err(|e| e.to_string())?.flatten() { + let _ = std::fs::copy(e.path(), new_b.join(e.file_name())); + } + } + Ok(wrote) +} + +pub fn migrate_legacy() { + if let Err(e) = migrate_from(&paths::legacy_config_dir(), &paths::legacy_data_dir(), &config_dir(), &data_dir()) { + eprintln!("settings migration failed: {e}"); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn migrates_old_ps5_packer_settings_once() { + let t = tempfile::tempdir().unwrap(); + let (old_cfg, old_data) = (t.path().join("old/cfg"), t.path().join("old/data")); + std::fs::create_dir_all(old_data.join("backups")).unwrap(); + std::fs::write(old_data.join("backups/1-config.ini"), "x").unwrap(); + std::fs::create_dir_all(&old_cfg).unwrap(); + std::fs::write(old_cfg.join("settings.json"), r#"{"ps5Host":"10.0.0.5","ftpPort":2121}"#).unwrap(); + let (new_cfg, new_data) = (t.path().join("new/cfg"), t.path().join("new/data")); + assert!(migrate_from(&old_cfg, &old_data, &new_cfg, &new_data).unwrap()); + let s: Settings = serde_json::from_str(&std::fs::read_to_string(new_cfg.join("settings.json")).unwrap()).unwrap(); + assert_eq!(s.ps5_host, "10.0.0.5"); + assert!(s.setup_done, "people who configured Packer skip the wizard"); + assert!(new_data.join("backups/1-config.ini").is_file()); + assert!(!migrate_from(&old_cfg, &old_data, &new_cfg, &new_data).unwrap(), "second run is a no-op"); + } + + #[test] + fn old_defaults_user_gets_old_host_and_the_wizard() { + let t = tempfile::tempdir().unwrap(); + let old_data = t.path().join("old/data"); + std::fs::create_dir_all(&old_data).unwrap(); + let new_cfg = t.path().join("new/cfg"); + assert!(migrate_from(&t.path().join("old/cfg"), &old_data, &new_cfg, &t.path().join("new/data")).unwrap()); + let s: Settings = serde_json::from_str(&std::fs::read_to_string(new_cfg.join("settings.json")).unwrap()).unwrap(); + assert_eq!(s.ps5_host, "192.168.0.50"); + assert!(!s.setup_done); + } + + #[test] + fn fresh_install_writes_nothing() { + let t = tempfile::tempdir().unwrap(); + let new_cfg = t.path().join("new/cfg"); + assert!(!migrate_from(&t.path().join("a"), &t.path().join("b"), &new_cfg, &t.path().join("c")).unwrap()); + assert!(!new_cfg.join("settings.json").exists()); + } +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index d22c7ce..1a2ba51 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,8 +1,8 @@ { "$schema": "https://schema.tauri.app/config/2", - "productName": "Packer", + "productName": "Prospack", "version": "0.1.0", - "identifier": "dev.unicxrn.ps5packer", + "identifier": "io.github.unicxrn.prospack", "build": { "beforeDevCommand": "npm run dev", "beforeBuildCommand": "npm run build", @@ -13,7 +13,7 @@ "windows": [ { "label": "main", - "title": "Packer", + "title": "Prospack", "width": 1320, "height": 860, "minWidth": 760, @@ -24,12 +24,23 @@ "dragDropEnabled": true } ], - "security": { "csp": null } + "security": { + "csp": null + } }, "bundle": { "active": true, - "targets": ["deb", "appimage"], - "icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.png"], + "targets": [ + "deb", + "appimage" + ], + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.png" + ], "resources": [] - } + }, + "mainBinaryName": "prospack" } diff --git a/src/App.svelte b/src/App.svelte index 46a7ca6..1457669 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -171,7 +171,7 @@ {#if dragging && jobs.length}
-
Drop to add to the queuePacker only reads the dump folder
+
Drop to add to the queueProspack only reads the dump folder
{/if} diff --git a/src/lib/EmptyState.svelte b/src/lib/EmptyState.svelte index 3bcac17..55cb265 100644 --- a/src/lib/EmptyState.svelte +++ b/src/lib/EmptyState.svelte @@ -14,11 +14,11 @@
One-time setup

Install the compression engine

- Packer uses MkPFS 1.1.0, the same format Game Compressor builds on. It goes into a private Python + Prospack uses MkPFS 1.1.0, the same format Game Compressor builds on. It goes into a private Python environment in {engine.dir} and doesn't touch your system Python.

{#if !engine.python} -

Python 3 wasn't found. Install it with your package manager, then restart Packer.

+

Python 3 wasn't found. Install it with your package manager, then restart Prospack.

{:else}
    diff --git a/src/lib/Focus.svelte b/src/lib/Focus.svelte index f86059d..36ed023 100644 --- a/src/lib/Focus.svelte +++ b/src/lib/Focus.svelte @@ -93,7 +93,7 @@ {#if job.status === "queued"}
    Queued

    {ahead ? `Waiting for ${ahead} ${ahead === 1 ? "game" : "games"} ahead` : "Starting…"}

    -

    {n(job.dump.fileCount)} files, {gb(job.dump.totalBytes)}. Packer handles one game at a time.

    +

    {n(job.dump.fileCount)} files, {gb(job.dump.totalBytes)}. Prospack handles one game at a time.

    {:else if job.status === "ready"}
    Verified
    diff --git a/src/lib/Settings.svelte b/src/lib/Settings.svelte index 7437841..f451d91 100644 --- a/src/lib/Settings.svelte +++ b/src/lib/Settings.svelte @@ -21,7 +21,7 @@ } async function browse() { - const p = await api.pickFolder("Where should Packer write images?"); + const p = await api.pickFolder("Where should Prospack write images?"); if (p) draft.outputDir = p; } @@ -96,7 +96,7 @@ Behaviour