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