This folder is a small HTTP API for AI agents to upload, list, and delete public files stored on the PHP server’s filesystem under the prefix (storage root). There is no database.
Humans open the returned url in a browser. Images and PDFs use file.php; HTML uses view.php; audio and video use play.php with a mobile player. Agents use mcp.php for all operations. Operators manage files in admin.php (SSO login).
Use this when an AI agent needs to publish a file (chart, screenshot, PDF report, simple HTML page) and get back a stable HTTPS link to share with a human or embed elsewhere.
Other extensions are rejected. The filename must include the correct extension so the MIME type is set correctly (inline display for images/PDF/HTML/media; download for archives).
All settings come from the environment (stack.env in Docker):
CDN_AUTH_TOKEN — secret shared with your agent; required on every API call.CDN_PODCAST_RSS_TOKEN — secret for the private Pocket Casts Feed only (rss.php?token=…). Keep separate from the agent token.CDN_STORAGE_DIR — directory on the server where files are stored (default /var/www/cdn-data).CDN_STORAGE_PREFIX — folder prefix inside storage (empty = root).CDN_PUBLIC_BASE_URL — optional full URL to this folder if auto-detect fails behind a proxy.CDN_MAX_UPLOAD_BYTES — max file size per upload (currently 300 MB). For multipart uploads, PHP upload_max_filesize and post_max_size must be ≥ this value.Every request to mcp.php must be authenticated. Send Authorization: Bearer <CDN_AUTH_TOKEN> or include "token" / form field token (or ?token= for read-only GET list).
Uploaded files are public. Anyone with the URL can open them. Do not upload secrets.
mcp.phpUse POST with JSON or multipart/form-data. Every request must include action and authentication.
GET ?action=list&token=… is supported for read-only listing.
Prefer multipart for files ≳ a few MB (no base64 bloat). JSON + content_base64 remains fine for small/simple agent calls.
{"action":"list","token":"YOUR_TOKEN"}
Returns all files under the configured storage prefix with pathname, url, size, and uploaded_at.
Look up one file by pathname or public url (play.php, stream.php, view.php, or file.php).
{"action":"get","token":"YOUR_TOKEN","url":"https://example.com/cdn/play.php?pathname=uuid-track.mp3"}
Send file bytes as base64 or raw UTF-8/binary in content for small HTML. Max 300 MB per request (see CDN_MAX_UPLOAD_BYTES).
{
"action": "upload",
"token": "YOUR_TOKEN",
"filename": "chart.png",
"content_base64": "iVBORw0KGgoAAAANSUhEUg..."
}
Raw file bytes via multipart/form-data. Fields: action=upload, file (or content), optional filename (defaults to the upload name), optional token if not using Bearer.
curl -fsS -X POST "https://example.com/cdn/mcp.php" \ -H "Authorization: Bearer YOUR_TOKEN" \ -F "action=upload" \ -F "filename=big.mp3" \ -F "file=@/path/to/big.mp3;type=audio/mpeg"
On success (both paths) the response includes url — give this link to the human.
url points to file.php?pathname=… (also in blob_url).url points to view.php?pathname=…. Direct file URL is in blob_url.url points to play.php?pathname=… (mobile player with resume). Stream URL is stream.php; direct file URL in blob_url.Delete one file by pathname or public url (play.php, stream.php, view.php, or file.php).
{"action":"delete","token":"YOUR_TOKEN","url":"https://example.com/cdn/play.php?pathname=uuid-track.mp3"}
Deletes every file under the configured CDN prefix. Use with care.
{"action":"delete_all","token":"YOUR_TOKEN"}
view.php (HTML proxy)Public, no auth. Serves HTML from local storage with Content-Type: text/html and Content-Disposition: inline.
GET view.php?pathname=uuid-page.html
Only .html / .htm files under your CDN prefix are allowed.
play.php (audio / video player)Public, no auth. Mobile-friendly player with play/pause toggle, ±15 s skip, seek bar, sleep timer (15 / 30 / 60 min), download, and playback position saved in localStorage for resume.
GET play.php?pathname=uuid-track.mp3
Supported: mp3, m4a, webm. Media bytes are served via stream.php (Range-aware proxy).
stream.php (media proxy)Public stream proxy with byte-range support for seeking. Append &download=1 to force download.
GET stream.php?pathname=uuid-track.mp3 GET stream.php?pathname=uuid-track.mp3&download=1
file.php (direct file)Public, no auth. Serves images, PDFs, archives, and other non-HTML/non-media files with the correct Content-Type. Archives download; images and PDFs display inline.
GET file.php?pathname=uuid-chart.png
podcast.php (Episode API for agents)Dedicated API for uploading listen-able Episodes (audio + metadata + optional cover). Auth is the same as mcp.php (Authorization: Bearer, JSON/form token, or ?token= on GET). Each Episode is stored under podcasts/episodes/{id}/ with an episode.json Episode Record as the source of truth. Manage Episodes and copy the private Feed URL in admin.php.
GET ?action=list_episodes&token=… is supported for read-only listing.
Prefer multipart for large audio (e.g. tens of MB). JSON + base64 remains for small/simple calls.
{
"action": "add_episode",
"token": "YOUR_TOKEN",
"title": "Episode title",
"description": "Optional description",
"pub_date": "2026-08-11T12:00:00Z",
"duration_seconds": 3600,
"source_url": "https://example.com/original",
"audio_filename": "talk.mp3",
"audio_base64": "…",
"image_filename": "cover.jpg",
"image_base64": "…"
}
title, audio_filename, audio_base64.mp3 or m4a only.image_filename and image_base64 are required. Cover bytes are sniffed (magic) and stored with a matching extension/Content-Type even if image_filename is wrong (e.g. JPEG named cover.png).CDN_MAX_UPLOAD_BYTES per binary part as mcp.php.episode.audio.stream_url is the Range-aware playback URL (stream.php).Raw audio (and optional cover) bytes. Fields: action=add_episode, title, audio (file), optional audio_filename, optional image + image_filename, plus optional description, pub_date, duration_seconds, source_url, token.
curl -fsS -X POST "https://example.com/cdn/podcast.php" \ -H "Authorization: Bearer YOUR_TOKEN" \ -F "action=add_episode" \ -F "title=Test large episode" \ -F "audio=@/path/to/big.mp3;type=audio/mpeg" \ -F "image=@/path/to/cover.png;type=image/png" \ -F "duration_seconds=3600"
{"action":"list_episodes","token":"YOUR_TOKEN"}
Returns Episode Records newest pub_date first.
{"action":"get_episode","token":"YOUR_TOKEN","id":"EPISODE_UUID"}
{"action":"delete_episode","token":"YOUR_TOKEN","id":"EPISODE_UUID"}
Deletes the Episode Record plus audio and cover files for that id.
rss.php (private Feed)RSS 2.0 Feed of all Episodes for podcast apps (e.g. Pocket Casts). Private via CDN_PODCAST_RSS_TOKEN (not the agent CDN_AUTH_TOKEN). Includes <itunes:block>Yes</itunes:block>.
GET rss.php?token=CDN_PODCAST_RSS_TOKEN
Enclosure URLs point at stream.php (Range-aware). Paste the full URL into Pocket Casts Discover/search.
filename.url from the upload response (not ?download=1).url from the upload response (the view.php link).url from the upload response (the play.php link).JSON includes "ok": true on success. Errors return HTTP 4xx/5xx with "ok": false and an error field.
PHP 8.3, no database. Files live on the server filesystem under CDN_STORAGE_DIR.