RRuna

Storage

Unified access to local disks and object storage

storage manages named disks. It uses a local file driver by default. Production deployments can install an S3-compatible object storage driver on demand.

Install

go get github.com/duxweb/runa/storage

S3 driver:

go get github.com/duxweb/runa/storage/s3

Connect to an application

package main

import (
    "context"

    "github.com/duxweb/runa"
    "github.com/duxweb/runa/storage"
)

func main() {
    app := runa.New()
    app.Install(storage.Provider(
        storage.RegisterDisk("public", storage.Prefix("public"), storage.Public()),
    ))

    if err := app.Freeze(context.Background()); err != nil {
        panic(err)
    }

    disk := storage.Default().MustOf("public")
    _ = disk.PutString(context.Background(), "hello.txt", "Hello Runa", storage.ContentType("text/plain"))
}

storage.Provider() registers *storage.Registry into DI and reads disk config.

Standalone New usage

registry := storage.New(storage.Root("./data/storage"), storage.DriverURLPrefix("/files"))
disk := registry.MustOf(storage.DiskPublic)

_ = disk.PutString(context.Background(), "hello.txt", "Hello", storage.ContentType("text/plain"))
body, _ := disk.GetString(context.Background(), "hello.txt")
_ = body

Config

storage reads storage.disks.<name> and only applies config to disks that have already been registered. New() registers local, public, private, and cloud by default.

[storage.disks.public]
driver = "local"
prefix = "public"
public = true
url_prefix = "/files"
domain = "https://cdn.example.com"

[storage.disks.private]
driver = "local"
prefix = "private"
public = false
Key Type Description
driver string driver name, default local
prefix string disk path prefix
public bool whether the disk is public
url_prefix string URL prefix
domain string URL domain
meta table custom metadata

S3 driver

Install both the storage provider and the S3 driver provider. s3storage.Provider(...) registers a driver named s3; disks can then use driver = "s3" from config or storage.Use("s3") from code.

import s3storage "github.com/duxweb/runa/storage/s3"

app.Install(
    storage.Provider(storage.RegisterDisk("cloud", storage.Use("s3"), storage.Public())),
    s3storage.Provider(
        s3storage.Bucket("app"),
        s3storage.Region("us-east-1"),
        s3storage.Endpoint("https://s3.example.com"),
        s3storage.Credentials("access", "secret"),
        s3storage.PathStyle(true),
    ),
)

Config-only setup:

[storage.disks.cloud]
driver = "s3"
prefix = "uploads"
public = true

[storage.s3]
bucket = "app"
region = "us-east-1"
endpoint = "https://s3.example.com"
access_key = "access"
secret_key = "secret"
path_style = true
domain = "https://cdn.example.com"
url_prefix = "/files"

storage.s3 overrides shared [s3] connection values. Use [s3.<name>] plus s3storage.Use(name) when several S3-compatible connections are configured. If static credentials are not provided, the AWS SDK default credential chain is used, including environment variables, shared config, and IAM roles. For MinIO, R2, OSS, or other S3-compatible services, set endpoint and path_style as required.

When endpoint is set, the driver treats the target as a generic S3-compatible service. It does not detect or branch on provider names such as OSS, COS, R2, or MinIO. Instead, it applies provider-neutral compatibility behavior: request checksum calculation is lowered to when_required, copy sources are URL-escaped for non-ASCII and reserved characters, and delete calls use single-object DeleteObject requests instead of multi-object delete. This avoids common checksum, copy-source, and Content-MD5 differences across S3-compatible providers while keeping native AWS S3 behavior unchanged when endpoint is not set.

S3-compatible providers

These recipes use the same s3storage.Provider(...) and [storage.disks.<name>] setup shown above.

# MinIO
[storage.s3]
bucket = "app"
region = "us-east-1"
endpoint = "http://127.0.0.1:9000"
access_key = "minioadmin"
secret_key = "minioadmin"
path_style = true

