Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Files & Directories

AeorDB exposes a content-addressable filesystem through its file routes. Every path under /files/ represents either a file or a directory.

Endpoint Summary

MethodPathDescriptionAuthStatus Codes
PUT/files/{path}Store a fileYes201, 400, 404, 409, 413, 500
GET/files/{path}Read a file or list a directoryYes200, 404, 500
DELETE/files/{path}Delete a fileYes200, 404, 500
HEAD/files/{path}Check existence and get metadataYes200, 404, 500
PATCH/files/{path}Rename or move a file or symlinkYes200, 400, 404, 500
POST/files/shareShare paths with users/groupsYes (root)200, 400, 404, 500
GET/files/shares?path=List active shares for a pathYes200, 500
DELETE/files/sharesRevoke a shareYes (root)200, 404, 500
POST/files/share-linkCreate a share link (scoped API key + JWT URL)Yes (root)201, 400, 403, 500
GET/files/share-links?path=List active share links for a pathYes (root)200, 403, 500
DELETE/files/share-links/{key_id}Revoke a share linkYes (root)200, 403, 404, 500
GET/files/shared-with-meList paths shared with the current userYes200, 403, 500
PUT/links/{path}Create or update a symlinkYes201, 400, 500

Searching by metadata: Files can also be searched by their metadata – filename, extension, size, content type, and timestamps – using virtual fields in the query API. Virtual field queries require no index configuration; just query with @-prefixed field names like @filename, @extension, or @size.


PUT /files/

Store a file at the given path. Parent directories are created automatically. If a file already exists at the path, it is overwritten (creating a new version).

Streaming uploads: Files are streamed in 256 KB chunks – the server never buffers the full file in memory. Files up to the router-level limit (10 GB) are supported. For resumable uploads of very large files, see the chunked upload protocol.

Request

  • Headers:
    • Authorization: Bearer <token> (required)
    • Content-Type (optional) – auto-detected from magic bytes if omitted
  • Body: raw file bytes

Response

Status: 201 Created

{
  "path": "/data/report.pdf",
  "content_type": "application/pdf",
  "size": 245678,
  "created_at": 1775968398000,
  "updated_at": 1775968398000
}

Side Effects

  • If the path matches /.config/indexes.json (or a nested variant like /data/.config/indexes.json), a reindex task is automatically enqueued for the parent directory. Any existing pending or running reindex for that path is cancelled first.
  • Triggers entries_created events on the event bus.
  • Runs any deployed store-phase plugins.

Example

curl -X PUT http://localhost:6830/files/data/report.pdf \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/pdf" \
  --data-binary @report.pdf

Error Responses

StatusCondition
400Invalid input (e.g., empty path)
404Parent path references a non-existent entity
409Path conflict (e.g., file exists where directory expected)
413Payload exceeds the router-level body limit (10 GB)
500Internal storage failure

GET /files/

Read a file or list a directory. The server determines the type automatically:

  • If the path resolves to a file, the file content is streamed with appropriate headers.
  • If the path resolves to a directory, a JSON object with an items array of children is returned.

Request

  • Headers:
    • Authorization: Bearer <token> (required)

Query Parameters

ParamTypeDefaultDescription
snapshotstringRead the file as it was at this named snapshot
versionstringRead the file at this version hash (hex)
nofollowbooleanfalseIf the path is a symlink, return metadata instead of following
depthinteger0Directory listing depth: 0 = immediate children, -1 = unlimited recursion
globstringFilter directory listing by file name glob pattern (*, ?, [abc])
limitintegerMaximum number of entries to return in a directory listing
offsetintegerNumber of entries to skip before returning results

File Response

Status: 200 OK

Headers:

HeaderDescription
X-AeorDB-PathCanonical path of the file
X-AeorDB-SizeFile size in bytes
X-AeorDB-Created-AtUnix timestamp (milliseconds)
X-AeorDB-Updated-AtUnix timestamp (milliseconds)
Content-TypeMIME type (if known)

