* feat(pkg): choose which drive install packages are kept on Uploaded packages always went to internal storage (/user/data/ps5upload/pkg_library), which fills the console's own drive. The Volumes screen now lets you mark a drive "Use for packages"; new packages are staged under <drive>/ps5upload/pkg_library on it. - Per console, remembered on this device. Internal stays the default. - Falls back to internal storage, with a notice, when the chosen drive isn't connected. - The package library lists packages on every drive; rows show the drive. - The folder chain on the drive is created one level at a time. - The External Packages scan skips a drive's ps5upload/ folder, so staged packages aren't listed twice. Hardware (Phat, FW 5.10): Star Wars Racer Revenge base+patch staged on /mnt/ext0 installed, patched 01.00->01.02 and launched in 2 of 3 trials (80 MB/s); the first stalled after the header read and is covered by the daemon-recycle fix that follows. Internal control: 1/1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(install): recycle the installer daemon after a stalled delivery A console-path install that stalls leaves the daemon's loopback job "serving" until Sony reads every byte, which never happens, so every later install was refused as "an install is already in progress" until the daemon restarted, although the failure told the user to retry. Measured on the Phat (FW 5.10). A stalled daemon-served install now stops the daemon the same way a wedged stream refusal already did, and the next install's ensure brings up a fresh one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * PS5Upload v5.36.0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Engine
The Rust workspace: everything between the desktop app and the PS5.
The engine is a local HTTP service. The Tauri app doesn't talk to the
console directly — it calls the engine, which speaks FTX2 to the payload.
That split is deliberate: the same API is reachable from a browser, a
script, or CI, so nothing the app can do is locked inside the app. See
the self-hosted-engine entries in ../FAQ.md.
client (Tauri/React) ──HTTP──▶ ps5upload-engine :19113
│ FTX2 binary framing
▼
payload :9113 transfer
:9114 management
Crates
| Crate | What it is |
|---|---|
ftx2-proto |
Wire format: frame type IDs, header layout, constants. Shared by everything that speaks FTX2. |
ps5upload-core |
The bulk of the logic — connection handling and socket tuning, transfer/resume/verification, filesystem and app RPCs, volume parsing, package install, saves, cheats, hardware, SMB, BPS patching. |
ps5upload-engine |
The Axum HTTP service (~100 routes), job tracking, SSE progress events, and the desktop + mobile entry points. |
ps5upload-pkg |
.pkg parsing — headers, entries, split-file sets. |
ps5upload-lab |
CLI for driving the payload's control channel by hand. Useful when you want one frame, not a workflow. |
ps5upload-tests |
Integration tests against an in-process mock FTX2 server. |
ps5upload-bench |
Throughput benchmarks. |
Working on it
cargo test --workspace # no PS5 required — mock server
cargo build --release -p ps5upload-engine
Run it standalone and point it at a console:
PS5_ADDR=<ip>:9113 cargo run -p ps5upload-engine
curl "http://127.0.0.1:19113/api/ps5/status?addr=<ip>:9114"
PS5UPLOAD_ALLOW_IP lets another machine reach it. The API is
unauthenticated and can read, write and delete files on the console —
keep it on a trusted LAN.
Testing
engine/crates/ps5upload-tests/tests/ runs against a loopback mock
server: single-file, streaming, directory, packed small-file shards,
resume-after-drop, retry classification, digest mismatch, exclude rules,
.zip/.7z streaming, hardware commands, and volume parsing. Unit tests
live beside their modules.
Anything touching real transfer behaviour still needs hardware — see
../TESTING.md.
Things that will bite you
- The payload emits JSON; serde parses it. Field names must be
snake_caseor serde silently leaves the field at its default — a zero or an empty string, not an error. One malformed byte rejects the whole response, so anything interpolated into a payload-side JSON string has to be escaped first. ok: falseinside a 200 is a refusal, not a success. Several payload commands answer that way.client/src/api/ps5.tshas anassertOk()for action endpoints; status endpoints keepok:falseas data because there it means "unsupported on this console".- Two ports, two roles. Transfer frames go to 9113, everything else to
9114. The wrong port returns
wrong_portrather than working oddly.