All API routes live under /api/, except the single-sign-on hand-off at /auth/sso. Requests and responses are JSON.

The endpoint tables on this page are generated from the server's own route table with make docs; do not edit them by hand. An endpoint added to the code and not to this page is a feature nobody can find, so the generator refuses to run when a route has no description.

Response envelope

Success — the payload is always under data:

{ "data": { "path": "/", "name": "files" } }

Failure:

{ "error": { "message": "invalid path", "code": "bad_request" } }

Stable error codes: bad_request, unauthorized, forbidden, not_found, conflict, internal_error, too_many_attempts, csrf_failed.

Authentication

Sessions use a single signed cookie (filegator_session). State-changing requests must also send the X-CSRF-Token header, whose value is the filegator_csrf cookie.

MethodPathDescription
POST/api/auth/login{"username","password"} → user
POST/api/auth/logoutEnds the session
GET/api/auth/meThe signed-in user, or 401
POST/api/auth/passwordChange your own password
GET/api/pingHealth check
{ "current_password": "old", "new_password": "new" }

The current password must match, so a stolen session cannot take over the account. A new session cookie is returned because the session fingerprint covers the password hash.

Files

All paths are relative to the user's home folder and always start with /. A .. segment that would leave the home folder is rejected with 403.

MethodPathDescription
GET/api/filesList a directory
GET/api/files/batchStream a zip of selected paths
POST/api/files/chmodSet permission bits
GET/api/files/contentRead a text file for the editor
POST/api/files/copyCopy entries into a folder
POST/api/files/createCreate an empty file
POST/api/files/deleteDelete permanently
GET/api/files/downloadDownload a file
POST/api/files/mkdirCreate a directory
POST/api/files/moveMove entries into a folder
POST/api/files/renameRename, or move in one step
POST/api/files/saveWrite text content
GET/api/files/searchSearch names below a folder
GET/api/files/statDescribe one entry
POST/api/files/unzipExtract an archive into a new folder
GET/api/files/uploadWhich chunks are already stored (resume)
POST/api/files/uploadSend one chunk
POST/api/files/upload/cancelDiscard a staged upload and free its chunks
POST/api/files/zipCreate an archive

All POST routes require the filegator_csrf cookie value in the X-CSRF-Token header.

Create

{ "parent": "/", "name": "notes.txt" }

name must not contain /, \ or be ./... Returns the new entry with 201.

Rename

{ "from": "/notes.txt", "to": "/archive/notes.txt" }

The request fails with 409 if the destination exists. Renaming into a different folder moves the entry.

Move / Copy

{ "paths": ["/a.txt", "/b.txt"], "target": "/archive" }

Entries keep their own names inside target. copy recurse into directories.

Delete

{ "paths": ["/a.txt", "/old"] }

Deletion is permanent. Deleting / is refused. Returns 204.

Chmod

{ "paths": ["/a.txt"], "mode": "644" }

mode is three octal digits (a leading 0 is accepted). Requires the chmod permission in addition to read and write. Changing the home folder itself is refused. Returns 204.

Upload

Uploads use the Resumable.js field names, so an existing upload client keeps working:

resumableFilename, resumableRelativePath, resumableIdentifier,
resumableChunkNumber, resumableTotalChunks, resumableTotalSize, file

Each response reports progress:

{ "data": { "status": "uploaded", "received": 2, "total": 4 } }

The final chunk returns 201 with the stored entry:

{
  "data": {
    "status": "stored",
    "entry": { "name": "a.bin", "path": "/a.bin" },
    "received": 4,
    "total": 4
  }
}

Read for editing

GET /api/files/content?path=/notes.md
{
  "data": {
    "path": "/notes.md",
    "name": "notes.md",
    "content": "line one\nline two\n",
    "modified": "2026-10-23T21:19:52Z"
  }
}

Requires read. Only extensions in frontend_config.editable are served. Binary files (a NUL byte in the first kilobyte) are refused with 400, and files over 2 MB with 413.

Save

{ "path": "/notes.md", "content": "text" }