Body: raw file bytes (streamed)

Directory Response

Status: 200 OK

Each entry includes path, hash, and numeric entry_type fields. Symlink entries also include a target field.

Entry types: 2 = file, 3 = directory, 8 = symlink.

Effective permissions: When a directory listing is accessed with a scoped API key or as a non-root user, each item in the response includes an effective_permissions field – an 8-character crudlify string indicating what operations the caller can perform on that item. Ancestor directory items (directories leading to the scoped path) receive read+list permissions only.

{
  "items": [
    {
      "path": "/data/report.pdf",
      "name": "report.pdf",
      "entry_type": 2,
      "hash": "a3f8c1...",
      "size": 245678,
      "created_at": 1775968398000,
      "updated_at": 1775968398000,
      "content_type": "application/pdf"
    },
    {
      "path": "/data/images",
      "name": "images",
      "entry_type": 3,
      "hash": "b2c4d5...",
      "size": 0,
      "created_at": 1775968000000,
      "updated_at": 1775968000000,
      "content_type": null
    },
    {
      "path": "/data/latest",
      "name": "latest",
      "entry_type": 8,
      "hash": "c3d5e6...",
      "target": "/data/report.pdf",
      "size": 0,
      "created_at": 1775968500000,
      "updated_at": 1775968500000,
      "content_type": null
    }
  ]
}

Paginated Directory Listing

Use limit and offset to paginate directory listings:

curl "http://localhost:6830/files/data/?limit=10&offset=20" \
  -H "Authorization: Bearer $TOKEN"

When limit or offset is provided, the response includes pagination metadata:

{
  "items": [...],
  "total": 150,
  "limit": 10,
  "offset": 20
}
FieldTypeDescription
totalintegerTotal number of entries (before pagination)
limitintegerThe limit that was applied
offsetintegerThe offset that was applied

Examples

Read a file:

curl http://localhost:6830/files/data/report.pdf \
  -H "Authorization: Bearer $TOKEN" \
  -o report.pdf

List a directory:

curl http://localhost:6830/files/data/ \
  -H "Authorization: Bearer $TOKEN"

Recursive Directory Listing

Use the depth and glob query parameters to list files recursively:

# List all files recursively
curl http://localhost:6830/files/data/?depth=-1 \
  -H "Authorization: Bearer $TOKEN"

# List only .psd files anywhere under /assets/
curl "http://localhost:6830/files/assets/?depth=-1&glob=*.psd" \
  -H "Authorization: Bearer $TOKEN"

# List one level deep
curl http://localhost:6830/files/data/?depth=1 \
  -H "Authorization: Bearer $TOKEN"

When depth > 0 or depth = -1, the response contains files only in a flat list. Directory entries are traversed but not included in the output.

Versioned Reads

Read a file as it was at a specific snapshot or version:

# Read file at a named snapshot
curl "http://localhost:6830/files/data/report.pdf?snapshot=v1.0" \
  -H "Authorization: Bearer $TOKEN"

# Read file at a specific version hash
curl "http://localhost:6830/files/data/report.pdf?version=a1b2c3..." \
  -H "Authorization: Bearer $TOKEN"

If both snapshot and version are provided, snapshot takes precedence. Returns 404 if the file did not exist at that version.

Error Responses

StatusCondition
404Path does not exist as file or directory
500Internal read failure

DELETE /files/

Delete a file or empty directory at the given path. Creates a DeletionRecord and removes the entry from its parent directory listing. The handler tries file deletion first, then directory deletion, then returns 404. Non-empty directories return 400 with “Directory is not empty (N children)”.

Request

  • Headers:
    • Authorization: Bearer <token> (required)

Response

Status: 200 OK

{
  "deleted": true,
  "path": "/data/report.pdf"
}

Side Effects

  • Triggers entries_deleted events on the event bus.
  • Updates index entries for the deleted file.

Example

