mirror of
https://github.com/seregonwar/zftpd.git
synced 2026-10-21 11:12:57 -05:00
Builds without ENABLE_PKG_INSTALL now report it in /api/status (features.pkg_install) and the web interface hides every installer entry point — the Games action, the file explorer item and the progress pollers — instead of offering actions that only answer 409. The release workflow already builds with the flag off, so this release ships without the remote PKG installer.
280 lines
9.0 KiB
Markdown
280 lines
9.0 KiB
Markdown
# 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.
|
|
|
|
```json
|
|
{"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:**
|
|
```json
|
|
{
|
|
"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.
|
|
|
|
```json
|
|
{"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`.
|
|
|
|
```text
|
|
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.
|
|
|
|
```text
|
|
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.
|
|
|
|
```text
|
|
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:
|
|
|
|
```json
|
|
{"ok":true,"id":1,"state":"launching","title_id":"CUSA00001","target":"download"}
|
|
```
|
|
|
|
`GET /api/dump/status` (the response the web UI polls):
|
|
|
|
```json
|
|
{"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:
|
|
|
|
```json
|
|
{"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.
|
|
|
|
```json
|
|
{"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`):
|
|
|
|
```json
|
|
{"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.
|
|
|
|
```json
|
|
{"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`):**
|
|
```yaml
|
|
rest_command:
|
|
ps5_notify:
|
|
url: "http://{{ states('sensor.ps5_ip') }}:2121/api/notify?text={{ text | urlencode }}"
|
|
method: GET
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"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`:
|
|
|
|
```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:
|
|
|
|
```json
|
|
{"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.
|