API-Referenz

HTTP-Routen

PlainApp stellt neben der GraphQL-API unter POST /graphql einen kleinen Satz HTTP-Endpoints bereit. Diese Endpoints verarbeiten Datei-Auslieferung, Multipart-Uploads, Zip-Streaming, DLNA-Casting, WebSocket-Ereignisse und Systemsteuerung. Alle Datei-/Upload-/Zip-Endpoints erfordern den c-id-Header (und meist einen verschlüsselten id-Query-Parameter).

Typdefinitionen

bash
# Common headers for protected HTTP endpoints:
c-id: <client-id>
# /fs, /proxyfs, /zip/* use an encrypted "id" query parameter produced
# by the GraphQL files()/fileIds() queries. The id is opaque to the client.

# Upload endpoints additionally require an encrypted "info" multipart part:
#   info = chaCha20Encrypt(token, json({ dir, size, replace, isAppFile }))
#   file = <raw bytes>
# Chunked uploads use:
#   info = chaCha20Encrypt(token, json({ fileId, index, size }))

# Status codes follow HTTP semantics:
#   200 OK              – success
#   201 Created         – upload stored
#   204 No Content      – init pending password
#   400 Bad Request     – missing/invalid params
#   401 Unauthorized    – bad token
#   403 Forbidden       – expired/invalid encrypted id
#   404 Not Found       – file/entry missing
#   410 Gone            – server shutting down
#   429 Too Many Requests – concurrent zip limit hit

Operationen

http

POST /graphql

Haupt-GraphQL-Endpoint. Hier alle GraphQL-Queries und -Mutationen mit c-id + Authorization-Headern senden. Der /peer_graphql-Endpoint ist das Peer-to-Peer-Äquivalent zwischen gekoppelten Geräten.

bash
curl --request POST \
  --url http://192.168.1.100:8080/graphql \
  --header 'c-id: <client-id>' \
  --header 'Authorization: Bearer <api-token>' \
  --header 'Content-Type: application/json' \
  --data '{"query":"{ app { battery deviceName } }"}'
http

GET /fs

Liefert eine Datei, eine content://-URI oder ein Paket-Icon aus. Der id-Query-Parameter ist ein verschlüsselter String, der von files()/fileIds() zurückgegeben wird. Unterstützt Thumbnail-Erzeugung (?w=&h=&cc=), Byte-Bereiche (?offset=&length=) für Low-Throughput-Transporte, HEIF→PNG-Konvertierung, 3gp→MP4-Transcodierung und Download-Modus (dl=1) mit einem Content-Disposition-Attachment-Header.

bash
# Original (full size, inline)
curl http://192.168.1.100:8080/fs?id=<encrypted-id> -o file.jpg

# Thumbnail (center-cropped to 200x200)
curl 'http://192.168.1.100:8080/fs?id=<encrypted-id>&w=200&h=200' -o thumb.jpg

# Download (attachment)
curl 'http://192.168.1.100:8080/fs?id=<encrypted-id>&dl=1' -OJ

# Byte range (BLE chunked download)
curl 'http://192.168.1.100:8080/fs?id=<encrypted-id>&offset=0&length=4096' \
  -o chunk.bin
http

GET /proxyfs

Proxy für eine Peer-HTTP-URL. Der id-Query-Parameter entschlüsselt zu einer vollständigen http(s)-URL auf einem gekoppelten Peer-Gerät; der Server streamt die Upstream-Antwort zurück. Verwendet für Peer-to-Peer-Datei-Downloads über Wi-Fi Aware.

bash
curl http://192.168.1.100:8080/proxyfs?id=<encrypted-peer-url> -o peer-file.jpg
http

POST /upload

Upload einer einzelnen Datei via multipart/form-data. Der "info"-Teil (ChaCha20-verschlüsseltes JSON) muss vor dem "file"-Teil stehen. Wenn isAppFile=true, werden die Bytes in den inhaltsadressierbaren AppFileStore importiert (dedupliziert nach Hash); andernfalls wird die Datei nach info.dir/<fileName> geschrieben. Gibt den endgültigen Dateinamen zurück (kann ein "(1)"-Suffix haben, um Überschreiben zu vermeiden).

bash
curl --request POST \
  --url http://192.168.1.100:8080/upload \
  --header 'c-id: <client-id>' \
  --form 'info=<encrypted-info-bytes>;type=application/octet-stream' \
  --form 'file=@/path/to/local.jpg'
http

POST /upload_chunk

Upload eines Chunks eines fortsetzbaren, in Chunks aufgeteilten Uploads. Jeder Chunk wird auf der Festplatte unter upload_tmp/{fileId}/chunk_{index} gespeichert. Gibt "<index>:<savedSize>" zurück. Nachdem alle Chunks eingetroffen sind, die GraphQL-Mutation mergeChunks aufrufen, um die endgültige Datei zusammenzufügen.

bash
curl --request POST \
  --url http://192.168.1.100:8080/upload_chunk \
  --header 'c-id: <client-id>' \
  --form 'info=<encrypted-chunk-info-bytes>;type=application/octet-stream' \
  --form 'file=@/path/to/chunk_0.bin'
