Disk storage provider

Local filesystem provider implementing storage.ProviderPort against a sandboxed directory on disk.

Overview

The disk provider stores objects directly on the local filesystem under a configured base directory. Path resolution goes through Piko's safedisk sandbox. User-supplied keys cannot escape the configured root via traversal (../), absolute paths, or symlink games. The sandbox rejects them at the boundary. The sandbox builds on Go's os.Root. That uses kernel-level path resolution (openat2 with RESOLVE_BENEATH on Linux) to guard against symlink escapes and time-of-check-to-time-of-use races, not just static prefix checks.

Atomic writes use the temp-file-and-rename pattern, so a crash mid-write leaves either the old object or no object, never a partial one. Each Put fsyncs the temporary data file before the rename and fsyncs the parent directory after. A successful return survives a crash even on journaling filesystems, where the metadata journal flushes independently of file data.

Disk's character is simple, fast, single-node. It fits local development, single-node deployments, and any workload whose data fits on a single attached volume and where per-request object-storage costs would otherwise dominate. The disk provider does not synchronise across replicas. Every node has its own filesystem. It is not suitable for horizontally scaled production deployments unless paired with a shared network filesystem like NFS or EFS. Even then a cloud object store (S3, GCS, R2) is usually a better fit.

Requirements

  • A writable directory at BaseDirectory with enough free space for your workload.
  • The process user must own or have read/write permission on the base directory.

Configuration

import "piko.sh/piko/wdk/storage/storage_provider_disk"

provider, err := storage_provider_disk.NewDiskProvider(storage_provider_disk.Config{
    BaseDirectory: "/var/lib/myapp/storage", // required; root of the sandboxed area
})
if err != nil {
    return err
}

NewDiskProvider returns a storage.ProviderPort, the same port type every storage provider returns. There is no context argument, no migration step, and no code generation. One call produces a port that registers directly through piko.WithStorageProvider. BaseDirectory is the only required field. The constructor fails at boot when the base directory is missing and the config supplies no sandbox, so a misconfigured path surfaces before the first request.

Pass storage.ProviderOption values as variadic arguments after the config for rate-limiter overrides.

For advanced containment or testing, supply your own sandbox through Config.Sandbox, or a Config.SandboxFactory to build one. The constructor prefers either over building a default sandbox from BaseDirectory, so the call site stays the same. safedisk is a reusable Piko primitive, not specific to storage.

Bootstrap

ssr := piko.New(
    piko.WithStorageProvider("disk", provider),
    piko.WithDefaultStorageProvider("disk"),
)

Presigned URLs and multipart

The disk provider does not generate native presigned URLs or support native multipart uploads. The storage service layer fills the gap. When a caller requests a presigned upload or download URL, the service substitutes an HMAC-signed token paired with an HTTP endpoint instead of delegating to the provider. The behaviour differs from S3, GCS, or R2, which sign URLs the object store serves directly.

For a frontend on a different host or port, set the presign base URL so the generated URLs resolve cross-origin.

ssr := piko.New(
    piko.WithStorageProvider("disk", provider),
    piko.WithDefaultStorageProvider("disk"),
    piko.WithStoragePresignBaseURL("https://cms.example.com"),
)

Health checks

The readiness probe reports the provider degraded when free space under the base directory drops below 1024 MB. A single-node operator watching readiness probes should account for this floor when sizing the volume.

See also

Other storage providers:

Storage transformers:

Framework docs: