Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.3.18
3.3.19
4 changes: 2 additions & 2 deletions client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion client/package.json
Original file line number Diff line number Diff line change
@@ -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 <phantomptr@gmail.com>",
Expand Down
10 changes: 5 additions & 5 deletions client/src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion client/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
2 changes: 1 addition & 1 deletion client/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
46 changes: 46 additions & 0 deletions client/src/state/pkgLibrary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ import {
installSpaceWarning,
installedLastResult,
runPkgInstall,
recordPkgInstalled,
isPkgInstalledHere,
PKG_MAY_NOT_LAUNCH_MESSAGE,
PKG_PATCH_REJECTED_HINT,
type PkgEntry,
Expand Down Expand Up @@ -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<string, string>();
(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);
Expand Down
68 changes: 50 additions & 18 deletions client/src/state/pkgLibrary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, PkgPathMeta> {
Expand All @@ -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<string, string[]> {
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
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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,
Expand Down
9 changes: 5 additions & 4 deletions client/src/state/uploadQueue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -704,11 +704,12 @@ export const useUploadQueueStore = create<QueueState>((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.",
Expand Down
14 changes: 7 additions & 7 deletions engine/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion engine/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion payload/include/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading