diff --git a/CHANGELOG.md b/CHANGELOG.md index 6754d8ca..34a3297f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ What's new in ps5upload, written for humans. --- +## 3.3.19 + +- **"Installed" no longer leaks between consoles.** If you staged the same .pkg + on more than one PS5 and installed it on just one, the others wrongly showed + it as already installed (Reinstall). Each console now tracks its own installs, + so a package reads as installed only on the console you actually installed it + on. + ## 3.3.18 - **Cancel a single upload without stopping the whole queue.** The item that's diff --git a/VERSION b/VERSION index 42c4a762..d62498e6 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.3.18 +3.3.19 diff --git a/client/package-lock.json b/client/package-lock.json index a1f7c286..ca125cd8 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "ps5upload-client", - "version": "3.3.18", + "version": "3.3.19", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ps5upload-client", - "version": "3.3.18", + "version": "3.3.19", "dependencies": { "@tauri-apps/api": "^2.11.0", "@tauri-apps/plugin-dialog": "^2.7.1", diff --git a/client/package.json b/client/package.json index e3d25295..2c4d7db7 100644 --- a/client/package.json +++ b/client/package.json @@ -1,7 +1,7 @@ { "name": "ps5upload-client", "private": true, - "version": "3.3.18", + "version": "3.3.19", "description": "The all-in-one PS5 companion app.", "homepage": "https://github.com/phantomptr/ps5upload", "author": "PhantomPtr ", diff --git a/client/src-tauri/Cargo.lock b/client/src-tauri/Cargo.lock index 8cb4107c..f49f6a63 100644 --- a/client/src-tauri/Cargo.lock +++ b/client/src-tauri/Cargo.lock @@ -1237,7 +1237,7 @@ dependencies = [ [[package]] name = "ftx2-proto" -version = "3.3.18" +version = "3.3.19" dependencies = [ "serde", "thiserror 2.0.18", @@ -3319,7 +3319,7 @@ dependencies = [ [[package]] name = "ps5upload-core" -version = "3.3.18" +version = "3.3.19" dependencies = [ "anyhow", "base64 0.22.1", @@ -3336,7 +3336,7 @@ dependencies = [ [[package]] name = "ps5upload-desktop" -version = "3.3.18" +version = "3.3.19" dependencies = [ "anyhow", "base64 0.22.1", @@ -3369,7 +3369,7 @@ dependencies = [ [[package]] name = "ps5upload-engine" -version = "3.3.18" +version = "3.3.19" dependencies = [ "anyhow", "axum", @@ -3387,7 +3387,7 @@ dependencies = [ [[package]] name = "ps5upload-pkg" -version = "3.3.18" +version = "3.3.19" dependencies = [ "serde", "thiserror 2.0.18", diff --git a/client/src-tauri/Cargo.toml b/client/src-tauri/Cargo.toml index deaeab5a..bb66fda9 100644 --- a/client/src-tauri/Cargo.toml +++ b/client/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ps5upload-desktop" -version = "3.3.18" +version = "3.3.19" description = "The all-in-one PS5 companion app." edition = "2021" rust-version = "1.77" diff --git a/client/src-tauri/tauri.conf.json b/client/src-tauri/tauri.conf.json index 5c6624a1..933bbe01 100644 --- a/client/src-tauri/tauri.conf.json +++ b/client/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "PS5Upload", - "version": "3.3.18", + "version": "3.3.19", "identifier": "com.phantomptr.ps5upload", "build": { "beforeDevCommand": "npm run dev:vite", diff --git a/client/src/state/pkgLibrary.test.ts b/client/src/state/pkgLibrary.test.ts index a45f50dc..84ab5fca 100644 --- a/client/src/state/pkgLibrary.test.ts +++ b/client/src/state/pkgLibrary.test.ts @@ -38,6 +38,8 @@ import { installSpaceWarning, installedLastResult, runPkgInstall, + recordPkgInstalled, + isPkgInstalledHere, PKG_MAY_NOT_LAUNCH_MESSAGE, PKG_PATCH_REJECTED_HINT, type PkgEntry, @@ -230,6 +232,50 @@ describe("pkgRowInstalled (installed/Reinstall badge)", () => { }); }); +describe("recordPkgInstalled / isPkgInstalledHere (per-console isolation)", () => { + // The reported bug: the same .pkg staged on multiple consoles lands at an + // identical path, and installing on ONE console used to mark it installed on + // ALL of them (the flag was keyed on path only). It must be scoped per host. + beforeEach(() => { + const store = new globalThis.Map(); + (globalThis as { window?: unknown }).window = { + localStorage: { + getItem: (k: string) => (store.has(k) ? store.get(k)! : null), + setItem: (k: string, v: string) => void store.set(k, String(v)), + removeItem: (k: string) => void store.delete(k), + clear: () => store.clear(), + }, + }; + }); + afterEach(() => { + delete (globalThis as { window?: unknown }).window; + }); + + const PATH = "/data/ps5upload/pkg_temp/Game[v01.04].pkg"; + const A = "192.168.1.10"; + const B = "192.168.1.20"; + + it("installing on one console does NOT mark it installed on another", () => { + recordPkgInstalled(A, PATH); + expect(isPkgInstalledHere(A, PATH)).toBe(true); + // The sibling console with the SAME staged path must read as not-installed. + expect(isPkgInstalledHere(B, PATH)).toBe(false); + }); + + it("normalizes host:port to the bare host (addr form is accepted)", () => { + recordPkgInstalled("192.168.1.10:9113", PATH); + expect(isPkgInstalledHere("192.168.1.10", PATH)).toBe(true); + expect(isPkgInstalledHere("192.168.1.10:1234", PATH)).toBe(true); + }); + + it("tracks distinct paths per console independently", () => { + const other = "/data/ps5upload/pkg_temp/Other.pkg"; + recordPkgInstalled(A, PATH); + expect(isPkgInstalledHere(A, PATH)).toBe(true); + expect(isPkgInstalledHere(A, other)).toBe(false); + }); +}); + describe("pkgInstallMayNotLaunch", () => { it("trusts the engine's explicit may_not_launch flag", () => { expect(pkgInstallMayNotLaunch({ may_not_launch: true })).toBe(true); diff --git a/client/src/state/pkgLibrary.ts b/client/src/state/pkgLibrary.ts index 17ce907f..f78bebc3 100644 --- a/client/src/state/pkgLibrary.ts +++ b/client/src/state/pkgLibrary.ts @@ -295,11 +295,6 @@ interface PkgPathMeta { /** PARAM.SFO `CATEGORY` (`gd`/`gp`/`ac`) — authoritative, vs. the directory * inference. Populated when we read the staged pkg off the console. */ category?: string; - /** True once we've installed THIS exact staged package via the app. Lets an - * update/DLC row show "Reinstall" after it's installed — the console's - * app_list only tracks the base title id, so it can't tell us a specific - * update/DLC was applied; this per-package record can. Survives restarts. */ - installed?: boolean; } function loadPathMetaCache(): Record { @@ -325,14 +320,49 @@ function cachePathMeta(path: string, meta: PkgPathMeta): void { } } -/** Persistently mark the staged package at `path` as having been installed via - * the app, so its library row reads "Reinstall" instead of "Install". This is - * the only reliable per-package signal for an update/DLC — the console's - * app_list is keyed on the base title id and can't confirm a specific add-on. - * Called from both install paths (the library tab and the upload-queue - * finisher). Best-effort and survives restarts. */ -export function recordPkgInstalled(path: string): void { - cachePathMeta(path, { installed: true }); +// Per-CONSOLE record of which staged paths we've installed. Keyed by bare host +// (port-stripped) → the set of installed paths on THAT console. This must NOT +// live in the path-only PATH_META cache: staged packages land at identical +// paths on every console (e.g. the shared staging dir), so a path-keyed flag +// would make installing on one console light up "Reinstall" on all the others +// that happen to have the same file. The console's own app_list can't +// distinguish a specific update/DLC (it only tracks the base title id), so this +// is the authoritative per-console signal. Survives restarts. +const INSTALLED_CACHE_KEY = "ps5upload.pkg_library.installed.v1"; + +function loadInstalledCache(): Record { + if (typeof window === "undefined") return {}; + try { + const raw = window.localStorage.getItem(INSTALLED_CACHE_KEY); + const parsed = raw ? JSON.parse(raw) : {}; + return parsed && typeof parsed === "object" ? parsed : {}; + } catch { + return {}; + } +} + +/** Persistently mark the staged package at `path` as installed ON THIS CONSOLE, + * so its library row reads "Reinstall" instead of "Install" — scoped to `host` + * so the same file staged on a sibling console is unaffected. Called from both + * install paths (the library tab and the upload-queue finisher). */ +export function recordPkgInstalled(host: string, path: string): void { + if (!path || typeof window === "undefined") return; + try { + const h = hostOf(host); + const c = loadInstalledCache(); + const set = new Set(c[h] ?? []); + set.add(path); + c[h] = [...set]; + window.localStorage.setItem(INSTALLED_CACHE_KEY, JSON.stringify(c)); + } catch { + /* best-effort */ + } +} + +/** Whether `path` was installed on `host` via the app (see recordPkgInstalled). */ +export function isPkgInstalledHere(host: string, path: string): boolean { + if (!path || typeof window === "undefined") return false; + return (loadInstalledCache()[hostOf(host)] ?? []).includes(path); } /** The trailing path component of a local file path (handles both `/` and @@ -940,7 +970,7 @@ const makePkgLibraryStore = () => title: titles[contentId], originalName: pathMeta[path]?.name, appVer: pathMeta[path]?.appVer, - installedHere: pathMeta[path]?.installed, + installedHere: isPkgInstalledHere(host, path), titleId: titleIdFromContentId(contentId) ?? undefined, // Authoritative category (read off the console) when we have it, // else the directory inference (updates/ → gp, dlc/ → ac). @@ -1335,10 +1365,12 @@ const makePkgLibraryStore = () => .finish(actId, installed ? "done" : stalled ? "stopped" : "failed"); if (installed) { - // Record THIS package as installed (per-path, persisted) and reflect it - // on the row, so an update/DLC that's been installed shows "Reinstall" - // — not "Install" — even though app_list can't confirm an add-on. - cachePathMeta(path, { installed: true }); + // Record THIS package as installed ON THIS CONSOLE (persisted) and + // reflect it on the row, so an update/DLC that's been installed shows + // "Reinstall" — not "Install" — even though app_list can't confirm an + // add-on. Scoped to `host` so a sibling console with the same staged + // file isn't wrongly marked installed. + recordPkgInstalled(host, path); patch({ status: "idle", installedHere: true, diff --git a/client/src/state/uploadQueue.ts b/client/src/state/uploadQueue.ts index c8e2783c..51333c9f 100644 --- a/client/src/state/uploadQueue.ts +++ b/client/src/state/uploadQueue.ts @@ -704,11 +704,12 @@ export const useUploadQueueStore = create((set, get) => { installPhase = r.mayNotLaunch ? "warn" : "done"; installedTitle = item.installedTitle ?? item.contentId ?? item.displayName ?? null; - // Persist this exact package as installed (per staged path) so its + // Persist this exact package as installed ON THIS CONSOLE so its // library row shows "Reinstall" — works for an update/DLC, which - // the console's app_list can't confirm. Survives auto-delete being - // off (the staged file then reappears in the library). - recordPkgInstalled(finalDest); + // the console's app_list can't confirm. Scoped to the item's host + // so a sibling console with the same staged file isn't affected. + // Survives auto-delete being off (the staged file then reappears). + recordPkgInstalled(hostOf(item.addr), finalDest); if (r.mayNotLaunch) { mountWarnings.push( "Installed, but it may not launch on this firmware — re-install from the Install Package tab if it won't start.", diff --git a/engine/Cargo.lock b/engine/Cargo.lock index 18d93fbb..c6547f86 100644 --- a/engine/Cargo.lock +++ b/engine/Cargo.lock @@ -494,7 +494,7 @@ dependencies = [ [[package]] name = "ftx2-proto" -version = "3.3.18" +version = "3.3.19" dependencies = [ "serde", "thiserror", @@ -954,7 +954,7 @@ dependencies = [ [[package]] name = "ps5upload-bench" -version = "3.3.18" +version = "3.3.19" dependencies = [ "criterion", "ftx2-proto", @@ -964,7 +964,7 @@ dependencies = [ [[package]] name = "ps5upload-core" -version = "3.3.18" +version = "3.3.19" dependencies = [ "anyhow", "base64", @@ -981,7 +981,7 @@ dependencies = [ [[package]] name = "ps5upload-engine" -version = "3.3.18" +version = "3.3.19" dependencies = [ "anyhow", "axum", @@ -999,7 +999,7 @@ dependencies = [ [[package]] name = "ps5upload-lab" -version = "3.3.18" +version = "3.3.19" dependencies = [ "anyhow", "ftx2-proto", @@ -1008,7 +1008,7 @@ dependencies = [ [[package]] name = "ps5upload-pkg" -version = "3.3.18" +version = "3.3.19" dependencies = [ "serde", "serde_json", @@ -1017,7 +1017,7 @@ dependencies = [ [[package]] name = "ps5upload-tests" -version = "3.3.18" +version = "3.3.19" dependencies = [ "anyhow", "ftx2-proto", diff --git a/engine/Cargo.toml b/engine/Cargo.toml index 1c0c6824..75106617 100644 --- a/engine/Cargo.toml +++ b/engine/Cargo.toml @@ -13,7 +13,7 @@ resolver = "2" [workspace.package] edition = "2021" license = "GPL-3.0-or-later" -version = "3.3.18" +version = "3.3.19" [workspace.dependencies] anyhow = "1.0" diff --git a/payload/include/config.h b/payload/include/config.h index 1e9b36b0..48d4d39c 100644 --- a/payload/include/config.h +++ b/payload/include/config.h @@ -5,7 +5,7 @@ * UI tell apart an old payload still running from a build that includes * a particular fix, without having to boot the console. Keep in sync * with the desktop app's package.json during releases. */ -#define PS5UPLOAD2_VERSION "3.3.18" +#define PS5UPLOAD2_VERSION "3.3.19" /* Author credit — embedded in the startup toast so anyone looking at * the console screen knows who wrote the software that just loaded. * Kept separate from VERSION so release scripts can bump the version