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
198 changes: 198 additions & 0 deletions .github/workflows/docker-engine.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
name: docker-engine

# Builds and publishes the ps5upload-engine Docker image to GHCR.
# Tag pushes (v*) publish a multi-arch (amd64 + arm64) manifest.
# PRs that touch engine/ get a build-only smoke-check (no push).
#
# Manual re-run (e.g. after an Actions outage wedges a tag-push run):
# gh workflow run docker-engine.yml -f tag=v3.3.16
on:
push:
tags: ['v*']
pull_request:
paths: ['engine/**', '.github/workflows/docker-engine.yml']
workflow_dispatch:
inputs:
tag:
description: "Release tag to publish (e.g. v3.3.16). Must already exist."
required: true

# Least privilege at the top: read-only by default. Only the jobs that actually
# push to GHCR (build, merge) opt into packages:write below — the PR `verify`
# job inherits read-only and never gets a write-capable token.
permissions:
contents: read

concurrency:
group: docker-engine-${{ github.event.inputs.tag || github.ref }}
cancel-in-progress: true

jobs:
# ── PR gate: build amd64, no push ──────────────────────────────────────────
verify:
if: github.event_name == 'pull_request'
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6

- uses: docker/setup-buildx-action@v3

- uses: docker/build-push-action@v6
with:
context: engine
file: engine/Dockerfile
platforms: linux/amd64
push: false
cache-from: type=gha
cache-to: type=gha,mode=max

# ── Resolve + validate the tag ONCE, hand the trusted values to build/merge ─
# Centralizing this means the tag is regex-validated before it ever reaches a
# checkout `ref:` (no ref injection from a hand-typed workflow_dispatch input),
# and the lowercased image name is computed once via the safe env pattern
# instead of interpolating ${{ github.repository_owner }} into shell.
resolve:
if: github.event_name != 'pull_request'
runs-on: ubuntu-24.04
outputs:
tag: ${{ steps.t.outputs.tag }}
version: ${{ steps.t.outputs.version }}
major_minor: ${{ steps.t.outputs.major_minor }}
image: ${{ steps.t.outputs.image }}
steps:
- name: Validate tag + derive image name
id: t
env:
INPUT_TAG: ${{ github.event.inputs.tag || github.ref_name }}
OWNER: ${{ github.repository_owner }}
run: |
raw_tag="$INPUT_TAG"
if ! printf '%s' "$raw_tag" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+([-+._A-Za-z0-9]*)?$'; then
echo "::error::Refusing to proceed: '$raw_tag' is not a valid version tag"
exit 1
fi
version="${raw_tag#v}"
owner_lc="$(printf '%s' "$OWNER" | tr '[:upper:]' '[:lower:]')"
{
echo "tag=$raw_tag"
echo "version=$version"
echo "major_minor=${version%.*}"
echo "image=ghcr.io/${owner_lc}/ps5upload-engine"
} >> "$GITHUB_OUTPUT"

# ── Per-arch native build, push by digest (no tag yet) ─────────────────────
build:
needs: resolve
if: github.event_name != 'pull_request'
runs-on: ${{ matrix.os }}
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
include:
- { os: ubuntu-24.04, platform: linux/amd64 }
- { os: ubuntu-24.04-arm, platform: linux/arm64 }
env:
IMAGE: ${{ needs.resolve.outputs.image }}
steps:
- uses: actions/checkout@v6
with:
ref: ${{ needs.resolve.outputs.tag }}

- name: Prepare environment
env:
PLATFORM: ${{ matrix.platform }}
run: echo "PLATFORM_PAIR=$(printf '%s' "$PLATFORM" | tr '/' '-')" >> "$GITHUB_ENV"

- uses: docker/setup-buildx-action@v3

- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- uses: docker/metadata-action@v5
id: meta
with:
images: ${{ env.IMAGE }}

- name: Build and push by digest
id: push
uses: docker/build-push-action@v6
with:
context: engine
file: engine/Dockerfile
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha,scope=${{ env.PLATFORM_PAIR }}
cache-to: type=gha,mode=max,scope=${{ env.PLATFORM_PAIR }}

