File Management
OSAPI can upload files to a central Object Store and deploy them to managed hosts with SHA-based idempotency. File operations run through the job system, so the API server never writes to the filesystem directly -- agents handle all deployment.
What It Does
| Operation | Description |
|---|---|
| Upload | Store a file (base64-encoded) in the NATS Object Store |
| List | List all files stored in the Object Store |
| Get | Retrieve metadata for a specific stored file |
| Delete | Remove a file from the Object Store |
| Deploy | Deploy a file from Object Store to agent filesystem |
| Undeploy | Remove a deployed file from disk (state preserved) |
| Status | Check whether a deployed file is in-sync or drifted |
Upload / List / Get / Delete manage files in the central NATS Object Store. Files are stored by name and tracked with SHA-256 checksums. These operations are synchronous REST calls -- they do not go through the job system.
Deploy creates an asynchronous job that fetches the file from the Object Store and writes it to the target path on the agent's filesystem. Deploy supports optional file permissions (mode, owner, group) and Go template rendering.
Undeploy creates an asynchronous job that removes a previously deployed file from the agent's filesystem. The file-state KV record is preserved so the undeploy is auditable and a subsequent deploy can detect the change.
Status creates an asynchronous job that compares the current file on disk
against its expected SHA-256 from the file-state KV bucket. It reports one of
three states: in-sync, drifted, or missing.
How It Works
File Upload Flow
- The CLI (or SDK) computes a SHA-256 of the local file and queries the Object Store to check whether it already holds the same content. If the SHA matches, the upload is skipped entirely (no bytes sent over the network).
- If the file is new or the SHA differs, the CLI sends the file via a multipart upload.
- On the server side, if a file with the same name already exists and the
content differs, the server rejects the upload with 409 Conflict unless
?force=trueis passed. - If the content is identical, the server returns
changed: falsewithout rewriting the object. - With
--force, both the SDK pre-check and the server-side digest guard are bypassed — the file is always written andchanged: trueis returned.
File Deploy Flow
Deploy follows the standard job processing flow. The agent fetches the file from Object Store, computes its SHA-256, and checks the file-state KV for a previous deploy. If the SHA matches, the file is skipped (idempotent no-op). If the content differs, the agent writes the file to disk and updates the file-state KV with the new SHA-256.
You can target a specific host, broadcast to all hosts with _all, or route by
label.
File Undeploy Flow
osapi client node file undeploy --target HOST --path /etc/app/app.conf
Undeploy follows the standard job processing flow. The agent
removes the file from the filesystem but preserves the file-state KV record so
the operation is auditable. A subsequent deploy will write the file even if the
content has not changed (since the file is now absent). If the file does not
exist on disk, the operation returns changed: false.
SHA-Based Idempotency
Every deploy operation computes a SHA-256 of the file content and compares it against the previously deployed SHA stored in the file-state KV bucket. If the hashes match, the file is not rewritten. This makes repeated deploys safe and efficient -- only actual changes hit the filesystem.
The file-state KV has no TTL, so deploy state persists indefinitely until explicitly removed.
Protected Objects
Files stored under the osapi/ name prefix are protected. Both uploads and
deletes to osapi/* names return 403 Forbidden. These objects are managed
exclusively by osapi itself — the agent seeds them on startup from embedded
templates and updates them automatically when a new osapi version ships with
changes.
Protected objects are used by meta providers such as the cron provider, which
references them at deploy time. The osapi/ prefix is reserved; use any other
prefix for your own files.
Template Rendering
When content_type is set to template, the file content is processed as a Go
text/template before being written to disk. The template context provides
three top-level fields:
| Field | Description |
|---|---|
.Facts | Agent's collected system facts (map) |
.Vars | User-supplied template variables (map) |
.Hostname | Target agent's hostname (string) |
Example Template
A configuration file that adapts to each host:
# Generated for {{ .Hostname }}
listen_address = {{ .Vars.listen_address }}
workers = {{ .Facts.cpu_count }}
arch = {{ .Facts.architecture }}
Deploy it with template variables:
osapi client node file deploy \
--object-name app.conf.tmpl \
--path /etc/app/app.conf \
--content-type template \
--var listen_address=0.0.0.0:8080 \
--target _all
Each agent renders the template with its own facts and hostname, so the same template produces host-specific configuration across a fleet.
Available Fact Keys
Facts are exposed as a flat map via JSON round-tripping of the agent's
FactsRegistration. Use dot-syntax (.Facts.key) for keys that are valid Go
identifiers:
| Key | Type | Description | Example |
|---|---|---|---|
architecture | string | CPU architecture | amd64, arm64 |
kernel_version | string | OS kernel version | 6.8.0-51 |
cpu_count | number | Logical CPU count | 8 |
fqdn | string | Fully qualified domain name | web-01.lan |
service_mgr | string | Init system | systemd |
package_mgr | string | System package manager | apt |
containerized | boolean | Running inside a container | true |
primary_interface | string | Default route interface name | eth0 |
interfaces | []object | Network interfaces | (see below) |
routes | []object | IP routing table | (see below) |
Access scalar facts with dot-syntax:
arch = {{ .Facts.architecture }}
cpus = {{ .Facts.cpu_count }}
For keys with underscores, both {{ .Facts.kernel_version }} and
{{ index .Facts "kernel_version" }} work.
Templates use Go's missingkey=error option. Accessing a key that doesn't exist
via dot-syntax (e.g., {{ .Vars.missing }} or {{ .Facts.bogus }}) causes
the deploy to fail with an error rather than silently rendering
<no value>.
However, {{ index .Facts "nonexistent" }} uses Go's built-in index function,
which returns the zero value for the map's value type — rendering <no value>
without an error. Prefer dot-syntax over index for fact access so that
typos are caught at deploy time.
Meta Provider Templates
Domains that use file deployment (service management, certificate management)
inherit template support automatically. When the uploaded object has
content_type: template, the file provider renders it at deploy time — the meta
provider does not need to specify the content type.
For example, a systemd unit file template:
[Unit]
Description=App on {{ .Hostname }}
[Service]
ExecStart=/usr/bin/app --cpus {{ .Facts.cpu_count }}
Upload as a template, then deploy via the service API:
osapi client file upload \
--name my-unit --file app.service --content-type template
osapi client node service create \
--target web-01 --name my-app --object my-unit
Staleness Detection
When an object is re-uploaded to the Object Store with new content, existing
deployments become stale — the deployed file no longer matches the source. The
file stale command detects this by comparing the SHA-256 hash of each
deployment against the current object content.
osapi client file stale
This is a controller-side check that does not contact agents. It compares the
file-state KV (which tracks what was deployed) against the Object Store (which
has the current content). Use file deploy to bring stale deployments up to
date.
Stale detection covers all providers that use the file provider:
- Service unit files
- CA certificates
- Cron scripts
- Direct file deployments
Configuration
File management uses two NATS infrastructure components in addition to the general job infrastructure:
- Object Store (
nats.objects) -- stores uploaded file content. Configured with bucket name, max size, storage backend, and chunk size. - File State KV (
nats.file_state) -- tracks deploy state (SHA-256, path, timestamps) per host. Has no TTL -- state persists until explicitly removed.
See Configuration for the full reference.
nats:
objects:
bucket: 'file-objects'
max_bytes: 104857600
storage: 'file'
replicas: 1
max_chunk_size: 262144
file_state:
bucket: 'file-state'
storage: 'file'
replicas: 1
Permissions
File operations require file:* permissions. The admin and write roles
include both file:read and file:write. The read role includes only
file:read. See the API reference for the permission required
by each endpoint.
Related
- System Facts -- facts available in template context
- Job System -- how async job processing works
- Authentication & RBAC -- permissions and roles
- Architecture -- system design overview