Skip to content
Hoody.com

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.

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).

NameInTypeRequiredDescription
labelquerystringNoOnly 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' });

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.

NameInTypeRequiredDescription
idpathstringYesThe 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');

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.

FieldTypeRequiredDescription
backend_idstringYesID of an existing backend connection.
mount_pathstringNoPath 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.
labelstringNoHuman-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_deliverbooleanNoSaved 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_configobjectNoCustom 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
}
});

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.

NameInTypeRequiredDescription
idpathstringYesThe mount id.
switchquerystringNowait (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.
FieldTypeRequiredDescription
auto_deliverbooleanNoSaved 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_configobjectNoVFS 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' }
);

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 a vfs_config of 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 larger write_back), and what is left then is kept on the server as a pending upload, listed before the answer and named in uploads.pending_upload_id. When another mount uses that connection, or a request to the backend uses it meanwhile, the files keep uploading through it, and uploads says they are not delivered yet.
  • wait: the same, and the answer waits up to wait_seconds (1 to 600, default 120) until nothing is held open and nothing is left to upload. uploads.delivered is 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 with keep; 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, and uploads.discarded says 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 without mount_path. Refused for a mount served by its backend’s shared connection (409 UPLOADS_DISCARD_SHARED).

Servers that take uploads list mount_delete_uploads in the features of GET /api/v1/version; an older server ignores it.

NameInTypeRequiredDescription
idpathstringYesThe mount id.
uploadsquerystringNoWhat 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_secondsqueryintegerNoWith 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
});

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.

NameInTypeRequiredDescription
backend_idquerystringNoOnly 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' });

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.

NameInTypeRequiredDescription
idpathstringYesPending upload ID.
cursorquerystringNonext_cursor from the previous page. Omit it for the first page.
limitqueryintegerNoLargest 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 });

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.

NameInTypeRequiredDescription
idpathstringYesPending upload ID.
pathquerystringNoA path from the item’s files, exactly as listed. Give this or path_b64, not both.
path_b64querystringNoA path_b64 from the item’s files, exactly as listed. Give this or path, not both.
RangeheaderstringNoAsk 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-RangeheaderstringNoHonour 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'
});

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.

NameInTypeRequiredDescription
idpathstringYesPending upload ID.
FieldTypeRequiredDescription
backend_idstringNoDeliver 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.
pathsarray of stringNoPaths from the item’s files, exactly as listed. Omit this and paths_b64 to deliver every complete file.
paths_b64array of stringNopath_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']
});

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.

NameInTypeRequiredDescription
idpathstringYesPending 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');

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.

NameInTypeRequiredDescription
idpathstringYesPending 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');

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();

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}.

NameInTypeRequiredDescription
idpathstringYesPending 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');