Advanced Backends
Section titled “Advanced Backends”Mount a backends as a directory: list active mounts, create new ones, update VFS settings, and remove mounts. The page also covers pending uploads, the held files a mount collects when its backend connection is interrupted or its uploads fall behind, and the unreadable records that block a clean teardown.
Mounts use the files-1 service on each container. All requests below are served by the container-scoped URL https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com.
Mounts
Section titled “Mounts”List all mounts
Section titled “List all mounts”GET /api/v1/mounts
Get the list of active filesystem mounts. Each entry carries the same fields as GET /api/v1/mounts/{id}, including its uploads (files still held by the mount, and files still open on it).
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
label | query | string | No | Only mounts whose label equals this value are returned. |
import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.files.mounts.list({ label: 'Photos' });curl -X GET 'https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com/api/v1/mounts?label=Photos' \ -H "Authorization: Bearer <token>"{ "count": 2, "mounts": [ { "id": "mount_550e8400e29b41d4a716446655440000", "backend_id": "backend_660e8400e29b41d4a716446655440000", "mount_path": "/var/lib/hoody/mounts/mount_550e8400e29b41d4a716446655440000", "status": "active", "created_at": 1718300000, "updated_at": 1718300100, "auto_deliver": true, "label": "Photos", "metrics": { "bytes_read": 0, "bytes_written": 0, "cache_hits": 0, "cache_misses": 0, "files_accessed": 0, "last_accessed": 0 }, "pending_uploads": { "count": 0, "bytes": 0 }, "vfs_config": { "cache_mode": "writes", "cache_max_size": 10737418240, "cache_max_age": 3600, "read_chunk_size": 134217728, "write_back": 5, "dir_cache_time": 300, "poll_interval": 60, "read_only": false, "allow_other": false, "allow_non_empty": false }, "uploads": { "state": "idle", "dirty": 0, "open_handles": 0 } } ]}{ "code": "INVALID_PARAMETER", "error": "Invalid request", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INVALID_PARAMETER | Invalid request | A query parameter, a header or the request body is not one this operation accepts, or a copy or a move names a destination inside the directory it copies or moves; the message names what was refused. | Correct the request. |
Get mount details
Section titled “Get mount details”GET /api/v1/mounts/{id}
Get one mount: its current settings, its uploads (files written through it that are still being uploaded or held back, and files still open on it), and its last error if it is not active.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The mount id. |
import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.files.mounts.get('mount_550e8400e29b41d4a716446655440000');curl -X GET 'https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com/api/v1/mounts/mount_550e8400e29b41d4a716446655440000' \ -H "Authorization: Bearer <token>"{ "id": "mount_550e8400e29b41d4a716446655440000", "backend_id": "backend_660e8400e29b41d4a716446655440000", "mount_path": "/var/lib/hoody/mounts/mount_550e8400e29b41d4a716446655440000", "status": "active", "created_at": 1718300000, "updated_at": 1718300100, "auto_deliver": true, "label": "Photos", "metrics": { "bytes_read": 0, "bytes_written": 0, "cache_hits": 0, "cache_misses": 0, "files_accessed": 0, "last_accessed": 0 }, "pending_uploads": { "count": 0, "bytes": 0 }, "vfs_config": { "cache_mode": "writes", "cache_max_size": 10737418240, "cache_max_age": 3600, "read_chunk_size": 134217728, "write_back": 5, "dir_cache_time": 300, "poll_interval": 60, "read_only": false, "allow_other": false, "allow_non_empty": false }, "uploads": { "state": "idle", "dirty": 0, "open_handles": 0 }}{ "code": "MOUNT_NOT_FOUND", "error": "Mount not found", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
MOUNT_NOT_FOUND | Mount not found | No mount exists with the specified ID. | Verify the mount ID is correct, or list all mounts. |
The response includes a Retry-After header (seconds to wait) when the server’s storage check has not passed.
{ "code": "MOUNT_STORAGE_UNAVAILABLE", "error": "Mounts are unavailable until the server's storage check passes; retry later.", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
MOUNT_STORAGE_UNAVAILABLE | Mounts unavailable | Mounts are unavailable until the server’s storage check passes; retry later. The server’s storage check did not pass (for example, the mount directory and the directory of the backends’ own mounts overlap), so no mount or backend connection can start, and backend file operations are refused. When the check fails on the service’s own directories, the message goes on to name the directory (the state or the run directory), what is wrong with it (for example, a directory above it that is writable by group or others, or owned by another user) and how to correct it. The refusal comes before the mount or backend is looked up, so an unknown id is answered 503, not 404, until the check passes. Existing mounts show the error, and the mount list still answers. Other files are served as usual. Nothing was changed. | Correct the directory the message names, if it names one; then retry after the time given in Retry-After (the check runs again by then). |
Create persistent FUSE mount
Section titled “Create persistent FUSE mount”POST /api/v1/mounts
Create a persistent FUSE filesystem mount for a connected backend. Every mount is persistent and restored on restart. A mount whose saved settings cannot be applied at restart comes back with status error; sending the same settings through PATCH /api/v1/mounts/{id} starts it again.
The vfs_config object sets custom VFS settings from the start, with the same fields and rules as PATCH /api/v1/mounts/{id}; fields you omit take their defaults. Custom settings are supported only on the backend types PATCH /api/v1/mounts/{id} lists; other types are refused with 400. A mount_path inside the server’s internal directories is refused with 400.
A file written through the mount is uploaded to the backend after the upload delay (vfs_config.write_back). When the same file on the backend is also changed in that window, the upload that reaches the backend last wins, even when it carries the older change, and neither side errors. Keep one writer per file on a mounted backend.
Request Body
Section titled “Request Body”| Field | Type | Required | Description |
|---|---|---|---|
backend_id | string | Yes | ID of an existing backend connection. |
mount_path | string | No | Path for the mount. If omitted, the mount gets a new directory of its own in the configured mount directory. Relative paths are resolved under the configured mount directory. |
label | string | No | Human-readable label for this mount (for example My NAS, Work S3, Photos Backup). Used by the UI to identify mounts; can also be used to filter mounts via GET /api/v1/mounts?label=.... |
auto_deliver | boolean | No | Saved with the mount. It currently has no effect: files kept on the server are not uploaded, whatever its value. Changing it does not restart the mount. Default for new mounts: true. |
vfs_config | object | No | Custom VFS settings. Fields you omit take their defaults. See PATCH /api/v1/mounts/{id} for the supported fields and ranges. |
import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.files.mounts.create({ backend_id: 'backend_660e8400e29b41d4a716446655440000', label: 'Photos', vfs_config: { cache_max_age: 3600, cache_max_size: 10737418240, cache_mode: 'writes', dir_cache_time: 300, poll_interval: 60, read_chunk_size: 134217728, read_only: false, write_back: 5 }});curl -X POST 'https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com/api/v1/mounts' \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "backend_id": "backend_660e8400e29b41d4a716446655440000", "label": "Photos", "vfs_config": { "cache_max_age": 3600, "cache_max_size": 10737418240, "cache_mode": "writes", "dir_cache_time": 300, "poll_interval": 60, "read_chunk_size": 134217728, "read_only": false, "write_back": 5 } }'{ "success": true, "message": "Mount created", "data": { "id": "mount_550e8400e29b41d4a716446655440000", "backend_id": "backend_660e8400e29b41d4a716446655440000", "mount_path": "/var/lib/hoody/mounts/mount_550e8400e29b41d4a716446655440000", "status": "mounting", "created_at": 1718300000, "updated_at": 1718300000, "auto_deliver": true, "label": "Photos", "metrics": { "bytes_read": 0, "bytes_written": 0, "cache_hits": 0, "cache_misses": 0, "files_accessed": 0, "last_accessed": 0 }, "pending_uploads": { "count": 0, "bytes": 0 }, "vfs_config": { "cache_mode": "writes", "cache_max_size": 10737418240, "cache_max_age": 3600, "read_chunk_size": 134217728, "write_back": 5, "dir_cache_time": 300, "poll_interval": 60, "read_only": false, "allow_other": false, "allow_non_empty": false }, "uploads": { "state": "unknown", "dirty": null, "open_handles": null } }}{ "code": "VFS_CONFIG_INVALID", "error": "Invalid VFS setting: cache_max_age is out of range", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INVALID_JSON | Invalid JSON | The request body could not be read, or is not a JSON object. | Send a JSON object. |
VFS_FIELD_UNKNOWN | Unknown field | The body or vfs_config has a field that does not exist. | Check the field names; the message lists the valid ones. |
VFS_FIELD_NULL | Null value | A field was sent as null. | Omit the field to keep its value. |
VFS_FIELD_NOT_SETTABLE | Field cannot be set | allow_other or allow_non_empty was sent as true. They are accepted only as false, so a GET response can be sent back unchanged. | Omit the field, or send false. |
VFS_CONFIG_INVALID | Invalid VFS setting | A value has the wrong form or is out of range, or a field is given twice (under both of its names). | Follow the message; each field’s range is in the VfsConfigPatch schema. |
VFS_CONFIG_UNSUPPORTED_BACKEND | Custom settings not supported | The mount’s backend type does not support custom VFS settings. | Keep the default settings for this backend. |
MOUNT_PATH_INVALID | Invalid mount path | mount_path contains a .. component, is not under the configured mount directory, cannot be resolved, or crosses into another mount inside the mount directory. Nothing was created. | Send a path under the mount directory, or omit mount_path. |
MOUNT_PATH_RESERVED | Mount path reserved | mount_path is one of the server’s own internal directories, is inside one, or is a directory above one. | Choose another mount_path, or omit it. |
VFS_PATCH_EMPTY | Empty vfs_config | vfs_config was sent without any field. Nothing was created. | Set at least one vfs_config field, or omit vfs_config for the default settings; the message lists the fields. |
INVALID_PARAMETER | Invalid field | backend_id is missing or not a string, or mount_path, label or auto_deliver has the wrong type. Nothing was created. | Follow the message; each field’s type is in the request schema. |
{ "code": "REMOTE_DISABLED", "error": "Remote mounts are disabled on this server", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
REMOTE_DISABLED | Remote mounts disabled | The server was started without remote backend support. | Start the server with --allow-remote. |
{ "code": "BACKEND_NOT_FOUND", "error": "Backend not found", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
BACKEND_NOT_FOUND | Backend not found | backend_id names no backend. Nothing was created. | List the backends and pass an existing ID. |
The response includes a Retry-After header (seconds to wait) when the answer is MOUNT_BUSY.
{ "code": "MOUNT_PATH_IN_USE", "error": "Another mount uses this mount path", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
MOUNT_BUSY | Mount busy | The create ran out of time before it changed anything: a wait before the mount was committed (for uploads from before a service restart, the backend, its connection or mount start, or the mount path) took longer than a create is given, or files from before a service restart could not all be checked in time. Nothing was created. | Retry after the time given in Retry-After. |
MOUNT_PATH_EXISTS | Mount path exists | Something that is not a mount already exists at the mount path, or appeared there while the mount was being created. Nothing was created. | Choose another mount path, or remove what is there. |
MOUNT_PATH_CHANGED | Mount path changed | A directory on the mount path changed while the mount was being created, for example it was replaced by a symbolic link. Nothing was created. | Check the directories on the mount path, then retry. |
MOUNT_PATH_IN_USE | Mount path in use | Another mount uses this mount path, or a path inside or around it, including a mount in error; or a files request is changing that path right now. Nothing was created. | Choose another mount path, delete the other mount, or retry when the files request has finished. |
MOUNT_UPDATE_IN_PROGRESS | Backend busy | Files from before a service restart are still uploading to this backend. The message says how many files, and the last upload error if any. | Retry after they finish. Nothing was created. |
{ "code": "PAYLOAD_TOO_LARGE", "error": "Request body too large", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PAYLOAD_TOO_LARGE | Request body too large | The request body is larger than 64 KiB. | Send only backend_id, mount_path, label, vfs_config and auto_deliver. |
{ "code": "DEDICATED_VFS_LIMIT", "error": "Custom settings limit reached", "retiring": 0, "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
MOUNT_COUNT_LIMIT | Mount limit reached | Reserved; not currently sent: no answer carries this code. The server runs at most 100 mounts, and that limit is reached. | Delete a mount you no longer need. |
DEDICATED_VFS_LIMIT | Custom settings limit reached | The server runs a limited number of mount connections with custom settings, and that limit is reached. A mount whose settings were changed, or that was removed, keeps its previous connection until the files open on it are closed, and that connection counts too; retiring in the body says how many do. Retrying does not help until one ends. | Set another mount back to the default settings, delete one, or create this mount with the default settings; or close the files still open on changed or removed mounts. |
{ "code": "MOUNT_CREATE_FAILED", "error": "Mount could not be created", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
MOUNT_CREATE_FAILED | Mount could not be created | The mount could not be registered, saved, or linked at its mount path, or the server’s kernel cannot change mount paths safely (Linux 5.6 or later is needed). Nothing was created. | Check the mount path’s parent directory and the server’s storage, then retry. |
{ "code": "MOUNTS_UNSUPPORTED", "error": "Mounts not supported", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
MOUNTS_UNSUPPORTED | Mounts not supported | Reserved; not currently sent: no answer carries this code. The server was built without mount support. | Use a build with mount support. |
{ "code": "MOUNT_START_FAILED", "error": "Mount could not start", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
MOUNT_START_FAILED | Mount could not start | The mount could not start, for example because the backend refused its credentials. Nothing was created. | Test the backend connection, then retry. |
The response includes a Retry-After header (seconds to wait) when the answer is MOUNT_STORAGE_UNAVAILABLE.
{ "code": "MOUNT_NOT_READY", "error": "Mount not ready", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
MOUNT_NOT_READY | Mount not ready | The backend’s mount, or the mount path, did not become ready in time, or the backend’s connection was replaced while its mount was checked, or stopped while the mount was created. Nothing was created. | Retry; if it persists, test the backend connection. |
MOUNT_STORAGE_UNAVAILABLE | Mounts unavailable | Mounts are unavailable until the server’s storage check passes; retry later. The server’s storage check did not pass (for example, the mount directory and the directory of the backends’ own mounts overlap), so no mount or backend connection can start, and backend file operations are refused. When the check fails on the service’s own directories, the message goes on to name the directory (the state or the run directory), what is wrong with it (for example, a directory above it that is writable by group or others, or owned by another user) and how to correct it. The refusal comes before the mount or backend is looked up, so an unknown id is answered 503, not 404, until the check passes. Existing mounts show the error, and the mount list still answers. Other files are served as usual. Nothing was changed. | Correct the directory the message names, if it names one; then retry after the time given in Retry-After (the check runs again by then). |
SERVICE_STOPPING | Service stopping | Reserved; not currently sent: no answer carries this code. The service is stopping, so the request was not carried out. Nothing was changed. | Retry once the service is back. |
{ "code": "PENDING_UPLOADS_LIMIT", "error": "Pending uploads limit reached", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PENDING_UPLOADS_LIMIT | Pending uploads limit reached | The server holds more than 20 GiB of files that were not uploaded, so no mount with custom settings can start. Mounts with the default settings keep working. Retrying alone does not help: delivering or discarding pending uploads lowers the held total. | Create the mount without vfs_config to use the default settings, or lower the held total first by delivering or discarding pending uploads. |
Update mount VFS configuration
Section titled “Update mount VFS configuration”PATCH /api/v1/mounts/{id}
Change the settings of an existing mount: its VFS settings (vfs_config) and its auto_deliver setting. The update is partial: fields you omit keep their current value. The response carries the mount as it is afterwards. A mount listed as error because its saved settings were refused can be repaired with an update.
Every change of vfs_config restarts the mount; a change of auto_deliver alone does not. The mount path switches to the new settings at once, and new opens use them. Files already open keep the previous settings until they are closed. When the previous settings were custom, that connection is kept until it shuts down: at the latest 24 hours after the switch, or sooner if its files cannot be checked for a minute, or its oldest unsent file is 24 hours old and its uploads are failing. A program whose working directory is inside the mount must change into it again after the switch.
Files written and closed before the update finish uploading before the switch, whatever the current settings are. They upload after the current upload delay (write_back; 5 seconds on the default settings). A mount on the default settings shares its upload queue with the backend’s other default-settings mounts, so their closed files are waited for too. Files still open are not waited for. If the uploads have not finished within 10 seconds, the update is refused with 409 and nothing changes. With switch=immediate the update does not wait for them: the new settings serve at once, and those files upload later with the previous settings, so such an upload can replace a newer write made through the new settings; previous_uploads_pending in the answer says how many files were still waiting. If those files cannot all be checked within 10 seconds, the update is refused with 409 MOUNT_BUSY and nothing changes.
read_only applies to the mount path only; the backend file API can still write.
Sending the settings the mount already has changes nothing: the answer is 200 with remounted: false. A mount set back to all the defaults shares its backend’s cache again, so changes made through other mounts or through the backend file API appear at once.
Custom settings are available for these backend types: azureblob, azurefiles, b2, drive, dropbox, fichier, filescom, ftp, gofile, google cloud storage, google photos, hdfs, http, imagekit, internetarchive, koofr, mega, netstorage, opendrive, oracleobjectstorage, pcloud, pixeldrain, qingstor, quatrix, s3, seafile, sftp, sia, smb, swift, webdav. Other backend types answer 400 and keep the default settings.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The mount id. |
switch | query | string | No | wait (the default): files closed before the update finish uploading before the switch. immediate: switch at once; those files upload later with the previous settings, and such an upload can replace a newer write made through the new settings. If those files cannot all be checked within 10 seconds, the update answers 409 MOUNT_BUSY and changes nothing. Allowed values: wait, immediate. |
Request Body
Section titled “Request Body”| Field | Type | Required | Description |
|---|---|---|---|
auto_deliver | boolean | No | Saved with the mount. It currently has no effect: files kept on the server are not uploaded, whatever its value. Changing it does not restart the mount. Default for new mounts: true. |
vfs_config | object | No | VFS settings to change. Each field you send replaces the mount’s current value; fields you omit keep it. Send at least one field across auto_deliver and vfs_config or the answer is 400 VFS_PATCH_EMPTY. |
import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.files.mounts.update( 'mount_550e8400e29b41d4a716446655440000', { vfs_config: { cache_max_age: 7200, write_back: 10 } }, { switch: 'wait' });curl -X PATCH 'https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com/api/v1/mounts/mount_550e8400e29b41d4a716446655440000?switch=wait' \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "vfs_config": { "cache_max_age": 7200, "write_back": 10 } }'{ "success": true, "message": "Mount updated", "remounted": true, "data": { "id": "mount_550e8400e29b41d4a716446655440000", "backend_id": "backend_660e8400e29b41d4a716446655440000", "mount_path": "/var/lib/hoody/mounts/mount_550e8400e29b41d4a716446655440000", "status": "active", "created_at": 1718300000, "updated_at": 1718300200, "auto_deliver": true, "label": "Photos", "metrics": { "bytes_read": 0, "bytes_written": 0, "cache_hits": 0, "cache_misses": 0, "files_accessed": 0, "last_accessed": 0 }, "pending_uploads": { "count": 0, "bytes": 0 }, "vfs_config": { "cache_mode": "writes", "cache_max_size": 10737418240, "cache_max_age": 7200, "read_chunk_size": 134217728, "write_back": 10, "dir_cache_time": 300, "poll_interval": 60, "read_only": false, "allow_other": false, "allow_non_empty": false }, "uploads": { "state": "idle", "dirty": 0, "open_handles": 0 } }}{ "code": "VFS_PATCH_EMPTY", "error": "Nothing to change", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
VFS_PATCH_EMPTY | Nothing to change | The body sets neither auto_deliver nor any field of vfs_config. | Send auto_deliver, or at least one vfs_config field; the message lists them. |
INVALID_JSON | Invalid JSON | The request body could not be read, or is not a JSON object. | Send a JSON object. |
VFS_FIELD_UNKNOWN | Unknown field | The body or vfs_config has a field that does not exist. | Check the field names; the message lists the valid ones. |
VFS_FIELD_NULL | Null value | A field was sent as null. | Omit the field to keep its value. |
VFS_FIELD_NOT_SETTABLE | Field cannot be set | allow_other or allow_non_empty was sent as true. They are accepted only as false, so a GET response can be sent back unchanged. | Omit the field, or send false. |
VFS_CONFIG_INVALID | Invalid VFS setting | A value has the wrong form or is out of range, or a field is given twice (under both of its names). | Follow the message; each field’s range is in the VfsConfigPatch schema. |
VFS_CONFIG_UNSUPPORTED_BACKEND | Custom settings not supported | The mount’s backend type does not support custom VFS settings. | Keep the default settings for this backend. |
MOUNT_PATH_INVALID | Invalid mount path | The saved mount_path of a mount restored in error no longer resolves under the mount directory (a .. component, outside it, or unresolvable), so the mount cannot be revived there. Nothing was changed. | Delete the mount and create it again at a path under the mount directory. |
MOUNT_PATH_RESERVED | Mount path reserved | This mount’s path is reserved by the service, so the mount cannot be served. Nothing was changed. | Delete the mount and create it again at another path. |
INVALID_PARAMETER | Invalid switch | switch is neither wait nor immediate. | Omit switch, or send wait or immediate. |
{ "code": "REMOTE_DISABLED", "error": "Remote mounts are disabled on this server", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
REMOTE_DISABLED | Remote mounts disabled | The server was started without remote backend support. | Start the server with --allow-remote. |
{ "code": "MOUNT_NOT_FOUND", "error": "Mount not found", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
MOUNT_NOT_FOUND | Mount not found | No mount exists with the specified ID. | Verify the mount ID is correct, or list all mounts. |
The response includes a Retry-After header (seconds to wait) when the answer is MOUNT_BUSY.
{ "code": "MOUNT_BUSY", "error": "Mount busy", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
MOUNT_BUSY | Mount busy | Nothing was changed. With Retry-After: its start, other changes to mounts of the same backend, or a change to the mount meanwhile did not leave this update enough time, or files from before a service restart could not all be checked in time, or, with switch=immediate, the files still waiting to upload could not. Without Retry-After: the mount is being removed, so it is going away; once the removal finishes, an update answers 404 MOUNT_NOT_FOUND. | With Retry-After, retry after the time it gives. Without it, do not retry: create the mount again to keep it. |
MOUNT_UPDATE_IN_PROGRESS | Update in progress | Files written through the mount have not finished uploading within 10 seconds, or files from before a service restart are still uploading to this backend. The message says which: files waiting for the upload delay (retry after it, or update with switch=immediate), files that cannot be uploaded (fix the backend, or delete the mount), or files from before the restart, with how many files and the last upload error (retry after they finish). | Follow the message: retry after the stated delay, fix the backend, or, for uploads from before a service restart that are still pending, retry later. Nothing was changed. |
MOUNT_BACKEND_UNAVAILABLE | Backend unavailable | The mount’s backend is not connected. | Reconnect the backend, then retry. |
MOUNT_PATH_CHANGED | Mount path changed | Something other than this mount now occupies its mount path. | Check the mount path; delete and recreate the mount if needed. |
MOUNT_PATH_IN_USE | Mount path in use | Another mount now holds the saved mount_path of this mount restored in error, so it cannot be revived there. Nothing was changed. | Delete one of the two mounts, or create this one again at another path. |
{ "code": "PAYLOAD_TOO_LARGE", "error": "Request body too large", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PAYLOAD_TOO_LARGE | Request body too large | The request body is larger than 64 KiB. | Send only the fields to change. |
{ "code": "DEDICATED_VFS_LIMIT", "error": "Custom settings limit reached", "retiring": 0, "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
DEDICATED_VFS_LIMIT | Custom settings limit reached | The server runs a limited number of mount connections with custom settings, and that limit is reached. A mount whose settings were changed, or that was removed, keeps its previous connection until the files open on it are closed, and that connection counts too; retiring in the body says how many do. Retrying does not help until one ends. | Set another mount back to the default settings, delete one, or keep this mount on the defaults; or close the files still open on changed or removed mounts. |
{ "code": "MOUNT_PERSIST_FAILED", "error": "Settings could not be saved", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
MOUNT_PERSIST_FAILED | Settings could not be saved | The new settings could not be saved. The mount keeps its previous settings. | Check the server’s storage, then retry. |
MOUNT_SWAP_FAILED | Mount path could not be switched | The new settings were saved, but the mount path could not be switched to them. The previous settings keep serving; the mount shows the new settings with status error until a repeated update or a service restart switches them in. | Check the mount path’s parent directory, then repeat the update. |
{ "code": "MOUNTS_UNSUPPORTED", "error": "Mounts not supported", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
MOUNTS_UNSUPPORTED | Mounts not supported | Reserved; not currently sent: no answer carries this code. The server was built without mount support. | Use a build with mount support. |
{ "code": "MOUNT_REMOUNT_FAILED", "error": "Remount failed", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
MOUNT_REMOUNT_FAILED | Remount failed | The mount could not start with the new settings, for example because the backend refused its credentials. The mount keeps its previous settings. | Test the backend connection, then retry. |
The response includes a Retry-After header (seconds to wait) when the answer is MOUNT_STORAGE_UNAVAILABLE.
{ "code": "MOUNT_STORAGE_UNAVAILABLE", "error": "Mounts are unavailable until the server's storage check passes; retry later.", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
MOUNT_STORAGE_UNAVAILABLE | Mounts unavailable | Mounts are unavailable until the server’s storage check passes; retry later. The server’s storage check did not pass (for example, the mount directory and the directory of the backends’ own mounts overlap), so no mount or backend connection can start, and backend file operations are refused. When the check fails on the service’s own directories, the message goes on to name the directory (the state or the run directory), what is wrong with it (for example, a directory above it that is writable by group or others, or owned by another user) and how to correct it. The refusal comes before the mount or backend is looked up, so an unknown id is answered 503, not 404, until the check passes. Existing mounts show the error, and the mount list still answers. Other files are served as usual. Nothing was changed. | Correct the directory the message names, if it names one; then retry after the time given in Retry-After (the check runs again by then). |
SERVICE_STOPPING | Service stopping | Reserved; not currently sent: no answer carries this code. The service is stopping, so the request was not carried out. Nothing was changed. | Retry once the service is back. |
{ "code": "PENDING_UPLOADS_LIMIT", "error": "Pending uploads limit reached", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PENDING_UPLOADS_LIMIT | Pending uploads limit reached | The server holds more than 20 GiB of files that were not uploaded, so no mount with custom settings can start. Mounts with the default settings keep working. Retrying alone does not help: delivering or discarding pending uploads lowers the held total. | Keep this mount’s current settings, or set it back to the defaults, or lower the held total first by delivering or discarding pending uploads. |
Unmount filesystem
Section titled “Unmount filesystem”DELETE /api/v1/mounts/{id}
Remove a mount and disconnect the FUSE filesystem. A mount that is still mounting is removed at once. The uploads query parameter says what becomes of what was written on the mount and is not uploaded yet:
keep(the default): a mount with avfs_configof its own keeps uploading it in the background after the answer, and files still open on the mount keep working until they are closed. That goes on until nothing is held open and nothing is left to upload, for at most 24 hours; what is left then is kept on the server and is not uploaded. A mount served by its backend’s shared connection, which no other mount uses and which holds files not uploaded yet or open: the answer waits while they upload, at most about 35 seconds (longer with a largerwrite_back), and what is left then is kept on the server as a pending upload, listed before the answer and named inuploads.pending_upload_id. When another mount uses that connection, or a request to the backend uses it meanwhile, the files keep uploading through it, anduploadssays they are not delivered yet.wait: the same, and the answer waits up towait_seconds(1 to 600, default 120) until nothing is held open and nothing is left to upload.uploads.deliveredis then true. Otherwise it is false, with what was still held open and not uploaded (both null when uploads of the mount had already stopped with something left, now or before the DELETE, which is kept on the server), and the uploads go on as withkeep; so they do when the client goes away while it waits. On a mount served by its backend’s shared connection it waits for the connection, which the backend’s other mounts write through too.discard: what is not uploaded yet is deleted now and is never uploaded, and so are the mount’s uploads already kept on the server; files still open on the mount stop working, anduploads.discardedsays how many files that was. On a mount that is already removed (by an earlier DELETE, one still waiting included) it is not refused: it deletes what the mount left in the same way, and answers 200 withoutmount_path. Refused for a mount served by its backend’s shared connection (409UPLOADS_DISCARD_SHARED).
Servers that take uploads list mount_delete_uploads in the features of GET /api/v1/version; an older server ignores it.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The mount id. |
uploads | query | string | No | What becomes of what was written on the mount and is not uploaded yet: keep uploading it (keep), the same and wait for it (wait), or delete it (discard). Allowed values: keep, wait, discard. |
wait_seconds | query | integer | No | With uploads=wait only: the longest the answer waits, in seconds. |
import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.files.mounts.delete('mount_550e8400e29b41d4a716446655440000', { uploads: 'wait', wait_seconds: 120});curl -X DELETE 'https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com/api/v1/mounts/mount_550e8400e29b41d4a716446655440000?uploads=wait&wait_seconds=120' \ -H "Authorization: Bearer <token>"{ "success": true, "message": "Mount removed", "data": { "id": "mount_550e8400e29b41d4a716446655440000", "mount_path": "/var/lib/hoody/mounts/mount_550e8400e29b41d4a716446655440000", "removed": true, "uploads": { "delivered": true } }}{ "code": "INVALID_PARAMETER", "error": "Invalid parameter", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INVALID_PARAMETER | Invalid parameter | uploads is not keep, wait or discard, wait_seconds is not a whole number from 1 to 600, wait_seconds is given without uploads=wait, or either is given twice. Nothing was removed. | Send uploads=keep, wait or discard, and wait_seconds only with uploads=wait. |
This response is never sent with uploads=discard.
{ "code": "MOUNT_NOT_FOUND", "error": "Mount not found", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
MOUNT_NOT_FOUND | Mount not found | No mount exists with the specified ID. | Verify the mount ID is correct, or list all mounts. |
{ "code": "UPLOADS_DISCARD_SHARED", "error": "Nothing of this mount alone can be discarded", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
MOUNT_PATH_CHANGED | Mount path changed | A directory on the mount path moved while the mount was being removed, so its removal was taken back. The mount, its record and its settings are kept. | Retry. |
MOUNT_PATH_IN_USE | Mount path in use | A files request is changing a path at or around the mount path right now. Nothing was removed. | Retry when the files request has finished. |
UPLOADS_DISCARD_SHARED | Nothing of this mount alone can be discarded | The mount has no vfs_config of its own: its backend’s shared connection serves it, and the backend’s other mounts write through that connection too. Nothing was removed. | Remove it with uploads=wait or keep, or give it a vfs_config of its own first. |
{ "code": "INVALID_PARAMETER", "error": "A mount's path cannot be removed safely right now", "success": false}The response includes a Retry-After header (seconds to wait) when the answer is MOUNT_STORAGE_UNAVAILABLE.
{ "code": "MOUNT_STORAGE_UNAVAILABLE", "error": "Mounts are unavailable until the server's storage check passes; retry later.", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
MOUNT_STORAGE_UNAVAILABLE | Mounts unavailable | Mounts are unavailable until the server’s storage check passes; retry later. The server’s storage check did not pass (for example, the mount directory and the directory of the backends’ own mounts overlap), so no mount or backend connection can start, and backend file operations are refused. When the check fails on the service’s own directories, the message goes on to name the directory (the state or the run directory), what is wrong with it (for example, a directory above it that is writable by group or others, or owned by another user) and how to correct it. The refusal comes before the mount or backend is looked up, so an unknown id is answered 503, not 404, until the check passes. Existing mounts show the error, and the mount list still answers. Other files are served as usual. Nothing was changed. | Correct the directory the message names, if it names one; then retry after the time given in Retry-After (the check runs again by then). |
SERVICE_STOPPING | Service stopping | Reserved; not currently sent: no answer carries this code. The service is stopping, so the request was not carried out. Nothing was changed. | Retry once the service is back. |
Pending uploads
Section titled “Pending uploads”List pending uploads
Section titled “List pending uploads”GET /api/v1/pending-uploads
List unsent upload data. An item in state uploading holds files that are still being uploaded on their own, by uploads that kept running through a service restart or that belong to a mount that was removed or changed; stop it to hold them instead. An item in state pending holds files whose uploads stopped before they were sent (after a crash, a reboot or shutdown of the machine or container, or a stop request): they may be incomplete or out of date. Each file keeps the reason it was held. Each item groups the files of one mount session, with a count per reason; list its files with GET /api/v1/pending-uploads/{id}/files. Nothing here expires: an item stays until every file is delivered, or it is discarded, so an item can show a delivery that covered only part of its files.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
backend_id | query | string | No | Only items of this backend are returned. |
import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.files.uploads.list({ backend_id: 'backend_660e8400e29b41d4a716446655440000' });curl -X GET 'https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com/api/v1/pending-uploads?backend_id=backend_660e8400e29b41d4a716446655440000' \ -H "Authorization: Bearer <token>"{ "success": true, "data": [ { "id": "pu_770e8400e29b41d4a716446655440000", "state": "pending", "since": 1718300000, "file_count": 3, "incomplete_count": 0, "total_bytes": 1048576, "deliverable": false, "reasons": { "open_when_stopped": 0, "unconfirmed_at_stop": 0, "metadata_unreadable": 0, "upload_failing": 0, "stopped_by_request": 3, "changed_on_storage": 0, "check_failed": 0, "cannot_verify": 0 } } ]}{ "code": "INVALID_PARAMETER", "error": "Invalid request", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INVALID_PARAMETER | Invalid request | A query parameter, a header or the request body is not one this operation accepts, or a copy or a move names a destination inside the directory it copies or moves; the message names what was refused. | Correct the request. |
{ "code": "REMOTE_DISABLED", "error": "Remote mounts are disabled on this server", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
REMOTE_DISABLED | Remote mounts disabled | The server was started without remote backend support. | Start the server with --allow-remote. |
List a pending upload’s files
Section titled “List a pending upload’s files”GET /api/v1/pending-uploads/{id}/files
List the files of a pending upload, one page at a time, sorted by their raw path bytes. Each file is identified by path_b64; path is present only when the name is valid UTF-8. A page holds at most limit files. When more follow, the answer carries next_cursor: pass it as cursor to get the next page, and stop when it is absent. Paging stays correct while files leave the item during a delivery: a file that left is not listed, and no remaining file is skipped. limit is 1 to 1000, 1000 by default. complete says whether a file can be delivered.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | Pending upload ID. |
cursor | query | string | No | next_cursor from the previous page. Omit it for the first page. |
limit | query | integer | No | Largest number of files on the page. Default: 1000. |
import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.files.uploads.listFiles('pu_770e8400e29b41d4a716446655440000', { limit: 500 });curl -X GET 'https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com/api/v1/pending-uploads/pu_770e8400e29b41d4a716446655440000/files?limit=500' \ -H "Authorization: Bearer <token>"{ "success": true, "data": { "files": [ { "path": "Photos/2024/January/IMG_0001.jpg", "path_b64": "UGhvdG9zLzIwMjQvSmFudWFyeS9JTUdfMDAwMS5qcGc", "size": 524288, "modified_at": 1718300000, "reason": "stopped_by_request", "complete": true } ], "next_cursor": "eyJwYXRoIjoiUGhvdG9zLzIwMjQvSmFudWFyeS9JTUdfMDAwMi5qcGcifQ" }}{ "code": "INVALID_PARAMETER", "error": "Invalid parameter", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INVALID_PARAMETER | Invalid parameter | limit is not an integer from 1 to 1000, or cursor is not one this operation returned. | Omit cursor to start again from the first page. |
{ "code": "REMOTE_DISABLED", "error": "Remote mounts are disabled on this server", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
REMOTE_DISABLED | Remote mounts disabled | The server was started without remote backend support. | Start the server with --allow-remote. |
{ "code": "PENDING_UPLOAD_NOT_FOUND", "error": "Pending upload not found", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PENDING_UPLOAD_NOT_FOUND | Pending upload not found | No pending upload exists with the specified ID. An item whose files were all delivered or uploaded, or that was discarded, is gone. | List the pending uploads to get a current ID. |
Download a held file
Section titled “Download a held file”GET /api/v1/pending-uploads/{id}/file
Download the held copy of one file of a pending upload: the bytes the cache held when the file was held back. They may be incomplete; in a file with complete: false, missing ranges read as zeros. Name the file with exactly one of path (a listed path) or path_b64 (a listed path_b64, which every file has), as listed by GET /api/v1/pending-uploads/{id}/files; a name that is not one of the item’s files, including any path outside the item, is 404. This is the only way to recover an incomplete file, which delivery skips. Files stay downloadable while a delivery runs. An item in state uploading answers 409.
The answer uses application/octet-stream for a single range and multipart/byteranges when several ranges are satisfiable.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | Pending upload ID. |
path | query | string | No | A path from the item’s files, exactly as listed. Give this or path_b64, not both. |
path_b64 | query | string | No | A path_b64 from the item’s files, exactly as listed. Give this or path, not both. |
Range | header | string | No | Ask for part of the held file, as bytes=first-last, bytes=first- or bytes=-suffix_length, as for a file download. One satisfiable range answers 206 with Content-Range; several answer 206 as multipart/byteranges. Ranges that cannot be satisfied are dropped from a list, and 416 comes only when none is left. A malformed header is ignored (200, whole file). |
If-Range | header | string | No | Honour Range only if the held file still has this ETag, exactly; otherwise answer 200 with the whole file. |
import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.files.uploads.download('pu_770e8400e29b41d4a716446655440000', { path: 'Photos/2024/January/IMG_0001.jpg', Range: 'bytes=0-1023'});curl -X GET 'https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com/api/v1/pending-uploads/pu_770e8400e29b41d4a716446655440000/file?path=Photos%2F2024%2FJanuary%2FIMG_0001.jpg' \ -H "Authorization: Bearer <token>" \ -H "Range: bytes=0-1023" \ --output IMG_0001.jpgThe body is application/octet-stream: the raw bytes of the held file, which may be incomplete. The response carries Accept-Ranges: bytes, Content-Length, ETag, Last-Modified, and Content-Disposition: attachment; filename="<base name>".
The body is application/octet-stream (single range, Content-Range header carries the range) or multipart/byteranges (multiple ranges, each part with its own Content-Range).
The response carries no body: If-None-Match names the held copy’s ETag, or, without If-None-Match, the held copy is unchanged since If-Modified-Since.
{ "code": "INVALID_PATH", "error": "Invalid path", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INVALID_PATH | Invalid path | path is empty, not UTF-8, or contains a NUL byte. | Pass one of the item’s listed paths, or its path_b64. |
INVALID_PARAMETER | Invalid parameter | Both or neither of path and path_b64 were given, or path_b64 is not base64url without padding of a path. | Pass exactly one of path and path_b64, as listed. |
{ "code": "REMOTE_DISABLED", "error": "Remote mounts are disabled on this server", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
REMOTE_DISABLED | Remote mounts disabled | The server was started without remote backend support. | Start the server with --allow-remote. |
{ "code": "PENDING_UPLOAD_FILE_NOT_FOUND", "error": "Held file not found", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PENDING_UPLOAD_NOT_FOUND | Pending upload not found | No pending upload exists with the specified ID. An item whose files were all delivered or uploaded, or that was discarded, is gone. | List the pending uploads to get a current ID. |
PENDING_UPLOAD_FILE_NOT_FOUND | Held file not found | path or path_b64 is not one of the item’s files. | List the item’s files and pass a listed path or path_b64 exactly. |
{ "code": "PENDING_UPLOAD_BUSY", "error": "Pending upload busy", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PENDING_UPLOAD_BUSY | Pending upload busy | The item’s files are still uploading on their own (state uploading). | Wait for its uploads to finish, or stop it first. |
If-Match names no current ETag of the held copy, or, without If-Match, the held copy changed after If-Unmodified-Since.
No range in Range overlaps the held file, or several ranges add up to more than 64 MiB. The response carries Content-Range: bytes */<size>, the held file’s size.
Deliver a pending upload
Section titled “Deliver a pending upload”POST /api/v1/pending-uploads/{id}/deliver
Upload held files of a pending upload to a backend. This overwrites newer versions of the same files on the backend, including changes made since the files were held back, so confirm with the owner of the data first, or download the files and compare. Without paths or paths_b64, every complete file is delivered; with them, only the files they name: paths takes listed paths, paths_b64 listed path_b64 values (the only way to name a file whose name is not valid UTF-8), and the two can be combined. A file named twice, or a name that is not one of the item’s files, is refused with 400, and an incomplete file (complete: false) with 409; in both cases nothing starts. Incomplete files can only be downloaded or discarded.
Without backend_id the item’s saved connection settings are used; with backend_id, that backend’s current settings are, and they become the item’s saved settings once the upload starts (a delivery that fails before keeps the saved settings as they were; one cut short by a restart or an error while it switched them leaves them unsettled, listed in unsettled_backend_ids, and a delivery without backend_id is then refused with 409 until a delivery with backend_id switches them again); the files land at the same paths under it. While a delivery runs, the backend it uploads to cannot be updated, and deleting that backend with uploads=discard stops the delivery. The answer is 202 with the item in state delivering. Each file leaves the item as its upload lands. When the delivery ends, the item returns to pending with the files that were not delivered, and last_error says why when it ended early; the item disappears once no file is left, or when it is discarded. A delivery interrupted by a service restart, a crash, or a reboot or shutdown of the machine or container returns the item to pending with its undelivered files; deliver it again.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | Pending upload ID. |
Request Body
Section titled “Request Body”| Field | Type | Required | Description |
|---|---|---|---|
backend_id | string | No | Deliver with this backend’s current connection settings instead of the item’s saved ones. Use it when deliverable is false because the saved settings are no longer accepted, or to deliver to another backend. |
paths | array of string | No | Paths from the item’s files, exactly as listed. Omit this and paths_b64 to deliver every complete file. |
paths_b64 | array of string | No | path_b64 values from the item’s files, exactly as listed. Combined with paths. |
import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.files.uploads.deliver('pu_770e8400e29b41d4a716446655440000', { paths: ['Photos/2024/January/IMG_0001.jpg']});curl -X POST 'https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com/api/v1/pending-uploads/pu_770e8400e29b41d4a716446655440000/deliver' \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "paths": ["Photos/2024/January/IMG_0001.jpg"] }'{ "success": true, "data": { "id": "pu_770e8400e29b41d4a716446655440000", "state": "delivering", "since": 1718300000, "file_count": 3, "incomplete_count": 0, "total_bytes": 1048576, "deliverable": true, "reasons": { "open_when_stopped": 0, "unconfirmed_at_stop": 0, "metadata_unreadable": 0, "upload_failing": 0, "stopped_by_request": 3, "changed_on_storage": 0, "check_failed": 0, "cannot_verify": 0 } }}{ "code": "INVALID_PATH", "error": "Path not held", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INVALID_JSON | Invalid JSON | The request body is not a JSON object with only non-empty paths and paths_b64 arrays of strings and a backend_id string. | Send {"paths": [...], "paths_b64": [...], "backend_id": "..."} with any field left out, or no body. |
INVALID_PATH | Path not held | A name in paths or paths_b64 is not one of the item’s files. Nothing was delivered. | List the item’s files and pass paths or path_b64 values exactly as listed. |
INVALID_PARAMETER | Invalid file list | A file is named twice, within or across paths and paths_b64, or a paths_b64 entry is not base64url without padding of a path. Nothing was delivered. | Name each file once, with its listed path or path_b64. |
{ "code": "REMOTE_DISABLED", "error": "Remote mounts are disabled on this server", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
REMOTE_DISABLED | Remote mounts disabled | The server was started without remote backend support. | Start the server with --allow-remote. |
{ "code": "PENDING_UPLOAD_NOT_FOUND", "error": "Pending upload not found", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PENDING_UPLOAD_NOT_FOUND | Pending upload not found | No pending upload exists with the specified ID. An item whose files were all delivered or uploaded, or that was discarded, is gone. | List the pending uploads to get a current ID. |
BACKEND_NOT_FOUND | Backend not found | backend_id names no backend. | List the backends and pass an existing ID. |
{ "code": "PENDING_UPLOAD_BUSY", "error": "Pending upload busy", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PENDING_UPLOAD_BUSY | Pending upload busy | The item is being delivered, or its files are still uploading on their own (state uploading). | Wait for the running delivery to end; stop an uploading item first, or wait for its uploads to finish. |
PENDING_UPLOAD_NOT_DELIVERABLE | Cannot be delivered now | Without backend_id: the saved connection settings are no longer accepted (deliverable is false). With backend_id: that backend’s current settings are not accepted for a delivery, for example its type or its host. | Deliver with backend_id to use a backend’s current settings, or download the files. |
PENDING_UPLOAD_FILE_INCOMPLETE | File incomplete | A file named in paths or paths_b64 has complete: false, which cannot be delivered. Nothing was delivered. | Leave incomplete files out of paths; download or discard them. |
{ "code": "PAYLOAD_TOO_LARGE", "error": "Request body too large", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PAYLOAD_TOO_LARGE | Request body too large | The request body is larger than 4 MiB. | Deliver the files in several requests, or send no body to deliver every complete file. |
{ "code": "DEDICATED_VFS_LIMIT", "error": "Custom settings limit reached", "retiring": 0, "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
DEDICATED_VFS_LIMIT | Custom settings limit reached | A delivery counts against the server’s limit of mount connections with custom settings, and that limit is reached. Nothing was changed. A mount whose settings were changed, or that was removed, keeps its previous connection until the files open on it are closed, and that connection counts too; retiring in the body says how many do. Retrying does not help until one ends. | Retry after another delivery ends, or after a mount with custom settings is removed or set back to the defaults; or close the files still open on changed or removed mounts. |
{ "code": "PENDING_UPLOAD_PERSIST_FAILED", "error": "Delivery not started", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PENDING_UPLOAD_PERSIST_FAILED | Delivery not started | The delivery could not be saved safely, so it was not started. Nothing was copied or uploaded. | Retry; if it keeps failing, check the server’s disk. |
{ "code": "SERVICE_STOPPING", "error": "Service stopping", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
SERVICE_STOPPING | Service stopping | Reserved; not currently sent: no answer carries this code. The service is stopping, so the request was not carried out. Nothing was changed. | Retry once the service is back. |
Stop a running upload
Section titled “Stop a running upload”POST /api/v1/pending-uploads/{id}/stop
Stop the uploads of an item in state uploading (files from before a service restart, or of a mount that was removed or changed, that are still being uploaded on their own, or still open), or a delivery of an item in state delivering. Their unsent files are kept as a pending upload in state pending, with reason stopped_by_request (or open_when_stopped for a file that was still open), to deliver, download or discard. Nothing is lost. Files still open on a removed or changed mount stop working. Use it when those uploads hold up a mount, a mount update or a backend change, or keep failing. The answer is 200 with the item.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | Pending upload ID. |
import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.files.uploads.stop('pu_770e8400e29b41d4a716446655440000');curl -X POST 'https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com/api/v1/pending-uploads/pu_770e8400e29b41d4a716446655440000/stop' \ -H "Authorization: Bearer <token>"{ "success": true, "data": { "id": "pu_770e8400e29b41d4a716446655440000", "state": "pending", "since": 1718300100, "file_count": 3, "incomplete_count": 0, "total_bytes": 1048576, "deliverable": true, "reasons": { "open_when_stopped": 0, "unconfirmed_at_stop": 0, "metadata_unreadable": 0, "upload_failing": 0, "stopped_by_request": 3, "changed_on_storage": 0, "check_failed": 0, "cannot_verify": 0 } }}{ "code": "REMOTE_DISABLED", "error": "Remote mounts are disabled on this server", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
REMOTE_DISABLED | Remote mounts disabled | The server was started without remote backend support. | Start the server with --allow-remote. |
{ "code": "PENDING_UPLOAD_NOT_FOUND", "error": "Pending upload not found", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PENDING_UPLOAD_NOT_FOUND | Pending upload not found | No pending upload exists with the specified ID. An item whose files were all delivered or uploaded, or that was discarded, is gone. | List the pending uploads to get a current ID. |
{ "code": "PENDING_UPLOAD_BUSY", "error": "Pending upload busy", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PENDING_UPLOAD_BUSY | Pending upload busy | The item is in state pending, so there is nothing to stop. | List the pending uploads; a pending item can be delivered, downloaded or discarded. |
{ "code": "PENDING_UPLOAD_PERSIST_FAILED", "error": "Stop not recorded", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PENDING_UPLOAD_PERSIST_FAILED | Stop not recorded | The uploads stopped, but their unsent files could not be saved as a pending upload. Nothing was deleted. | Retry; if it keeps failing, check the server’s disk. |
Discard a pending upload
Section titled “Discard a pending upload”DELETE /api/v1/pending-uploads/{id}
Delete every held file of a pending upload. They are the only copy of those changes, and this cannot be undone. Nothing on the backend is changed. The answer is 204. An item that is being delivered, or is still uploading, cannot be discarded until that ends; an uploading item can be stopped first.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | Pending upload ID. |
import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.files.uploads.delete('pu_770e8400e29b41d4a716446655440000');curl -X DELETE 'https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com/api/v1/pending-uploads/pu_770e8400e29b41d4a716446655440000' \ -H "Authorization: Bearer <token>"The response carries no body: the held files were discarded.
{ "code": "REMOTE_DISABLED", "error": "Remote mounts are disabled on this server", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
REMOTE_DISABLED | Remote mounts disabled | The server was started without remote backend support. | Start the server with --allow-remote. |
{ "code": "PENDING_UPLOAD_NOT_FOUND", "error": "Pending upload not found", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PENDING_UPLOAD_NOT_FOUND | Pending upload not found | No pending upload exists with the specified ID. An item whose files were all delivered or uploaded, or that was discarded, is gone. | List the pending uploads to get a current ID. |
{ "code": "PENDING_UPLOAD_BUSY", "error": "Pending upload busy", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PENDING_UPLOAD_BUSY | Pending upload busy | The item is being delivered, or its files are still uploading on their own (state uploading). | Wait for the delivery to end; stop an uploading item first, or wait for its uploads to finish. |
{ "code": "PENDING_UPLOAD_PERSIST_FAILED", "error": "Not discarded", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PENDING_UPLOAD_PERSIST_FAILED | Not discarded | The held files could not all be deleted, so the pending upload is kept with what is left. | Retry; if it keeps failing, check the server’s disk. |
Unreadable pending uploads
Section titled “Unreadable pending uploads”List unreadable pending uploads
Section titled “List unreadable pending uploads”GET /api/v1/pending-uploads/unreadable
List the stored records of unsent files that cannot be read, so what they hold is unknown. They are not listed by GET /api/v1/pending-uploads. While one exists, every mount DELETE with uploads=wait answers delivered: false, and every mount or backend DELETE with uploads=discard answers discarded: null (a backend is then kept), because none can confirm that nothing was left unsent. A backend DELETE does not take wait; to wait for delivery, remove its mounts with uploads=wait first. A record that could not be read only now may read again later; one listed as damaged will not. Delete one with DELETE /api/v1/pending-uploads/unreadable/{id}.
This endpoint takes no parameters.
import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.files.uploads.listUnreadable();curl -X GET 'https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com/api/v1/pending-uploads/unreadable' \ -H "Authorization: Bearer <token>"{ "success": true, "data": [ { "id": "880e8400e29b41d4a716446655440000", "reason": "unreadable", "size": 1048576 } ]}{ "code": "INVALID_PARAMETER", "error": "Invalid request", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INVALID_PARAMETER | Invalid request | A query parameter, a header or the request body is not one this operation accepts, or a copy or a move names a destination inside the directory it copies or moves; the message names what was refused. | Correct the request. |
{ "code": "REMOTE_DISABLED", "error": "Remote mounts are disabled on this server", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
REMOTE_DISABLED | Remote mounts disabled | The server was started without remote backend support. | Start the server with --allow-remote. |
Delete an unreadable pending upload
Section titled “Delete an unreadable pending upload”DELETE /api/v1/pending-uploads/unreadable/{id}
Delete a record listed by GET /api/v1/pending-uploads/unreadable, with every file it kept on the server, the staged copies of its deliveries included. What it held cannot be known: those may be the only copy of changes that never reached the backend, and this cannot be undone. Nothing on the backend is changed. The answer is 204; afterwards a mount or backend DELETE can confirm its uploads again. Only a record that still cannot be read is deleted here: one that reads again is a pending upload, discarded with DELETE /api/v1/pending-uploads/{id}.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | Pending upload ID. |
import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.files.uploads.deleteUnreadable('880e8400e29b41d4a716446655440000');curl -X DELETE 'https://{projectId}-{containerId}-files-1.{server}.containers.hoody.com/api/v1/pending-uploads/unreadable/880e8400e29b41d4a716446655440000' \ -H "Authorization: Bearer <token>"The response carries no body: the unreadable record and the files it kept were deleted.
{ "code": "REMOTE_DISABLED", "error": "Remote mounts are disabled on this server", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
REMOTE_DISABLED | Remote mounts disabled | The server was started without remote backend support. | Start the server with --allow-remote. |
{ "code": "PENDING_UPLOAD_NOT_FOUND", "error": "Pending upload not found", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PENDING_UPLOAD_NOT_FOUND | Pending upload not found | No unreadable record has this ID: it was deleted, it reads again (then discard it as a pending upload), or it never existed. | List the unreadable records to get a current ID. |
{ "code": "PENDING_UPLOAD_BUSY", "error": "Pending upload busy", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PENDING_UPLOAD_BUSY | Pending upload busy | An upload process that is still running uses this record or one of its deliveries, one an earlier run of the service started included, so it is not deleted. | Retry once that process has ended. |
{ "code": "PENDING_UPLOAD_PERSIST_FAILED", "error": "Not deleted", "success": false}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PENDING_UPLOAD_PERSIST_FAILED | Not deleted | The files the record kept could not all be deleted, or their deletion could not be made durable. Its manifest is deleted last: while it stays, the record is still listed and the DELETE can be repeated; once it is gone, a repeat makes its deletion durable before it answers 404. | Retry; if it keeps failing, check the server’s disk. |