Skip to content

Latest commit

 

History

History
279 lines (213 loc) · 9.03 KB

API.md

File metadata and controls

279 lines (213 loc) · 9.03 KB

zhttpd REST API Documentation

Endpoints

GET /api/status

Identity and build switches of the running daemon. The web interface polls it for the connection state and to know which features the payload was compiled with, so views can hide what the build does not have.

{"ok":true,"version":"1.6.0","instance_id":"15114b8a2c31d9f0",
 "start_monotonic_ns":463980095296000,"pid":74,"platform":"ps4",
 "features":{"pkg_install":false}}
  • instance_id changes when the payload is re-injected: clients treat it as a restart and drop their server-side handles.
  • features.pkg_install mirrors the ENABLE_PKG_INSTALL build flag. When it is false the package installer endpoints answer 409 PKG installation is disabled for this build and the UI hides the installer entirely.

GET /api/list

List directory contents.

Query Parameters:

  • path (required): Directory path

Response:

{
  "path": "/home",
  "entries": [
    {
      "name": "file.txt",
      "type": "file",
      "size": 1024
    },
    {
      "name": "subdir",
      "type": "directory",
      "size": 0
    }
  ]
}

Errors:

  • 404: Directory not found
  • 500: Internal error

GET /api/download

Download file (TODO).

GET /api/mounts

Lists the removable volumes that are really connected (/mnt/usb0…/mnt/usb7, /mnt/ext0, /mnt/ext1). The console creates those directories even with nothing plugged in, so zftpd checks whether a filesystem is mounted on them (different st_dev from the parent directory) and reports size and free space. The file manager's Places list is built from this, so empty slots no longer appear as drives.

{"ok":true,"mounts":[{"path":"/mnt/usb0","name":"usb0","kind":"usb","total":32007397376,"free":12561055744}]}

GET /api/archive/zip

Streams a ZIP archive of the selection — the bulk download used by the web UI for "Download as ZIP". Entries are produced while the response is sent (no temporary archive on the console), stored uncompressed, and folders are walked recursively. ZIP64 records are emitted automatically for entries, offsets or counts past the 32-bit limits, so multi-gigabyte selections stay valid.

The response announces the exact Content-Length (no chunked encoding), so console browsers show progress and complete the download reliably.

Query Parameters:

  • path (required, repeatable): file or folder, confined to the HTTP root.
  • name (optional): download name, default zftpd-<timestamp>.zip.

Up to 64 paths per request; unreadable or unconfined entries are skipped, and when the selection exceeds the entry limit the response carries X-Zftpd-Truncated: entry-limit.

GET /api/archive/zip?path=/data/games&path=/data/saves/config.bin&name=backup.zip

GET /api/file/get

Streams a file to the client. By default the raw on-disk bytes are sent (zero-copy sendfile), exactly like every other transfer path.

Query Parameters:

  • path (required): file to send, confined to the HTTP root.
  • decrypt (optional): 1 asks for the decrypted content when the file is a SELF container (PS4/PS5). The kernel pager is swapped in for the transfer (MAP_SELF), so the client receives the plain executable instead of the encrypted container. Other files are always sent untouched, and the response carries X-Zftpd-Decrypted: self plus chunked encoding.

The web UI exposes this as Settings → Downloads → Decrypt protected files, which appends the flag to every download and preview link.

GET /api/file/get?path=/mnt/disc/CUSA00001/sce_sys/eboot.bin&decrypt=1

Game dump

Decrypted dump of an installed title, read from the running title's sandbox mount (/mnt/sandbox/<TITLEID>-app0[-patch0-union] on PS4, _000/app0 and friends under /mnt/sandbox on PS5), where the kernel exposes the application in the clear; SELF/SPRX entries are decrypted through the self pager while they are read. There is no fallback to the encrypted /user/app image, so a title that is not running is launched first and the job waits for its sandbox to mount. One dump runs at a time.

POST /api/dump/start
{"title_id":"CUSA00001","target":"download","format":"zip","decrypt":1}
{"title_id":"CUSA00001","target":"local","dest":"/mnt/usb0/homebrew","format":"files"}

GET  /api/dump/status          # state, source, entries, size, bytes_done
GET  /api/dump?id=N            # streams the prepared archive (Content-Length)
POST /api/dump/cancel          # {"id":N} optional; the single slot is implied