Only extensions listed in frontend_config.editable may be written.

Directory listing

{
  "data": {
    "path": "/docs",
    "parent": "/",
    "entries": [
      {
        "name": "sub",
        "path": "/docs/sub",
        "type": "dir",
        "size": 0,
        "modified": "2026-10-23T17:30:00Z",
        "mode": "755"
      }
    ]
  }
}

Downloads

Files are sent as attachment unless their extension is listed in download_inline (default: pdf, jpg, jpeg, png, gif, webp, bmp, avif), in which case they are rendered inline with their real content type. Responses always include X-Content-Type-Options: nosniff.

Everything else stays an attachment, which is what keeps an uploaded .html or .svg from executing on this origin. That protection is the default, not a ceiling: an administrator can list svg or html on Admin → Files → Inline download extensions, and then those files are rendered — which means script in them runs on this origin. A listed extension is rendered with its real content type; the admin page shows a warning while a risky one is in the list. * renders everything and is the most dangerous setting there is.

Entries are matched as exact extensions: listing html does not render notes.mhtml. A leading dot is accepted (pdf and .pdf are the same), and the list is stored in dotless lowercase form.

The endpoint decides only how the browser is told to present the file. The interface then opens that URL in a new tab, so a download never replaces the file manager — see api.openDownload in the frontend. Any client is free to fetch the URL directly.

Name collisions

Creating, renaming, moving, copying or uploading onto a name that already exists returns 409 and writes nothing. The caller may instead pick a policy with on_collision:

ValueBehaviour
refuse (default)409 Conflict, nothing written
upcountnext free name: report.pdf → report (1).pdf → report (2).pdf
replacethe existing entry is deleted first — destructive
POST /api/files/create
{ "parent": "/", "name": "report.pdf", "on_collision": "upcount" }
GET /api/files/search?path=/docs&q=report
{
  "data": {
    "term": "report",
    "path": "/docs",
    "limited": false,
    "results": [
      {
        "name": "report.txt",
        "path": "/docs/sub/report.txt",
        "type": "file",
        "size": 12,
        "modified": "2026-10-23T17:30:00Z",
        "mode": "644",
        "parent": "/docs/sub"
      }
    ]
  }
}

Archives

Archives use the three generated endpoints POST /api/files/zip, POST /api/files/unzip and GET /api/files/batch, each described above.

POST /api/files/zip
{ "paths": ["/docs"], "destination": "/", "name": "docs.zip" }

The archive is written in place and returned as a file entry. An existing name is refused with 409 — nothing is ever overwritten. Without name the configured default_archive_name is used. Requires read, write and zip.

POST /api/files/unzip
{ "path": "/docs.zip", "destination": "/", "folder": "" }

Extraction always happens inside a new subfolder, named after the archive (docs.zip → docs) unless folder is given, so loose files never spill into the current directory. An existing folder is refused with 409. Entry names containing .., absolute paths or anything resolving outside the destination reject the whole archive with 400 (zip-slip protection), as does a corrupt file. Requires read, write and zip.

GET /api/files/batch?path=/a.txt&path=/docs&name=pick.zip

Streams application/zip directly; no temporary archive is created and no archive id is issued. Requires read, download and batchdownload.

Configuration and languages

These routes are public: a browser needs them before anyone signs in.

MethodPathDescription
GET/api/capabilitiesWhich features this deployment supports
GET/api/configPublic settings the browser needs before login
GET/api/i18nAvailable languages, and the configured default
GET/api/i18n/{lang}Messages for one language

GET /api/config returns the settings the browser needs before login. Like every response, it is wrapped in {"data": …}:

{
  "data": {
    "app_name": "FileGator",
    "language": "english",
    "language_selection": true,
    "logo": "",
    "upload_max_size": 104857600,
    "upload_chunk_size": 1048576,
    "upload_simultaneous": 3,
    "default_archive_name": "archive.zip",
    "editable": [".txt", ".md"],
    "date_format": "YYYY/MM/DD HH:mm:ss",
    "guest_redirection": ""
  }
}