- name: Export digest
env:
DIGEST: ${{ steps.push.outputs.digest }}
run: |
mkdir -p /tmp/digests
touch "/tmp/digests/${DIGEST#sha256:}"

- uses: actions/upload-artifact@v7
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1

# ── Merge per-arch digests into a multi-arch manifest ──────────────────────
merge:
needs: [resolve, build]
if: github.event_name != 'pull_request'
runs-on: ubuntu-24.04
permissions:
contents: read
packages: write
env:
IMAGE: ${{ needs.resolve.outputs.image }}
VERSION: ${{ needs.resolve.outputs.version }}
MAJOR_MINOR: ${{ needs.resolve.outputs.major_minor }}
steps:
- uses: actions/download-artifact@v8
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true

- uses: docker/setup-buildx-action@v3

- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- uses: docker/metadata-action@v5
id: meta
with:
images: ${{ env.IMAGE }}
tags: |
type=raw,value=${{ env.VERSION }}
type=raw,value=${{ env.MAJOR_MINOR }}
type=raw,value=latest

- name: Create and push multi-arch manifest
working-directory: /tmp/digests
env:
META_JSON: ${{ steps.meta.outputs.json }}
run: |
# Word-splitting here is intentional: the jq output expands to
# `-t tagA -t tagB ...` and the printf to space-separated
# `image@sha256:<digest>` operands, each a distinct argv entry.
# shellcheck disable=SC2046
docker buildx imagetools create \
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$META_JSON") \
$(printf "${IMAGE}@sha256:%s " *)

- name: Inspect manifest
run: docker buildx imagetools inspect "${IMAGE}:${VERSION}"
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,20 @@ What's new in ps5upload, written for humans.

---

## 3.3.18

- **Cancel a single upload without stopping the whole queue.** The item that's
actively uploading now has a Cancel button. It stops just that transfer (the
partial upload stays resumable) and the rest of that console's queue keeps
going — handy when one big item is hogging the line. ("Stop" still halts
everything as before.)
- **A ready-to-run engine Docker image.** The self-hosted transfer engine is
now published as an official multi-arch image at
`ghcr.io/phantomptr/ps5upload-engine` (`:latest` or pin `:<version>`), so you
can run it on a NAS or home server without building from source. As always:
the engine has no password — keep it on a trusted LAN, never the internet.
Thanks to @Twice6804 for the contribution.

## 3.3.17

UI polish pass — tighter on phones and small windows.
Expand Down
6 changes: 4 additions & 2 deletions FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -617,8 +617,10 @@ person uploading, another browsing"), no coordination is needed.
self-hosted engine)? (3.3.7)**
Yes. The app normally launches its transfer engine as a bundled background
process on `127.0.0.1:19113`, but you can run that engine somewhere else — a
home server, a NAS, or the tiny Docker image in `engine/` — and point the app at
it in **Settings → Engine URL**. When the URL isn't loopback, the app skips the
home server, a NAS — and point the app at it in **Settings → Engine URL**.
An official multi-arch image (`linux/amd64` + `linux/arm64`) is published at
`ghcr.io/phantomptr/ps5upload-engine` — use `:latest` for the newest release
or `:<version>` to pin (e.g. `docker pull ghcr.io/phantomptr/ps5upload-engine:3.3.16`). When the URL isn't loopback, the app skips the
bundled engine and talks to your remote one (cover art included). Two things to
know: (1) the engine's API has **no password**, so to let a remote machine reach
it you set `PS5UPLOAD_ALLOW_IP` to that machine's IP — do this **only on a
Expand Down
31 changes: 31 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ ADB ?= $(ANDROID_HOME)/platform-tools/adb
.PHONY: android-deps android-init android-build android-deploy _android-install-if-device run-android
.PHONY: send-payload gen-fixtures sweep validate validate-xl
.PHONY: sync-version sync-version-check
.PHONY: docker-engine docker-engine-run