http

GET /zip/dir

Streamt ein einzelnes Verzeichnis als Zip-Archiv. Der id-Query-Parameter entschlüsselt zu einem Verzeichnispfad. Auf dem Gerät wird nur eine Zip-Operation gleichzeitig ausgeführt — gleichzeitige Anfragen erhalten HTTP 429.

bash
curl 'http://192.168.1.100:8080/zip/dir?id=<encrypted-dir-id>' \
  -o folder.zip
http

GET /zip/files

Streamt mehrere Dateien (oder Mediensuchergebnisse) als ein Zip. Der id entschlüsselt zu { type, query, id, name }. Für den Typ FILE wird die Dateiliste von der GraphQL files()-Query unter request.id in TempHelper gespeichert; für Medientypen führt der Server searchZipItems(type, query, id) aus.

bash
curl 'http://192.168.1.100:8080/zip/files?id=<encrypted-request>' \
  -o selection.zip
http

GET /media/{id}

DLNA-Medien-Endpoint. Liefert einen zuvor registrierten Medienpfad (registriert via UrlHelper.getMediaHttpUrl) an einen TV / DLNA-Renderer. URL-Quellen werden proxied, content://-URIs werden gestreamt, Bilder wie-ist geliefert und alle anderen Dateien mit DLNA-spezifischen Headern + HTTP 206-Range-Unterstützung, damit Renderer den Stream akzeptieren.

bash
# Play on a DLNA renderer:
curl http://192.168.1.100:8080/media/<id>.mp4 -o video.mp4
http

NOTIFY /callback/cast

DLNA-Renderer-Callback. Empfängt das NOTIFY-XML des Renderers und aktualisiert den CastPlayer-Zustand. Bei TransportState=STOPPED (ohne AVTransportURIMetaData) geht der Player automatisch zum nächsten Playlist-Eintrag über. Parst auch RelTime / TrackDuration für Positionsaktualisierungen.

bash
# Sent by the DLNA renderer (not by the client):
NOTIFY /callback/cast HTTP/1.1
Content-Type: text/xml

<?xml ...><e:propertyset>...TransportState val="PLAYING"...</e:propertyset>
http

GET /health

Unauthentifizierter Health-Check-Endpoint. Gibt den App-Paketnamen als Klartext zurück. Verwenden, um zu prüfen, ob der HTTP-Server erreichbar ist.

bash
curl http://192.168.1.100:8080/health
http

GET /shutdown

HTTP-Server herunterfahren. Nur von localhost aufrufbar — Remote-Anfragen erhalten HTTP 403. Schließt alle WebSocket-Sessions, leert die Online-Client-Menge und disposed den HTTP-Server.

bash
# Must be run on the device (e.g. via adb shell):
curl http://localhost:8080/shutdown
http

POST /init

Initialisiert eine Client-Session. Erfordert den c-id-Header. Wenn ein gültiger verschlüsselter Body vorhanden ist (mit dem zwischengespeicherten Token entschlüsselt), gilt die Session als authentifiziert und der Server antwortet 200. Andernfalls: wenn kein Passwort gesetzt ist, antwortet der Server mit einem frisch zurückgesetzten Passwort (200); wenn ein Passwort gesetzt ist, antwortet er 204 No Content und der Client muss sich über den WebSocket-Login-Flow authentifizieren.

bash
curl --request POST \
  --url http://192.168.1.100:8080/init \
  --header 'c-id: <client-id>' \
  --data-binary '<encrypted-token-bytes>'
http

WS /

Haupt-WebSocket-Endpoint. ?cid=<client-id> verwenden, um eine Session zu registrieren; der erste binäre Frame muss ein verschlüsselter Timestamp sein (oder, mit ?auth=1, eine verschlüsselte AuthRequest mit dem Passwort). Nach der Authentifizierung pusht der Server verschlüsselte Ereignis-Frames (Benachrichtigungen, Chat-Updates etc.) über diesen Socket.

bash
# Browser / JS example
const ws = new WebSocket('ws://192.168.1.100:8080/?cid=<client-id>')
ws.binaryType = 'arraybuffer'
ws.send(encryptWithToken(token, Date.now().toString()))

// Login variant:
const ws = new WebSocket('ws://192.168.1.100:8080/?cid=<client-id>&auth=1')
ws.send(encryptWithPassword(password, JSON.stringify({ password: hash })))
http

WS /status

Peer-Presence-WebSocket. ?cid=<peer-id> verwenden, um einen gekoppelten Peer als online zu markieren. Der erste binäre Frame muss eine PeerChatParser-verschlüsselte Anfrage sein; bei Erfolg sendet der Server den Text "ok" und markiert den Peer als online, bis der Socket geschlossen wird.

bash
# Peer-to-peer keepalive (typically opened automatically by the chat
# client after pairing — not usually invoked manually)
const ws = new WebSocket('ws://192.168.1.100:8080/status?cid=<peer-id>')
ws.send(peerEncryptedFrame)