Sensitive settings (session key, IP lists, users file) are never included.

config_read_only is true when the configuration file cannot be written (for example on a read-only volume). The server still runs on in-memory settings, but they are lost on restart: a save from the admin area then answers 409 with the code read_only_config, and the notice bar explains why. Changing the storage root or the auth provider behaves the same way — the switch applies to the running server, the call is refused.

startup_notes describes what this start of the server created or could not resolve, for example:

{
  "startup_notes": [
    {
      "kind": "info",
      "key": "users.created",
      "text": "Created private/users.json with the default accounts (admin and guest). The default password is published: change it on Users."
    }
  ]
}

They are shown in the notice bar to administrators, and are empty on a server that has been running for a while. The text is written by the server, so it is in English and names the paths involved; a configured users file is never named here.

Administration

These routes require an administrator session. POST also requires the CSRF token.

MethodPathDescription
GET/api/admin/authRead the auth provider settings
POST/api/admin/authSwitch or configure the auth provider
GET/api/admin/browseBrowse the host filesystem
GET/api/admin/rootRead the storage root
POST/api/admin/rootChange the storage root
GET/api/admin/settingsRead the editable settings
POST/api/admin/settingsSave the editable settings
GET/api/admin/statusConfiguration warnings
GET/api/admin/storageRead the storage backend settings
POST/api/admin/storageSwitch or configure the storage backend
GET/api/admin/storage/browseBrowse inside the storage root
GET/api/admin/usersList users
POST/api/admin/usersCreate a user
POST/api/admin/users/{username}Update a user
POST/api/admin/users/{username}/deleteDelete a user
POST/api/admin/users/{username}/passwordSet a password without knowing the old one

The browser UI is reached from the Setup button, which only appears for administrators.

Status warnings

GET /api/admin/status reports conditions that are otherwise only visible in the server log:

{
  "data": {
    "config_read_only": false,
    "users_file_read_only": false,
    "default_admin_password": true,
    "storage_root": "/srv/filegator/repository"
  }
}

The admin view shows these as banners. While default_admin_password is true, a Change password button opens the administrator's edit form directly.

Choosing a home folder

Home folders must stay inside the storage root. Use the scoped endpoint:

GET /api/admin/storage/browse?path=/team

Paths are storage-relative (/team/alice), and anything that would climb above the root is rejected with 403. The picker in the Users form starts at the storage root and has no .. entry there.

Choosing a storage root

The root may be any directory on the server, not just repository/:

GET /api/admin/browse?path=/srv/data
{
  "data": {
    "path": "/srv/data",
    "directories": [
      { "name": "projects", "path": "/srv/data/projects" }
    ]
  }
}

Only directories are listed — never file names or contents — so the picker cannot be used to read data. Broken symlinks are skipped; symlinks that point at a directory are offered.

Select a directory with:

POST /api/admin/root
{ "path": "/srv/data/projects" }

The change is validated, saved to storage.root and applied immediately, with no restart. If the path does not exist, is a file or is unreadable, the request fails and the previous root stays active.

Users

POST /api/admin/users
{
  "username": "alice",
  "name": "Alice",
  "role": "user",
  "homedir": "/team/alice",
  "permissions": ["read", "write"],
  "password": "secret",
  "create_home": true
}

Languages

Language codes are lowercase letters, digits and underscores (for example english, portuguese_br). The two endpoints are listed in the generated table under Configuration and languages.

GET /api/i18n answers:

{
  "data": {
    "languages": ["arabic", "english", "german"],
    "default": "english",
    "selectable": true
  }
}

Configuring the default language

The administrator sets the default in frontend_config:

{
  "frontend_config": {
    "language": "german",
    "language_selection": true
  }
}

Single sign-on

MethodPathDescription
GET/auth/ssoSigned hand-off from another application

GET /auth/sso is the endpoint another application redirects a browser to. The URL is signed with a per-application secret; every failure answers the same 403. See Single sign-on for the ticket format.