File Sharing
Upload, download, and share files with your team.
5 min readUploading Files
File uploads are submitted as multipart form data to the REST API. The server validates
the MIME type against a fixed allowlist before writing the file to disk. Files that fail
validation are rejected with a 415 Unsupported Media Type response before
any bytes are written.
multipart/form-data with a file field.MIME Allowlist
The following MIME types are accepted:
| Type | Extensions |
|---|---|
| Images | .jpg, .jpeg, .png, .gif, .webp |
| Documents | .pdf, .txt, .md |
| Spatial data | .geojson, .gpx, .kml |
| Audio | .webm (Opus), .ogg, .mp3, .wav |
| Video | .mp4, .webm, .mov |
| Archives | .zip |
Executable files, scripts, and binary formats outside the allowlist are blocked. Validation is performed on both MIME type and file extension to prevent bypass via content-type spoofing.
Drag-and-Drop UI
The slide-out Files panel accepts drag-and-drop uploads. Dragging one or more files onto the panel highlights a drop zone. Releasing the files begins the upload immediately. A progress bar tracks the upload percentage for each file. Multiple files can be queued and upload sequentially. Completed uploads appear in the file list in real time as the server confirms each one.
Downloading
Content-Type and Content-Disposition: attachment headers.All downloads require a valid JWT. The server checks the token on every download request, preventing unauthenticated access to file contents even when a direct URL is known. In the client, download links are constructed with the current session token appended as a query parameter so the browser's native download behavior works without requiring custom fetch logic.
File List Metadata
Each entry in the response includes:
- id — unique file identifier
- filename — original uploaded filename
- mime_type — validated MIME type
- size — file size in bytes
- uploader_callsign — callsign of the user who uploaded the file
- created_at — ISO 8601 upload timestamp
Spatial Content
When a file is a recognized spatial format, this endpoint returns a parsed representation rather than the raw bytes. GPX files are converted to GeoJSON server-side — the same pipeline used by the Map Overlays system. The response envelope looks like:
{
"type": "FeatureCollection",
"features": [ /* ... */ ],
"bbox": [-118.5, 33.9, -118.1, 34.2]
}
The bbox array follows the GeoJSON convention:
[minLon, minLat, maxLon, maxLat]. Clients can pass this directly to
MapLibre's fitBounds method to zoom the map to the file's extent.
This endpoint is the foundation for the overlay loading workflow. Uploading a GeoJSON or GPX file and then enabling it as an overlay uses this content endpoint to read the parsed geometry into the map source.
Media Playback
Audio and video files can be played directly from the Files panel without downloading. Playback uses authenticated blob URLs — the client fetches the file binary with a JWT header, then creates a temporary in-browser URL for the media element.
- Audio files — play inline with a play/stop toggle button on each row. Clicking play on a different file stops the current one. Blob URLs are revoked when playback ends or the panel closes.
- Video files — open in an overlay modal with native browser controls (play, pause, seek, fullscreen). Closing the modal revokes the blob URL.
Voice recordings created via the optional PTT recording toggle appear as regular
.webm audio files in the Files panel and can be played back, renamed,
or downloaded like any other file.
Real-Time Notifications
File upload and deletion events are broadcast over Socket.IO to all connected clients. This allows every user's Files panel to update its list in real time without polling.
| Event | Direction | Payload |
|---|---|---|
file:created |
Server → all clients | Full file metadata object (same fields as the list endpoint) |
file:updated |
Server → all clients | Updated file metadata object (e.g. after rename) |
file:deleted |
Server → all clients | { id } — the deleted file's identifier |
Storage
Uploaded files are written to the server filesystem within a dedicated directory that
is mounted as a named Docker volume. This means file contents survive container
restarts and image updates. The volume is declared in docker-compose.yml
and persists independently of the application containers.
File metadata — name, size, MIME type, uploader, and upload timestamp — is stored in
the PostgreSQL files table. The on-disk path is recorded in the
storage_path column and is used by the download and content endpoints to
locate the file on the filesystem.
To back up uploaded files, copy the Docker volume data directory alongside a PostgreSQL dump. Both are included in the admin Data Export ZIP archive — see Data Export & Import for details.
Permissions
File sharing enforces role-based access control at every endpoint.
| Action | Observer | Operator | Admin |
|---|---|---|---|
| List files | Yes | Yes | Yes |
| Download file | Yes | Yes | Yes |
| View spatial content | Yes | Yes | Yes |
| Upload file | No | Yes | Yes |
| Rename own file | No | Yes | Yes |
| Rename any file | No | No | Yes |
| Delete own file | No | Yes | Yes |
| Delete any file | No | No | Yes |
{ "filename": "new-name.ext" } as JSON. The filename is sanitized before storage. Only the original uploader or an admin can rename a file.When authentication is disabled (development mode), all endpoints behave as if the
requester has the admin role. Enable AUTH_REQUIRED=true
before deploying to a multi-user environment. See
Authentication & RBAC for configuration details.