Rename to Prospack

This commit is contained in:
unicxrn committed 2026-10-10 21:55:24 +02:00
1 parent a8287317ef
commit e3e80ac277
25 files changed
+210 -57

No files matched your search

+1 -1
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Packer</title>
<title>Prospack</title>
</head>
<body>
<div id="app"></div>
+3 -2
View File
@@ -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"
}
+1 -1
View File
@@ -2685,7 +2685,7 @@ dependencies = [
]
[[package]]
name = "ps5-packer"
name = "prospack"
version = "0.1.0"
dependencies = [
"base64 0.22.1",
+5 -3
View File
@@ -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]
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 41 KiB

+9
View File
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">
<defs>
<radialGradient id="g" cx="30%" cy="22%" r="85%"><stop offset="0" stop-color="#2a2340"/><stop offset="0.55" stop-color="#16161a"/><stop offset="1" stop-color="#0e0e10"/></radialGradient>
</defs>
<rect x="64" y="64" width="896" height="896" rx="208" fill="url(#g)"/>
<rect x="64.5" y="64.5" width="895" height="895" rx="207.5" fill="none" stroke="#ffffff" stroke-opacity="0.08" stroke-width="3"/>
<rect x="282" y="282" width="460" height="460" rx="104" fill="none" stroke="#ecebe7" stroke-width="52"/>
<path d="M400 512h224M512 400v224" stroke="#9b7bff" stroke-width="52" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 718 B

+1 -1
View File
@@ -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)?;
+4 -1
View File
@@ -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");
}
+1 -1
View File
@@ -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()
}
+43
View File
@@ -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<PathBuf> = OnceLock::new();
static DATA: OnceLock<PathBuf> = 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")
}
+8 -8
View File
@@ -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);
+4 -4
View File
@@ -160,7 +160,7 @@ pub fn write_hints(ftp: &mut Ftp, title_id: &str, backup_dir: &Path) -> Result<H
// Some servers refuse to rename over an existing file.
ftp.delete(&path).ok();
let (dir, name) = path.rsplit_once('/').unwrap();
ftp.rename(&format!("{dir}/.{name}.packer-tmp"), &path)?;
ftp.rename(&format!("{dir}/.{name}.prospack-tmp"), &path)?;
}
res.changed.push(format!("{file}: {line}"));
}
@@ -200,14 +200,14 @@ pub fn gc_record_json(r: &GcRecord, now: u64) -> 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,
});
+100 -15
View File
@@ -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<bool, String> {
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());
}
}
+18 -7
View File
@@ -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"
}
+1 -1
View File
@@ -171,7 +171,7 @@
{#if dragging && jobs.length}
<div class="dropveil" transition:fade={{ duration: 150 }}>
<div><b>Drop to add to the queue</b><span>Packer only reads the dump folder</span></div>
<div><b>Drop to add to the queue</b><span>Prospack only reads the dump folder</span></div>
</div>
{/if}
+3 -3
View File
@@ -14,11 +14,11 @@
<div class="kicker">One-time setup</div>
<h2>Install the compression engine</h2>
<p>
Packer uses <b>MkPFS 1.1.0</b>, the same format Game Compressor builds on. It goes into a private Python
Prospack uses <b>MkPFS 1.1.0</b>, the same format Game Compressor builds on. It goes into a private Python
environment in <span class="mono">{engine.dir}</span> and doesn't touch your system Python.
</p>
{#if !engine.python}
<p class="err">Python 3 wasn't found. Install it with your package manager, then restart Packer.</p>
<p class="err">Python 3 wasn't found. Install it with your package manager, then restart Prospack.</p>
{:else}
<button class="btn primary" disabled={installing} onclick={onInstall}>
{installing ? "Installing…" : "Install MkPFS"}
@@ -36,7 +36,7 @@
<path d="M20 18v9M15.5 22.5 20 18l4.5 4.5" stroke="var(--accent)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<h1>{dragging ? "Let go to add it" : "Drop a game dump here"}</h1>
<p>Or click to choose a folder, such as <span class="mono">PPSA01234-app</span>. Packer only reads it.</p>
<p>Or click to choose a folder, such as <span class="mono">PPSA01234-app</span>. Prospack only reads it.</p>
</button>
<ol class="steps reveal" style="animation-delay:.16s">
+1 -1
View File
@@ -93,7 +93,7 @@
{#if job.status === "queued"}
<div class="kicker">Queued</div>
<h2>{ahead ? `Waiting for ${ahead} ${ahead === 1 ? "game" : "games"} ahead` : "Starting…"}</h2>
<p class="desc">{n(job.dump.fileCount)} files, {gb(job.dump.totalBytes)}. Packer handles one game at a time.</p>
<p class="desc">{n(job.dump.fileCount)} files, {gb(job.dump.totalBytes)}. Prospack handles one game at a time.</p>
<div class="actions"><button class="btn ghost" onclick={onRemove}>Remove from queue</button></div>
{:else if job.status === "ready"}
<div class="kicker ok">Verified</div>
+3 -3
View File
@@ -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 @@
<legend class="kicker">Behaviour</legend>
<label class="toggle">
<input type="checkbox" bind:checked={draft.autoUpload} /><span class="sw"></span>
<span><b>Upload when verified</b><small>Otherwise Packer stops after Verify and waits for you.</small></span>
<span><b>Upload when verified</b><small>Otherwise Prospack stops after Verify and waits for you.</small></span>
</label>
<label class="toggle">
<input type="checkbox" bind:checked={draft.writeSmpHints} /><span class="sw"></span>
@@ -104,7 +104,7 @@
</label>
<label class="toggle">
<input type="checkbox" bind:checked={draft.markGcValidated} /><span class="sw"></span>
<span><b>Show as validated in Game Compressor</b><small>Writes Game Compressor's validation record after Packer's own file-by-file check, with the original size and space saved.</small></span>
<span><b>Show as validated in Game Compressor</b><small>Writes Game Compressor's validation record after Prospack's own file-by-file check, with the original size and space saved.</small></span>
</label>
<label class="toggle">
<input type="checkbox" bind:checked={draft.deleteLocalAfterUpload} /><span class="sw"></span>
+4 -5
View File
@@ -7,12 +7,11 @@
<header class="bar" data-tauri-drag-region>
<div class="mark" data-tauri-drag-region>
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" aria-hidden="true">
<rect x="1.5" y="1.5" width="15" height="15" rx="4" stroke="currentColor" stroke-width="1.4" />
<path d="M5.5 6.5h7" stroke="var(--accent)" stroke-width="1.4" stroke-linecap="round" />
<path d="M5.5 9.5h7M5.5 12.5h4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" />
<svg width="18" height="18" viewBox="0 0 28 28" fill="none" aria-hidden="true">
<rect x="5" y="5" width="18" height="18" rx="4" stroke="currentColor" stroke-width="2" />
<path d="M10 14h8M14 10v8" stroke="#9b7bff" stroke-width="2" stroke-linecap="round" />
</svg>
Packer
Prospack
</div>
<button class="conn" class:off={ps5 && !ps5.online} onclick={onSettings} title={ps5?.error ?? ""}>