curl -X DELETE http://localhost:6830/files/data/report.pdf \
  -H "Authorization: Bearer $TOKEN"

Error Responses

StatusCondition
400Directory is not empty
404Path not found (neither file nor directory)
500Internal deletion failure

PATCH /files/

Rename or move a file or symlink to a new path. This is a metadata-only operation – no data is copied in the content-addressed store. The file’s content hash remains the same; only the path mapping changes.

Request

  • Headers:
    • Authorization: Bearer <token> (required)
    • Content-Type: application/json (required)
  • Body:
{
  "to": "/new/path/report.pdf"
}
FieldTypeRequiredDescription
tostringYesDestination path for the file or symlink

Response

Status: 200 OK

{
  "from": "/data/report.pdf",
  "to": "/archive/report.pdf"
}

Side Effects

  • Triggers entries_created and entries_deleted events on the event bus.
  • If the path is a symlink, the symlink itself is moved (not the target).
  • If a file already exists at the destination path, the operation fails.

Example

# Move a file
curl -X PATCH http://localhost:6830/files/data/report.pdf \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"to": "/archive/report.pdf"}'

# Rename a symlink
curl -X PATCH http://localhost:6830/files/latest-logo \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"to": "/current-logo"}'

Error Responses

StatusCondition
400Missing to field or invalid destination path
404Source file or symlink not found
500Internal rename failure

AeorDB supports soft symlinks — entries that point to another path. Symlinks are transparent by default: reading a symlink path returns the target’s content.

Create or update a symlink.

Request Body:

{
  "target": "/assets/logo.psd"
}

Response: 201 Created

{
  "path": "/latest-logo",
  "target": "/assets/logo.psd",
  "entry_type": 8,
  "created_at": 1775968398000,
  "updated_at": 1775968398000
}

The target path does not need to exist at creation time (dangling symlinks are allowed).

By default, GET /files/{path} follows symlinks transparently:

# Returns the content of /assets/logo.psd
curl http://localhost:6830/files/latest-logo \
  -H "Authorization: Bearer $TOKEN"

To inspect the symlink itself without following it, use ?nofollow=true:

curl "http://localhost:6830/files/latest-logo?nofollow=true" \
  -H "Authorization: Bearer $TOKEN"

Returns the symlink metadata as JSON instead of the target’s content.

Symlinks can point to other symlinks — chains are followed recursively. AeorDB detects cycles and enforces a maximum resolution depth of 32 hops.

ScenarioResult
Symlink -> fileReturns file content
Symlink -> directoryReturns directory listing
Symlink -> symlink -> fileFollows chain, returns file content
Symlink -> nonexistent404 (dangling symlink)
Symlink cycle (A -> B -> A)400 with cycle detection message
Chain exceeds 32 hops400 with depth exceeded message

HEAD /files/{path} returns symlink metadata as headers:

X-AeorDB-Entry-Type: symlink
X-AeorDB-Symlink-Target: /assets/logo.psd
X-AeorDB-Path: /latest-logo
X-AeorDB-Created-At: 1775968398000
X-AeorDB-Updated-At: 1775968398000

DELETE /files/{path} on a symlink deletes the symlink itself, not the target:

curl -X DELETE http://localhost:6830/files/latest-logo \
  -H "Authorization: Bearer $TOKEN"
{
  "deleted": true,
  "path": "latest-logo",
  "entry_type": "symlink"
}

Symlinks appear in directory listings with entry_type: 8 and a target field:

{
  "path": "/data/latest",
  "name": "latest",
  "entry_type": 8,
  "hash": "c3d5e6...",
  "target": "/data/report.pdf",
  "size": 0,
  "created_at": 1775968500000,
  "updated_at": 1775968500000,
  "content_type": null
}

Symlinks are versioned like files. Snapshots capture the symlink’s target path at that point in time. Restoring a snapshot restores the link, not the resolved content.


Sharing

