Read files
Stream file contents over HTTP:
GET /api/v1/files/{path}?backend={id}This works for text, binary, and any other file type.
hoody-files connects a container to Google Drive, Dropbox, S3, and 62 cloud providers through a single HTTP API. It is a hoody-kit service rather than a Foundation feature, but cloud mounting is documented here with the rest of container storage.
Every storage backend hoody-files supports is both HTTP-accessible and POSIX-mountable. You can read Google Drive through the HTTP API, or mount it and treat it as a local directory.
Complete hoody-files documentation:
Overview:
Mounting cloud storage:
File operations:
Backend management:
hoody-files turns a cloud storage API into HTTP endpoints and into a mountable filesystem.
Google Drive API (OAuth, MIME types, pagination complexity) ↓ hoody-files abstraction ↓ ┌─────────────────┐ │ HTTP API │ ← curl, fetch, any HTTP client │ POSIX Mount │ ← ls, cp, cat, any filesystem tool └─────────────────┘Every backend is reachable two ways:
ls, cp, cat, and other standard tools work on it.The interface is the same for all 62 providers.
# Instead of learning 62 different APIs:# - Google Drive API (OAuth, pagination, MIME types)# - Dropbox API (cursors, batching, webhooks)# - S3 API (buckets, keys, multipart uploads)# ... 60 more APIs
# One unified HTTP API:GET /api/v1/files/{path}?backend={backend_id}
# Works identically for Google Drive, S3, Dropbox, OneDrive, etc.The full provider list is further down this page.
Prerequisite: OAuth credentials. OAuth-backed providers (Google Drive, Dropbox, OneDrive, Box, and so on) accept a
client_id/client_secretfrom the provider console plus atokenJSON blob from an OAuth consent flow. None of them are required: omit both credentials and an internal shared OAuth client is used instead (low performance; bringing your own client is recommended for full quota). The CLI is not interactive; pass the values as flags. For your own OAuth client, runhoody files backends connect drive -c <container-id> --client-id <id> --client-secret <secret>. To supply a pre-obtained provider token, use the HTTP request below; the current CLI incorrectly captures the subcommand’s--tokenas Hoody API authentication. The CLI prints only a success message and does not return the backend id, so runhoody files backends list -c <container-id> -o jsonafterwards to read it. Key/secret backends (S3, B2, etc.) need the provider’s access-key pair instead.
Connect a storage provider. The connection persists, so you configure a provider once and reuse it:
curl -X POST "https://$PROJECT-$CONTAINER-files-1.$SERVER.containers.hoody.com/api/v1/backends/drive" \ -H "Content-Type: application/json" \ -d '{ "client_id": "your-app.apps.googleusercontent.com", "client_secret": "your-secret", "token": "{\"access_token\":\"ya29...\"}" }'
# 201 Response: {"success": true, "data": {"id": "9b4d3e2f5a6c7890", "backend_type": "drive", "type": "cloud", "mount_paths": []}}curl -X POST "https://$PROJECT-$CONTAINER-files-1.$SERVER.containers.hoody.com/api/v1/backends/dropbox" \ -H "Content-Type: application/json" \ -d '{ "client_id": "your-client-id", "client_secret": "your-secret", "token": "{\"access_token\":\"sl.B...\"}" }'
# 201 Response: {"success": true, "data": {"id": "0c5e4f3a6b7d8901", "backend_type": "dropbox", "type": "cloud", "mount_paths": []}}curl -X POST "https://$PROJECT-$CONTAINER-files-1.$SERVER.containers.hoody.com/api/v1/backends/s3" \ -H "Content-Type: application/json" \ -d '{ "provider": "AWS", "access_key_id": "AKIAIOSFODNN7EXAMPLE", "secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", "region": "us-east-1", "endpoint": "s3.us-east-1.amazonaws.com" }'
# 201 Response: {"success": true, "data": {"id": "1a2b3c4d5e6f7080", "backend_type": "s3", "type": "object_storage", "mount_paths": []}}Every connected backend answers the same requests, whether it is Google Drive, S3, or SFTP:
# Read file from any connected backendhoody files get "Documents/report.pdf" --backend 9b4d3e2f5a6c7890 -c <container-id>
# List directory on a backend (get lists directory paths too)hoody files get "Documents/" --backend 1a2b3c4d5e6f7080 -c <container-id>// Read from Google Drive: the same call for all 62 providersconst driveFile = await client.files.get('Documents/report.pdf', { backend: '9b4d3e2f5a6c7890'});
// Read from S3: only the backend parameter changesconst s3File = await client.files.get('Documents/report.pdf', { backend: '1a2b3c4d5e6f7080'});# Read from Google Drivecurl "https://$PROJECT-$CONTAINER-files-1.$SERVER.containers.hoody.com/api/v1/files/Documents/report.pdf?backend=9b4d3e2f5a6c7890"
# Read from Dropbox: same endpoint, different backendcurl "https://$PROJECT-$CONTAINER-files-1.$SERVER.containers.hoody.com/api/v1/files/Documents/report.pdf?backend=0c5e4f3a6b7d8901"
# Read from S3: same API, different backendcurl "https://$PROJECT-$CONTAINER-files-1.$SERVER.containers.hoody.com/api/v1/files/Documents/report.pdf?backend=1a2b3c4d5e6f7080"One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Reads the same file from three connected backends; only the backend id in the query string changes.
# Google Drive
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-CONTAINER_ID-files-1.SERVER.containers.hoody.com/api/v1/files/Documents/report.pdf?backend=9b4d3e2f5a6c7890&method=GET&response=transparent
# Dropbox
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-CONTAINER_ID-files-1.SERVER.containers.hoody.com/api/v1/files/Documents/report.pdf?backend=0c5e4f3a6b7d8901&method=GET&response=transparent
# S3
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-CONTAINER_ID-files-1.SERVER.containers.hoody.com/api/v1/files/Documents/report.pdf?backend=1a2b3c4d5e6f7080&method=GET&response=transparent Major cloud storage (9)
Object storage (12)
s3 backend, provider selects the flavor)File protocols (5)
File sharing services (9)
Enterprise services (7)
Specialty services (6)
Utility and virtual backends (10)
Legacy and specialized (4)
Total: 62 backend types
Git repositories are also accessible read-only, but through the fetch-from-git file operation (?type=git) rather than a backends/ mount, so Git is not counted among the 62 backend types.
For detailed configuration parameters, see:
Without an abstraction layer, an application implements each provider’s own API:
// Google Drive: OAuth, MIME types, fileId abstractionconst drive = google.drive({version: 'v3', auth});const file = await drive.files.get({fileId: 'abc123', alt: 'media'});
// Dropbox: different auth, different structureconst dbx = new Dropbox({accessToken: TOKEN});const file = await dbx.filesDownload({path: '/folder/file.pdf'});
// S3: buckets, keys, and regions instead of pathsconst s3 = new AWS.S3();const file = await s3.getObject({Bucket: 'my-bucket', Key: 'folder/file.pdf'}).promise();
// You need 62 different implementationshoody-files collapses those into one request shape:
# Same API for all 62 providers - just change backend parametercurl "https://$PROJECT-$CONTAINER-files-1.$SERVER.containers.hoody.com/api/v1/files/folder/file.pdf?backend={backend_id}"
# Works for Google Drive, Dropbox, S3, OneDrive, Box, SFTP, WebDAV, etc.You learn one API instead of 62. Application code names a backend id rather than a provider SDK, so moving from S3 to Dropbox changes a parameter and nothing else. hoody-files translates each HTTP call into the provider’s own protocol and reports errors in one format across every backend.
Every connected backend supports the same operations.
Read files
Stream file contents over HTTP:
GET /api/v1/files/{path}?backend={id}This works for text, binary, and any other file type.
Verify a file hash
Get a file’s SHA256 hash, returned as plain text:
GET /api/v1/files/{path}?backend={id}&hashCompare it against your local copy to check integrity.
List directories
Browse directories as JSON:
GET /api/v1/files/{path}/?backend={id}Listings can be sorted and filtered.
Read file metadata
Get size, type, and modified time:
GET /api/v1/files/{path}?backend={id}&statThese queries are lightweight.
See the Hoody Files API for the complete set of operations.
The three requests below connect the backend, list the root directory, and read a file. After that, every file in the account is reachable over HTTP.
# Connect Google Drivecurl -X POST "https://$PROJECT-$CONTAINER-files-1.$SERVER.containers.hoody.com/api/v1/backends/drive" \ -H "Content-Type: application/json" \ -d '{ "client_id": "your-app.apps.googleusercontent.com", "client_secret": "your-client-secret", "token": "{\"access_token\":\"ya29.a0...\"}" }'
# Response includes backend ID{ "success": true, "message": "Google Drive backend connected successfully", "data": { "id": "9b4d3e2f5a6c7890", "backend_type": "drive", "type": "cloud", "mount_paths": [] }}# List root directorycurl "https://$PROJECT-$CONTAINER-files-1.$SERVER.containers.hoody.com/api/v1/files/?backend=9b4d3e2f5a6c7890"
# Response: DirectoryListing JSON (kind + paths array){ "kind": "Index", "href": "/", "uri_prefix": "/api/v1/files/", "dir_exists": true, "paths": [ {"name": "Documents", "path_type": "Dir", "size": 0, "mtime": 1716390000000, "revisions": null}, {"name": "Photos", "path_type": "Dir", "size": 0, "mtime": 1716380000000, "revisions": null}, {"name": "report.pdf", "path_type": "File", "size": 1048576, "mtime": 1716300000000, "revisions": 3} ]}# Read file contentcurl "https://$PROJECT-$CONTAINER-files-1.$SERVER.containers.hoody.com/api/v1/files/Documents/report.pdf?backend=9b4d3e2f5a6c7890" \ > report.pdf
# File downloaded from Google Drive via HTTPOne application can read from several providers at once. This pattern covers backup verification, data migration, and multi-cloud setups:
# Connect all your storagePOST /backends/drive → backend_drive_abcPOST /backends/dropbox → backend_dropbox_xyzPOST /backends/s3 → backend_s3_def
# Now access any file from any backendGET /files/project-data.json?backend=backend_drive_abcGET /files/project-data.json?backend=backend_dropbox_xyzGET /files/project-data.json?backend=backend_s3_def
# Same API, different storage providersA container can push its own backups to a cloud provider:
# Upload container backup to Google Drivetar czf backup.tar.gz /hoody/storage/myapp/
curl -X PUT "https://$PROJECT-$CONTAINER-files-1.$SERVER.containers.hoody.com/api/v1/files/Backups/backup.tar.gz?backend=backend_drive_abc" \ --data-binary "@backup.tar.gz"
# Hoody Files handles OAuth, chunking, retriesRead from the cloud, process in the container, write the results back:
# Download dataset from S3curl "https://$PROJECT-$CONTAINER-files-1.$SERVER.containers.hoody.com/api/v1/files/datasets/data.csv?backend=backend_s3_abc" > data.csv
# Process locallypython process.py data.csv > results.json
# Upload results to Dropboxcurl -X PUT "https://$PROJECT-$CONTAINER-files-1.$SERVER.containers.hoody.com/api/v1/files/Results/results.json?backend=backend_dropbox_xyz" \ --data-binary "@results.json"
# No cloud SDK involved; the transfers are plain HTTPCloud storage files can be exposed through a container URL:
# Connect S3 (the bucket is addressed in the path, e.g. my-public-files/images/logo.png)POST /backends/s3 {"provider": "AWS", "access_key_id": "AKIA...", "secret_access_key": "...", "region": "us-east-1", "endpoint": "s3.us-east-1.amazonaws.com"}
# Now serve files via Hoody ProxyGET https://{project}-{container}-files.../api/v1/files/images/logo.png?backend=backend_s3_abc
# S3 objects are now served over HTTPConsumer cloud services that authenticate with OAuth:
See the Cloud Storage API for mounting instructions.
Object stores that speak the S3 API:
See the Object Storage API for S3 configuration.
Servers reachable over standard file protocols:
See the File Protocols API for protocol mounting.
The path and query string do not change between providers. Only the backend id does.
# List a directory (identical for every backend)hoody files get "folder/" --backend $BACKEND_ID -c <container-id>
# Read a file (identical for every backend)hoody files get "folder/file.txt" --backend $BACKEND_ID -c <container-id>// List a directory (identical for every backend)// (files.get returns the JSON listing for a directory path)const listing = await client.files.get('folder/', { backend: BACKEND_ID});
// Read a file (identical for every backend)const file = await client.files.get('folder/file.txt', { backend: BACKEND_ID});# List a directory (identical for every backend)curl "https://$PROJECT-$CONTAINER-files-1.$SERVER.containers.hoody.com/api/v1/files/folder/?backend=$BACKEND_ID"
# Read a file (identical for every backend)curl "https://$PROJECT-$CONTAINER-files-1.$SERVER.containers.hoody.com/api/v1/files/folder/file.txt?backend=$BACKEND_ID"
# Read metadata (identical for every backend)curl "https://$PROJECT-$CONTAINER-files-1.$SERVER.containers.hoody.com/api/v1/files/folder/file.txt?backend=$BACKEND_ID&stat"One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Lists a folder, reads a file, and reads that file’s metadata — the same three links work for any connected backend.
# List a directory
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-CONTAINER_ID-files-1.SERVER.containers.hoody.com/api/v1/files/folder/?backend=BACKEND_ID&method=GET&response=transparent
# Read a file
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-CONTAINER_ID-files-1.SERVER.containers.hoody.com/api/v1/files/folder/file.txt?backend=BACKEND_ID&method=GET&response=transparent
# Read metadata
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-CONTAINER_ID-files-1.SERVER.containers.hoody.com/api/v1/files/folder/file.txt?backend=BACKEND_ID%26stat&method=GET&response=transparent Switching provider is a change to one variable:
// Change from S3 to Dropboxconst backend = 'backend_s3_abc'; // S3const backend = 'backend_dropbox_xyz'; // Dropbox
// Rest of code unchangedconst file = await fetch(`/api/v1/files/data.json?backend=${backend}`);There is no fixed limit on how many backends a container connects at the same time:
# Personal Google Drivebackend_drive_personal
# Work Google Drivebackend_drive_work
# AWS S3 productionbackend_s3_prod
# AWS S3 backupsbackend_s3_backup
# Dropbox archivebackend_dropbox_archive
# All accessible from one container via hoody-filesThe backend can be a runtime choice rather than a build-time one:
// User chooses storage providerconst backend = userPreference; // 'backend_drive_abc' or 'backend_s3_xyz'
// App code doesn't care which providerasync function saveFile(path, content, backend) { return fetch(`/api/v1/files/${path}?backend=${backend}`, { method: 'PUT', body: content });}
// Works with Google Drive, S3, Dropbox, OneDrive, etc.Move a file between two providers without pulling it to your own machine:
# Read from Google Drivecurl "https://$PROJECT-$CONTAINER-files-1.$SERVER.containers.hoody.com/api/v1/files/backup.zip?backend=backend_drive_abc" \ > backup.zip
# Upload to S3curl -X PUT "https://$PROJECT-$CONTAINER-files-1.$SERVER.containers.hoody.com/api/v1/files/backup.zip?backend=backend_s3_xyz" \ --data-binary "@backup.zip"
# The transfer runs in the container, on the server's bandwidthWrite the same backup to several providers for redundancy:
# Create backuptar czf critical-data.tar.gz /hoody/storage/production/
# Upload to 3 different cloud providersfor backend in backend_s3_primary backend_gcs_backup backend_backblaze_archive; do curl -X PUT "https://$PROJECT-$CONTAINER-files-1.$SERVER.containers.hoody.com/api/v1/files/Backups/critical-data.tar.gz?backend=$backend" \ --data-binary "@critical-data.tar.gz"done
# Same data in 3 locations (AWS, Google, Backblaze)The same three stages written out step by step:
# 1. Download dataset from Dropboxcurl "https://$PROJECT-$CONTAINER-files-1.$SERVER.containers.hoody.com/api/v1/files/datasets/raw-data.csv?backend=backend_dropbox_abc" \ > raw-data.csv
# 2. Process in containerpython analyze.py raw-data.csv > analysis-results.json
# 3. Upload results to Google Drivecurl -X PUT "https://$PROJECT-$CONTAINER-files-1.$SERVER.containers.hoody.com/api/v1/files/Results/analysis-results.json?backend=backend_drive_xyz" \ --data-binary "@analysis-results.json"
# Cloud → Container → Cloud (all via HTTP)Consumer OAuth requires a person to complete the consent flow. Service account credentials do not expire with a user session, so scheduled tasks keep working:
# Google Drive: Service Account (no user interaction){ "service_account_file": "/keys/service-account.json", "team_drive": "0ABC123xyz"}
# Box: JWT Authentication{ "box_config_file": "/keys/box-config.json", "box_sub_type": "enterprise"}A read-only scope removes write access from the credential:
# Google Drive: Read-only scope{ "scope": "drive.readonly"}
# Prevents accidental modifications# Limits damage if credentials compromisedA backend connection survives container restarts:
# Connect backend oncePOST /backends/drive → backend_drive_abc
# Use in all future requestsGET /files/data.json?backend=backend_drive_abc
# Connection remains active until explicitly disconnected# Verify connection worksGET /api/v1/files/?backend=backend_drive_abc
# Should return directory listing# If error: OAuth expired, credentials wrong, or network issueSee Managing Backends for testing and troubleshooting.
The crypt backend wraps another backend with zero-knowledge encryption:
# Connect the encrypted wrapperPOST /backends/crypt{ "remote": "backend_drive_abc:/Encrypted", "password": "your-encryption-password"}
# Now files are encrypted before uploadSee Encryption Layer for zero-knowledge encryption.
No. hoody-files streams content: it buffers small files and sends large ones chunk by chunk. The container never stores a complete copy of the cloud file.
Yes. hoody-files also provides:
Cloud mounting is optional. hoody-files works the same way for local storage.
Requests fail with 401 Unauthorized. Disconnect the backend and reconnect it with a fresh token:
# Disconnect expired backendDELETE /api/v1/backends/backend_drive_abc
# Reconnect with fresh OAuth tokenPOST /api/v1/backends/drive{"client_id": "...", "client_secret": "...", "token": "{\"access_token\":\"NEW_TOKEN\"}"}Yes. Personal and work Google Drive accounts can both be connected:
POST /backends/drive {"token": "PERSONAL_TOKEN"} → backend_drive_personalPOST /backends/drive {"token": "WORK_TOKEN"} → backend_drive_work
# Access both simultaneouslyGET /files/data.json?backend=backend_drive_personalGET /files/data.json?backend=backend_drive_workYes, with HTTP PUT. Send the file body to PUT /api/v1/files/{path}?backend={id} (or at the root /{path}). See Hoody Files API for upload endpoints.
Streaming: close to native speed, over the provider’s own transport. Some backends (notably Google Drive) disable HTTP/2 by default pending an upstream fix and fall back to HTTP/1.1, which adds a little per-request overhead. Other backends keep HTTP/2 enabled.
Batch operations: slightly slower, because each request carries HTTP overhead.
High throughput: run the cloud provider’s own SDK inside the container. hoody-files trades maximum throughput for a single interface.
Problem: POST /backends/{provider} returns error
Solutions:
OAuth token issues:
Credential errors:
Network connectivity:
# Test from containercurl https://www.googleapis.com # Google Drivecurl https://api.dropboxapi.com # Dropbox
# Should return response (not timeout)Problem: GET /files/?backend={id} returns empty or incomplete
Check:
Path is correct:
# Root directoryGET /files/?backend={id}
# Specific folderGET /files/Documents/?backend={id}Backend has data:
Permissions:
Problem: 429 Too Many Requests from cloud provider
Solutions:
Implement exponential backoff:
async function fetchWithRetry(url, retries = 3) { for (let i = 0; i < retries; i++) { const response = await fetch(url); if (response.status !== 429) return response; await sleep(Math.pow(2, i) * 1000); // 1s, 2s, 4s }}Use caching:
# Connect a cache layerPOST /backends/cache{"remote": "backend_drive_abc:", "chunk_size": "10M"}
# Repeated requests served from cacheReduce request frequency:
Storage pages:
hoody-files reference: