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
| Method | Path | Description | Auth | Status Codes |
|---|---|---|---|---|
| PUT | /files/{path} | Store a file | Yes | 201, 400, 404, 409, 413, 500 |
| GET | /files/{path} | Read a file or list a directory | Yes | 200, 206, 404, 416, 500 |
| POST | /files/fetch | Batch read file bodies as JSON strings | Yes | 200, 400, 404, 413, 500 |
| DELETE | /files/{path} | Delete a file | Yes | 200, 404, 500 |
| HEAD | /files/{path} | Check existence and get metadata | Yes | 200, 404, 500 |
| PATCH | /files/{path} | Rename a file/symlink (application/json) or JSON merge-patch into a stored document (application/merge-patch+json) | Yes | 200, 201, 400, 404, 413, 415, 500 |
| POST | /files/share | Share paths with users/groups | Yes (root) | 200, 400, 404, 500 |
| GET | /files/shares?path= | List active shares for a path | Yes | 200, 500 |
| DELETE | /files/shares | Revoke a share | Yes (root) | 200, 404, 500 |
| POST | /files/share-link | Create 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 path | Yes (root) | 200, 403, 500 |
| DELETE | /files/share-links/{key_id} | Revoke a share link | Yes (root) | 200, 403, 404, 500 |
| GET | /files/shared-with-me | List paths shared with the current user | Yes | 200, 403, 500 |
| POST | /files/copy | Copy files or directories | Yes | 200, 400, 404, 500 |
| POST | /files/search | Global cross-directory search | Yes | 200, 400, 500 |
| PUT | /links/{path} | Create or update a symlink | Yes | 201, 400, 500 |
Searching by metadata: Files can also be searched by their metadata – path, filename, extension, hash, size, content type, and timestamps – using virtual fields in the query API. New databases index these fields by default; legacy databases can add the default config and reindex. If a virtual-field index is absent, AeorDB falls back to a FileRecord metadata scan.
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
/.aeordb-config/indexes.json(or a nested variant like/data/.aeordb-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. Configs containing only virtual@metadata fields use metadata-only reindexing automatically. - Triggers
entries_createdevents 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
| Status | Condition |
|---|---|
| 400 | Invalid input (e.g., empty path) |
| 404 | Parent path references a non-existent entity |
| 409 | Path conflict (e.g., file exists where directory expected) |
| 413 | Payload exceeds the router-level body limit (10 GB) |
| 500 | Internal 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
itemsarray of children is returned.
Request
- Headers:
Authorization: Bearer <token>(required)Range: bytes=<start>-<end>(optional for file reads) – single byte ranges only
Query Parameters
| Param | Type | Default | Description |
|---|---|---|---|
snapshot | string | — | Read the file as it was at this named snapshot |
version | string | — | Read the file at this version hash (hex) |
nofollow | boolean | false | If the path is a symlink, return metadata instead of following |
depth | integer | 0 | Directory listing depth: 0 = immediate children, -1 = unlimited recursion |
glob | string | — | Filter directory listings by name, relative path, or full-path glob pattern (*, ?, **) |
limit | integer | — | Maximum number of entries to return in a directory listing |
offset | integer | — | Number of entries to skip before returning results |
File Response
Status: 200 OK for full reads, 206 Partial Content for valid byte-range reads
Headers:
| Header | Description |
|---|---|
Accept-Ranges | bytes for file responses |
Content-Length | Bytes returned in this response |
Content-Range | Returned on 206 Partial Content, e.g. bytes 1024-2047/4096 |
X-AeorDB-Path | Canonical path of the file |
X-AeorDB-Size | File size in bytes |
X-AeorDB-Created-At | Unix timestamp (milliseconds) |
X-AeorDB-Updated-At | Unix timestamp (milliseconds) |
Content-Type | MIME type (if known) |
Body: raw file bytes (streamed)
Byte Range Reads
File reads support HTTP single byte ranges for browser media seeking and partial downloads:
curl http://localhost:6830/files/media/movie.mp4 \
-H "Authorization: Bearer $TOKEN" \
-H "Range: bytes=1048576-2097151" \
-o movie.part
Supported forms:
| Range | Meaning |
|---|---|
bytes=100-199 | Inclusive byte range |
bytes=100- | From byte 100 through end of file |
bytes=-1024 | Last 1024 bytes |
Unsatisfiable or malformed byte ranges return 416 Range Not Satisfiable with Content-Range: bytes */<file_size>. Multi-range requests are not supported.
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_permissionsfield – 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
}
| Field | Type | Description |
|---|---|---|
total | integer | Total number of entries (before pagination) |
limit | integer | The limit that was applied |
offset | integer | The 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 JSON frames anywhere under /sessions/
curl "http://localhost:6830/files/sessions/?depth=-1&glob=**/frames/*.json" \
-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. Recursive globs can match a basename (*.psd), a path relative to the requested directory (**/frames/*.json), or a full path.
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
| Status | Condition |
|---|---|
| 404 | Path does not exist as file or directory |
| 416 | Requested byte range cannot be satisfied |
| 500 | Internal read failure |
POST /files/fetch
Fetch multiple files or multiple file ranges in one request. There are two request shapes:
paths: whole-file batch fetch, returned as a JSON object keyed by canonical path.items: range batch fetch, returned as an ordereditemsarray.
Provide either paths or items, not both. Directories, missing files, system paths, and unreadable paths return 404. File bytes are encoded into JSON strings with UTF-8 lossy conversion, so binary data may contain replacement characters.
Request
- Headers:
Authorization: Bearer <token>(required)Content-Type: application/json(required)
- Body:
{
"paths": [
"/data/a.txt",
"/data/b.json"
],
"max_bytes": 1048576
}
| Field | Type | Required | Description |
|---|---|---|---|
paths | array | Yes for whole-file mode | File paths to fetch |
items | array | Yes for range mode | Range fetch requests; see below |
max_bytes | integer | No | Optional lower cumulative byte limit for this request. Values above the server cap are clamped to the server cap. |
continue_on_error | boolean | No | Range mode only. If true, per-item errors are returned in the items array instead of aborting the request. |
Response
Status: 200 OK
{
"/data/a.txt": {
"path": "/data/a.txt",
"name": "a.txt",
"size": 324534,
"created_at": 1235413451345,
"updated_at": 134513453145,
"content_type": "text/plain",
"content": "..."
},
"/data/b.json": {
"path": "/data/b.json",
"name": "b.json",
"size": 2048,
"created_at": 1235413451345,
"updated_at": 134513453145,
"content_type": "application/json",
"content": "{\"ok\":true}"
}
}
Range Fetch Mode
Use items to fetch line, character, byte, or JSON Pointer ranges. This is the preferred follow-up for search hit locators.
{
"items": [
{
"id": "hit-1",
"path": "/data/a.txt",
"if_content_hash": "b3c1...",
"range": { "mode": "lines", "start": 10, "end": 14 }
},
{
"id": "hit-2",
"path": "/data/b.json",
"if_updated_at": 1775968398000,
"range": { "mode": "json_pointer", "pointer": "/messages/0/content" },
"max_bytes": 65536
}
],
"continue_on_error": true
}
| Item Field | Type | Required | Description |
|---|---|---|---|
id | string | No | Caller-supplied ID echoed in the response |
path | string | Yes | File path |
if_content_hash | string | No | Reject as stale if the file content hash changed since search |
if_updated_at | integer | No | Reject as stale if the file timestamp changed since search |
range.mode | string | Yes | lines, chars, bytes, or json_pointer |
range.start | integer | No | Start offset. Lines are 1-based inclusive; chars/bytes are 0-based inclusive |
range.end | integer | No | End offset. Lines are inclusive; chars/bytes are exclusive |
range.pointer | string | Required for json_pointer | RFC 6901 JSON Pointer |
max_bytes | integer | No | Per-item output cap, clamped by the server |
Range-mode response:
{
"items": [
{
"id": "hit-1",
"status": "ok",
"path": "/data/a.txt",
"name": "a.txt",
"size": 324534,
"created_at": 1235413451345,
"updated_at": 134513453145,
"content_hash": "b3c1...",
"content_type": "text/plain",
"range": { "mode": "lines", "start": 10, "end": 14, "pointer": null },
"source_size": 324534,
"content": "...",
"truncated": false
}
],
"has_errors": false
}
If continue_on_error is true, per-item errors have status values such as not_found, stale, invalid, too_large, or error.
Limits
| Limit | Value |
|---|---|
| Maximum paths | 10,000 |
| Maximum cumulative file bytes | 256 MB |
| Default per-item range output | 4 MB |
| Maximum per-item range output | 16 MB |
Example
curl -X POST http://localhost:6830/files/fetch \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"paths":["/data/a.txt","/data/b.json"]}'
Error Responses
| Status | Condition |
|---|---|
| 400 | Empty request, invalid range, both paths and items, or too many paths/items |
| 404 | Any requested path is missing, a directory, a system path, or not readable by the caller |
| 409 | Range item is stale because if_content_hash or if_updated_at no longer matches |
| 413 | Response would exceed the cumulative byte limit |
| 500 | Internal 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"
}
Deleted File Access
Deleted files are invisible to users who lack the d (delete) permission on the containing directory:
GET /files/deleted?path=/dirreturns an empty listGET /files/{path}returns 404 for deleted filesHEAD /files/{path}returns 404 for deleted files- Reading a deleted file via
?version=also returns 404
Root users always have full access to deleted files.
Side Effects
- Triggers
entries_deletedevents 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
| Status | Condition |
|---|---|
| 400 | Directory is not empty |
| 404 | Path not found (neither file nor directory) |
| 500 | Internal deletion failure |
PATCH /files/
PATCH is overloaded by Content-Type:
application/json→ rename the file or symlink (documented below).application/merge-patch+json→ server-side JSON merge into the stored document. See the dedicated JSON Merge Patch reference for that mode.
The content-type alone discriminates — a merge-patch body that happens to contain a "to" key will be merged into the file, not used as a rename instruction.
Rename mode
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 —application/merge-patch+jsonroutes to the merge endpoint instead)
- Body:
{
"to": "/new/path/report.pdf"
}
| Field | Type | Required | Description |
|---|---|---|---|
to | string | Yes | Destination path for the file or symlink |
Response
Status: 200 OK
{
"from": "/data/report.pdf",
"to": "/archive/report.pdf"
}
Side Effects
- Triggers
entries_createdandentries_deletedevents 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
| Status | Condition |
|---|---|
| 400 | Missing to field or invalid destination path |
| 404 | Source file or symlink not found |
| 500 | Internal rename failure |
Symlinks
AeorDB supports soft symlinks — entries that point to another path. Symlinks are transparent by default: reading a symlink path returns the target’s content.
PUT /links/
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).
Reading Symlinks
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.
Symlink Resolution
Symlinks can point to other symlinks — chains are followed recursively. AeorDB detects cycles and enforces a maximum resolution depth of 32 hops.
| Scenario | Result |
|---|---|
| Symlink -> file | Returns file content |
| Symlink -> directory | Returns directory listing |
| Symlink -> symlink -> file | Follows chain, returns file content |
| Symlink -> nonexistent | 404 (dangling symlink) |
| Symlink cycle (A -> B -> A) | 400 with cycle detection message |
| Chain exceeds 32 hops | 400 with depth exceeded message |
HEAD on Symlinks
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
Deleting Symlinks
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 in Directory Listings
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
}
Symlink Versioning
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..."
}
| Field | Type | Required | Description |
|---|---|---|---|
paths | array | Yes | Paths to share (files or directories) |
users | array | No | User UUIDs to share with |
groups | array | No | Group names to share with |
permissions | string | Yes | 8-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
PermissionLinkis created on the parent directory’s.permissionsfile with apath_patternmatching 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:
| Param | Type | Required | Description |
|---|---|---|---|
path | string | Yes | Path 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"
}
| Field | Type | Required | Description |
|---|---|---|---|
path | string | Yes | The shared path |
group | string | Yes | The group to revoke (user:{uuid} or group name) |
path_pattern | string | No | The 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)
| Status | Condition |
|---|---|
| 400 | Empty paths, no users/groups, or invalid permissions string |
| 403 | Non-root user attempted to share or unshare |
| 404 | Path not found, or no matching share to revoke |
| 500 | Internal failure writing permissions |
Link Sharing
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"
}
| Field | Type | Required | Description |
|---|---|---|---|
paths | array | Yes | Paths to share |
permissions | string | Yes | 8-character crudlify permission flags |
expires_in_days | integer | No | Days until expiry (null = never expires) |
base_url | string | Yes | Base 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"
}'
GET /files/share-links
List active share links for a path.
Auth: Root only.
Query Parameters:
| Param | Type | Required | Description |
|---|---|---|---|
path | string | Yes | Path 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"
DELETE /files/share-links/
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"
Error Responses (Link Sharing)
| Status | Condition |
|---|---|
| 400 | Missing or invalid fields (paths, permissions, base_url) |
| 403 | Non-root user attempted to create, list, or revoke share links |
| 404 | Share link not found (revoke) |
| 500 | Internal failure creating or revoking share link |
Shared With Me
GET /files/shared-with-me
List paths that have been shared with the current user. AeorDB resolves the caller’s group memberships against the cached grants index, returning paths where those groups have 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:
| Status | Condition |
|---|---|
| 403 | Called with a share token instead of a user token |
| 500 | Internal 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:
| Header | Value |
|---|---|
X-AeorDB-Entry-Type | file, directory, or symlink |
X-AeorDB-Path | Canonical path |
X-AeorDB-Size | File size in bytes (files only) |
X-AeorDB-Created-At | Unix timestamp in milliseconds (files only) |
X-AeorDB-Updated-At | Unix timestamp in milliseconds (files only) |
Content-Type | MIME type (files only, if known) |
X-AeorDB-Symlink-Target | Target 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
| Status | Condition |
|---|---|
| 404 | Path does not exist |
| 500 | Internal metadata lookup failure |
POST /files/copy
Copy files or directories to a new location. Uses content-addressed deduplication — no data is physically duplicated.
Request
- Headers:
Authorization: Bearer <token>(required)Content-Type: application/json(required)
- Body:
{
"paths": ["/src/file1.txt", "/src/file2.txt"],
"destination": "/dst/"
}
| Field | Type | Required | Description |
|---|---|---|---|
paths | array | Yes | Paths to copy (files or directories) |
destination | string | Yes | Destination directory |
Response
Status: 200 OK
{
"copied": ["/dst/file1.txt", "/dst/file2.txt"]
}
For directories in the paths list, all contents are recursively copied. If any individual copy fails, the error is reported in an errors array but other copies continue.
Example
curl -X POST http://localhost:6830/files/copy \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"paths": ["/src/file1.txt", "/src/file2.txt"],
"destination": "/dst/"
}'
Global Search
POST /files/search
Search across all indexed directories in the database. Unlike POST /files/query which targets a single directory, global search fans out across every directory that has indexes.
Request body:
{
"query": "hero-banner",
"limit": 20,
"offset": 0
}
The query field performs a broad fuzzy search across default-indexed text metadata fields that have fuzzy-capable indexes, such as @filename and @path.
For more targeted searches, use a structured where clause:
{
"where": {
"field": "@filename",
"op": "similar",
"value": "hero",
"threshold": 0.3
},
"limit": 20
}
To find every path under a root that contains a file with a known whole-file
content hash, query @hash with eq:
{
"path": "/some/root/",
"where": {
"field": "@hash",
"op": "eq",
"value": "b3c1..."
},
"limit": 1000
}
@hash is the raw whole-file hash (blake3(file bytes)) stored in the
FileRecord at write time. It is not the first chunk hash. New databases
bootstrap an exact string index for @hash; trigram hash indexes are not
created by default because exact hash lookup is the normal use case and trigram
hash indexes consume substantial memory on large databases. Existing databases
may need their /.aeordb-config/indexes.json updated and reindexed before
@hash eq uses only the exact index; reindexing also retires old hash index
strategies that are no longer present in the config. Legacy FileRecord v0
entries written before this field existed must be migrated before they can
participate in exact @hash lookup; trigger a forced reindex with
POST /system/tasks/reindex and "force": true to backfill them.
For agent workflows, /files/search and /files/query also support opt-in hit
locators with include_matches: true. Locator responses include snippets,
typed ranges, content_hash identity anchors, and fetch hints that can be used
with /files/fetch range mode. See Querying for
the full schema.
Response:
{
"results": [
{
"path": "/assets/hero-banner.png",
"score": 0.85,
"directory": "/assets",
"index_field": "@filename"
}
],
"total": 1,
"directories_searched": 42
}