AeorDB supports sharing files and directories with specific users and groups. Shares are stored as .permissions files in the filesystem — the sharing API is a convenience layer on top of the existing permission system.

POST /files/share

Share one or more paths with users and/or groups. For files, the permission is scoped to just that file using a path_pattern. For directories, the permission applies to everything inside.

Auth: Root only.

Request Body:

{
  "paths": ["/photos/sunset.jpg", "/photos/beach.jpg"],
  "users": ["6f94eecf-b136-47b4-9b47-c20f781f1f5b"],
  "groups": ["design-team"],
  "permissions": "crudl..."
}
FieldTypeRequiredDescription
pathsarrayYesPaths to share (files or directories)
usersarrayNoUser UUIDs to share with
groupsarrayNoGroup names to share with
permissionsstringYes8-character crudlify permission flags

At least one of users or groups must be non-empty.

Response: 200 OK

{
  "shared": 2,
  "paths": ["/photos/sunset.jpg", "/photos/beach.jpg"]
}

How it works:

  • For each file path, a PermissionLink is created on the parent directory’s .permissions file with a path_pattern matching the filename — so the permission only applies to that specific file.
  • For each directory path, the link is created with no path_pattern — it applies to everything in the directory.
  • If a link for the same group and pattern already exists, it is updated (not duplicated).

Example:

curl -X POST http://localhost:6830/files/share \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "paths": ["/photos/sunset.jpg"],
    "users": ["6f94eecf-b136-47b4-9b47-c20f781f1f5b"],
    "permissions": "cr..l..."
  }'

GET /files/shares

List active shares for a path.

Query Parameters:

ParamTypeRequiredDescription
pathstringYesPath to list shares for

Response: 200 OK

{
  "path": "/photos/sunset.jpg",
  "shares": [
    {
      "group": "user:6f94eecf-...",
      "username": "alice",
      "allow": "cr..l...",
      "deny": "........",
      "path_pattern": "sunset.jpg"
    },
    {
      "group": "design-team",
      "allow": "crudl...",
      "deny": "........",
      "path_pattern": null
    }
  ]
}

For user:{uuid} groups, the username field is resolved from the user store. For named groups, username is null.

When querying a specific file, only shares with a matching path_pattern (or directory-wide shares with no pattern) are returned.

Example:

curl "http://localhost:6830/files/shares?path=/photos/sunset.jpg" \
  -H "Authorization: Bearer $TOKEN"

DELETE /files/shares

Revoke a specific share by removing the matching PermissionLink.

Auth: Root only.

Request Body:

{
  "path": "/photos/sunset.jpg",
  "group": "user:6f94eecf-...",
  "path_pattern": "sunset.jpg"
}
FieldTypeRequiredDescription
pathstringYesThe shared path
groupstringYesThe group to revoke (user:{uuid} or group name)
path_patternstringNoThe path pattern to match (null for directory-wide links)

Response: 200 OK

{
  "revoked": true,
  "group": "user:6f94eecf-..."
}

Example:

curl -X DELETE http://localhost:6830/files/shares \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "path": "/photos/sunset.jpg",
    "group": "user:6f94eecf-...",
    "path_pattern": "sunset.jpg"
  }'

Error Responses (Sharing)

StatusCondition
400Empty paths, no users/groups, or invalid permissions string
403Non-root user attempted to share or unshare
404Path not found, or no matching share to revoke
500Internal failure writing permissions

Shareable URLs backed by scoped API keys. A share link bundles a scoped API key with a JWT token into a URL that opens the portal file browser, scoped to the shared path.

POST /files/share-link

Create a share link for one or more paths. This creates a scoped API key with user_id: null and generates a JWT token embedded in the URL.

Auth: Root only.

Request Body:

{
  "paths": ["/photos/vacation"],
  "permissions": "cr..l...",
  "expires_in_days": 30,
  "base_url": "https://files.example.com"
}
FieldTypeRequiredDescription
pathsarrayYesPaths to share
permissionsstringYes8-character crudlify permission flags
expires_in_daysintegerNoDays until expiry (null = never expires)
base_urlstringYesBase URL for the generated share link