target is download (default; a streamed ZIP the client then fetches with GET /api/dump?id=N) or local. format is zip (default) or files (console targets only — a streamed dump is always a ZIP). decrypt defaults to 1; 0 keeps the raw SELF containers.

POST /api/dump/start answers immediately with the slot id and the initial state; source, entries and size become available through the status as soon as the job worker has walked the source tree:

{"ok":true,"id":1,"state":"launching","title_id":"CUSA00001","target":"download"}

GET /api/dump/status (the response the web UI polls):

{"ok":true,"active":true,"id":1,"state":"running","title_id":"CUSA00001",
 "source":"/mnt/sandbox/CUSA00001_000/app0","entries":1234,"size":32212254720,
 "bytes_done":1073741824,"started":1790778983,"to_console":false,
 "cancelled":false,"message":""}

state is launching, running, ready (streamed dump prepared, waiting for the download), done or failed; message carries the failure reason (Cancelled after a cancel request). A slot that stops advancing is retired by its deadline and reported as failed, so a broken dump never blocks the next one.

POST /api/dump/cancel stops the job — the local writer gives up at the next chunk, a prepared archive is dropped, a streamed download ends early:

{"ok":true,"id":1,"state":"running","cancelled":true}

POST /api/system/eject

Ejects the Blu-ray disc from the console's optical drive (PS4/PS5). The drive is a FreeBSD cd(4) device: zftpd sends CDIOCALLOW + CDIOCEJECT to /dev/cd0 and shows a toast on success.

{"ok":true,"status":"ok","message":"Disc ejected","code":0}

Failures answer HTTP 200 with ok:false and the driver reason, so a client can tell a Digital Edition console (ENOENT) from a busy drive (EBUSY):

{"ok":false,"status":"error","message":"Drive busy: a disc game is running","code":-16}

GET /api/system/disc

Reports whether the console has an optical drive, so the UI can hide the action on Digital Edition consoles.

{"ok":true,"present":true}

GET|POST /api/notify

Show a custom system notification on PS4/PS5 (toast). On non-console builds the message is forwarded to syslog. Designed for local-network automation such as Home Assistant.

Query Parameters:

  • text (required): Notification message (URL-encoded). Max 1023 bytes after decode.
  • icon (optional): PS notification texture suffix, default icon_system. Allowed characters: [A-Za-z0-9_].

Examples:

GET /api/notify?text=Washing%20machine%20is%20done
GET /api/notify?text=Doorbell&icon=icon_system

Home Assistant (rest_command):

rest_command:
  ps5_notify:
    url: "http://{{ states('sensor.ps5_ip') }}:2121/api/notify?text={{ text | urlencode }}"
    method: GET

Response:

{
  "ok": true,
  "text": "Washing machine is done",
  "icon": "icon_system"
}

Errors:

  • 400: Missing/empty text, control characters, or invalid icon
  • 405: Method other than GET/POST

Prefer GET for automation clients. POST is subject to CSRF validation when web upload support is enabled.

Static Files

  • GET / → index.html
  • GET /style.css → CSS
  • GET /app.js → JavaScript

Adding Custom Endpoints

Edit src/http/http_api.c:

http_response_t* http_api_handle(const http_request_t *request) {
    if (strncmp(request->uri, "/api/custom", 11) == 0) {
        return handle_custom(request);
    }
    // ... existing handlers
}

Package installation

GET|POST /api/admin/games/install

Parameter Meaning
path Package already on console storage.
url Package the console downloads by itself (http/https).
dest internal (default) or extended.
overwrite 1 replaces an existing installation of the same title.

Exactly one of path and url is required. When the package is already installed the request answers 409 with {"error":"already_installed","title_id":…,"can_overwrite":true} and nothing is changed; the caller repeats it with overwrite=1 to replace the installation.

GET /api/admin/games/install_status reports the running task:

{"ok":true,"active":true,"task_id":-1,"progress":42,"error":0,
 "length":0,"transferred":0,"title_id":"","path":"https://…/package.pkg"}

On PS5 the installation is driven by a helper program that zftpd delivers to the loader on loopback, because the system installer API is only usable from a process the loader spawned. On PS4 the download service performs it directly.