# Cloudflare R2
[storage.s3]
bucket = "app"
region = "auto"
endpoint = "https://<account-id>.r2.cloudflarestorage.com"
access_key = "<access-key-id>"
secret_key = "<secret-access-key>"
path_style = true

# Alibaba Cloud OSS
[storage.s3]
bucket = "app"
region = "oss-cn-hangzhou"
endpoint = "https://oss-cn-hangzhou.aliyuncs.com"
access_key = "<access-key-id>"
secret_key = "<access-key-secret>"
path_style = false

# Tencent Cloud COS
[storage.s3]
bucket = "app-1250000000"
region = "ap-guangzhou"
endpoint = "https://cos.ap-guangzhou.myqcloud.com"
access_key = "<secret-id>"
secret_key = "<secret-key>"
path_style = false

# Qiniu Kodo S3
[storage.s3]
bucket = "app"
region = "z0"
endpoint = "https://s3-cn-east-1.qiniucs.com"
access_key = "<access-key>"
secret_key = "<secret-key>"
path_style = true

# Volcano Engine TOS
[storage.s3]
bucket = "app"
region = "cn-beijing"
endpoint = "https://tos-s3-cn-beijing.volces.com"
access_key = "<access-key>"
secret_key = "<secret-key>"
path_style = false

# Huawei OBS
[storage.s3]
bucket = "app"
region = "cn-north-4"
endpoint = "https://obs.cn-north-4.myhuaweicloud.com"
access_key = "<access-key>"
secret_key = "<secret-key>"
path_style = false
Provider CRUD TempURL SignPut SignPost
AWS S3 yes yes yes yes
MinIO yes yes yes usually yes
Cloudflare R2 yes yes yes usually yes
Alibaba Cloud OSS S3 yes yes yes partial
Tencent Cloud COS S3 yes yes yes usually yes
Qiniu Kodo S3 yes yes yes partial
Volcano TOS S3 yes yes yes partial
Huawei OBS S3 yes yes yes partial

For browser uploads, prefer SignPut unless you have already verified SigV4 POST policy support on the target provider. Provider-native callbacks, upload tokens, persistent processing, and image processing parameters are outside the S3 protocol; add a provider-specific driver only when those native-only features are required.

Common API

disk := storage.Default().MustOf("public")

_ = disk.PutString(ctx, "avatars/1.txt", "hello", storage.ContentType("text/plain"))
body, err := disk.GetString(ctx, "avatars/1.txt")
files, err := disk.List(ctx, "avatars", storage.Limit(100), storage.Recursive())
exists, err := disk.Exists(ctx, "avatars/1.txt")
url, err := disk.URL(ctx, "avatars/1.txt")
_ = body
_ = files
_ = exists
_ = url
_ = err

List returns one page with Items, CommonDirs, Cursor, and HasMore. Pass storage.Cursor(previous.Cursor) to continue pagination.

Choosing public and private disks

Disk Typical use
public Files that can be directly accessed by URL, such as avatars or public downloads
private Files that need permission checks, signed URLs, or application-controlled streaming

Use private disks for documents, exports, identity files, and other sensitive content.

Common mistakes

Local disk path is unclear

Local drivers read and write relative to their configured root. Use an explicit root path in production so files do not end up in an unexpected working directory.

URL is empty or unexpected

Public URLs depend on the disk’s URL/base configuration. Private disks may intentionally not expose a direct URL.

Using local public disk for production public files

For production public uploads, object storage plus CDN is usually more reliable than local disk on one application instance.

API quick reference

  • storage.New(options...) creates a standalone registry.
  • storage.Provider(...) connects to the framework lifecycle.
  • storage.Default() reads *storage.Registry from default DI.
  • storage.RegisterDriver(name, driver) registers a driver.
  • storage.RegisterDisk(name, options...) registers a disk.
  • registry.MustOf(name) gets a disk.
  • storage.LocalDriver(...) creates a local driver.
  • s3storage.Provider(...) registers the S3-compatible driver from options or config.
Edit this page