Response: 201 Created

{
  "url": "https://files.example.com/share?token=eyJ...",
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "key_id": "a1b2c3d4-...",
  "permissions": "cr..l...",
  "expires_at": 1778560398000,
  "paths": ["/photos/vacation"]
}

The URL opens the portal file browser scoped to the shared path. Ancestor directories are navigable (read+list only) – only path components leading to the target are visible.

Example:

curl -X POST http://localhost:6830/files/share-link \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "paths": ["/photos/vacation"],
    "permissions": "-r--l---",
    "expires_in_days": 7,
    "base_url": "https://files.example.com"
  }'

List active share links for a path.

Auth: Root only.

Query Parameters:

ParamTypeRequiredDescription
pathstringYesPath to list share links for

Response: 200 OK

{
  "path": "/photos/vacation",
  "links": [
    {
      "key_id": "a1b2c3d4-...",
      "label": "share-link:/photos/vacation",
      "permissions": "-r--l---",
      "expires_at": 1778560398000,
      "created_at": 1775968398000
    }
  ]
}

Example:

curl "http://localhost:6830/files/share-links?path=/photos/vacation" \
  -H "Authorization: Bearer $TOKEN"

Revoke a share link. This deletes the underlying scoped API key and invalidates the JWT.

Auth: Root only.

Response: 200 OK

{
  "revoked": true,
  "key_id": "a1b2c3d4-..."
}

Example:

curl -X DELETE http://localhost:6830/files/share-links/a1b2c3d4-... \
  -H "Authorization: Bearer $TOKEN"
StatusCondition
400Missing or invalid fields (paths, permissions, base_url)
403Non-root user attempted to create, list, or revoke share links
404Share link not found (revoke)
500Internal failure creating or revoking share link

Shared With Me

GET /files/shared-with-me

List paths that have been shared with the current user. Scans all .permissions files to find paths where the calling user has group-level access. This is used by the portal to discover entry points when the user has no root-level permissions.

Auth: Requires a user-bound token. Share tokens receive 403. Root users receive an empty list (they already have access to everything).

Response: 200 OK

{
  "paths": [
    {
      "path": "/photos/vacation/",
      "permissions": ".r..l...",
      "path_pattern": null
    }
  ]
}

Each entry includes the shared path, the permissions string granted, and an optional path_pattern (non-null when the share targets a specific file within a directory).

Example:

curl http://localhost:6830/files/shared-with-me \
  -H "Authorization: Bearer $TOKEN"

Error Responses:

StatusCondition
403Called with a share token instead of a user token
500Internal failure scanning permissions

HEAD /files/

Check whether a path exists and retrieve its metadata as response headers, without downloading the body. Works for both files and directories.

Request

  • Headers:
    • Authorization: Bearer <token> (required)

Response

Status: 200 OK (empty body)

Headers:

HeaderValue
X-AeorDB-Entry-Typefile, directory, or symlink
X-AeorDB-PathCanonical path
X-AeorDB-SizeFile size in bytes (files only)
X-AeorDB-Created-AtUnix timestamp in milliseconds (files only)
X-AeorDB-Updated-AtUnix timestamp in milliseconds (files only)
Content-TypeMIME type (files only, if known)
X-AeorDB-Symlink-TargetTarget path (symlinks only)

Example

curl -I http://localhost:6830/files/data/report.pdf \
  -H "Authorization: Bearer $TOKEN"
HTTP/1.1 200 OK
X-AeorDB-Entry-Type: file
X-AeorDB-Path: /data/report.pdf
X-AeorDB-Size: 245678
X-AeorDB-Created-At: 1775968398000
X-AeorDB-Updated-At: 1775968398000
Content-Type: application/pdf

Error Responses

StatusCondition
404Path does not exist
500Internal metadata lookup failure