# Default target
all: build
Expand Down Expand Up @@ -162,6 +163,10 @@ help:
@echo " make install-engine - Register systemd/launchd/Task Scheduler job"
@echo " make uninstall-engine - Remove the auto-launch registration"
@echo ""
@echo "Docker (self-hosted engine; CI publishes the same image to GHCR):"
@echo " make docker-engine - Build the engine image locally (scratch + static binary)"
@echo " make docker-engine-run - Run it (PS5_HOST=$(PS5_HOST)); exposes :19113"
@echo ""
@echo "Environment overrides (defaults shown):"
@echo " PS5_HOST=$(PS5_HOST) PS5 IP address"
@echo " PS5_LOADER_PORT=$(PS5_LOADER_PORT) PS5 payload loader port"
Expand Down Expand Up @@ -542,6 +547,32 @@ run-android: android-deps payload setup-client
@echo " Attach one first — check with: adb devices"
@cd $(CLIENT_DIR) && $(ANDROID_ENV) npx tauri android dev

#──────────────────────────────────────────────────────────────────────────────
# Docker — self-hosted engine image. Mirrors what .github/workflows/
# docker-engine.yml publishes to ghcr.io/<owner>/ps5upload-engine on each
# release tag (multi-arch there; single-arch host build here). `engine/` is the
# build context; the Dockerfile is a scratch image wrapping the static binary.
#
# SECURITY: the engine API is unauthenticated. Bind to a LAN interface and set
# PS5UPLOAD_ALLOW_IP only on a trusted network — never expose it to the
# internet. See the header of engine/Dockerfile.
#──────────────────────────────────────────────────────────────────────────────

DOCKER ?= docker
DOCKER_ENGINE_IMAGE ?= ps5upload-engine

docker-engine:
@command -v $(DOCKER) >/dev/null 2>&1 || { echo "ERROR: docker not found on PATH."; exit 1; }
@echo "Building $(DOCKER_ENGINE_IMAGE) image (context: $(ENGINE_DIR)/)..."
@$(DOCKER) build -t $(DOCKER_ENGINE_IMAGE) $(ENGINE_DIR)
@echo "✓ Built image $(DOCKER_ENGINE_IMAGE) — run with: make docker-engine-run"

# Run the locally-built engine image. Binds the published port and points it at
# the PS5's transfer port. Override PS5_HOST / the bind address as needed.
docker-engine-run: docker-engine
@echo "Running $(DOCKER_ENGINE_IMAGE) — engine on :19113, PS5 at $(PS5_HOST):9113 ..."
@$(DOCKER) run --rm -p 19113:19113 -e PS5_ADDR=$(PS5_HOST):9113 $(DOCKER_ENGINE_IMAGE)

#──────────────────────────────────────────────────────────────────────────────
# Testing
#──────────────────────────────────────────────────────────────────────────────
Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -444,13 +444,13 @@ below 4.x is obscure and above 12.70 is future work.
opening the desktop client.

**Q: Can the engine run on a different machine (remote / self-hosted)?**
* Yes (3.3.7+). Host the engine elsewhere — including the tiny scratch
image in `engine/Dockerfile` — and point the desktop app at it via
**Settings → Engine URL**; the app then talks to your engine instead of
the bundled one. To let a remote box reach it, set `PS5UPLOAD_ALLOW_IP`
to that box's IP. **Security:** the engine's API is unauthenticated (it
can read/write/delete PS5 files), so only do this on a trusted LAN —
never expose the engine to the internet.
* Yes (3.3.7+). Host the engine elsewhere and point the desktop app at it via
**Settings → Engine URL**. An official multi-arch image is published at
`ghcr.io/phantomptr/ps5upload-engine` (`:latest` or `:<version>`); you can
also build from `engine/Dockerfile`. To let a remote box reach it, set
`PS5UPLOAD_ALLOW_IP` to that box's IP. **Security:** the engine's API is
unauthenticated (it can read/write/delete PS5 files), so only do this on a
trusted LAN — never expose the engine to the internet.

## Contributing

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.3.17
3.3.18
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.17",
"version": "3.3.18",
"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.17"
version = "3.3.18"
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.17",
"version": "3.3.18",
"identifier": "com.phantomptr.ps5upload",
"build": {
"beforeDevCommand": "npm run dev:vite",
Expand Down
Loading
Loading