Storage
pkg/storage provides a unified file-storage API inspired by Laravel's Storage facade. Switch between local disk and S3-compatible storage with a single env variable.
Configuration
.envgo
STORAGE_DISK=local # default driver: "local" or "s3"Using the Default Disk
gogo
import "github.com/shashiranjanraj/kashvi/pkg/storage"
// Write
storage.Put("avatars/user-1.jpg", imageBytes)
storage.PutStream("uploads/file.pdf", r.Body)
// Read
data, err := storage.Get("avatars/user-1.jpg")
stream, err := storage.GetStream("uploads/file.pdf")
defer stream.Close()
// Metadata
exists := storage.Exists("avatars/user-1.jpg")
missing := storage.Missing("avatars/user-1.jpg")
size, _ := storage.Size("avatars/user-1.jpg")
modTime, _ := storage.LastModified("avatars/user-1.jpg")
// Public URL
url := storage.URL("avatars/user-1.jpg")
// Delete
storage.Delete("avatars/user-1.jpg")
// Copy / Move
storage.Copy("tmp/upload.jpg", "images/final.jpg")
storage.Move("tmp/upload.jpg", "archive/old.jpg")
// Directories
files, _ := storage.Files("avatars") // non-recursive
all, _ := storage.AllFiles("avatars") // recursive
dirs, _ := storage.Directories("uploads")
storage.MakeDirectory("exports")
storage.DeleteDirectory("tmp")Using a Specific Disk
gogo
// Use S3 explicitly
storage.Use("s3").Put("backups/db.sql.gz", data)
// Use local disk explicitly
storage.Use("local").Get("cache/data.json")Why Use() and not Disk()?
Method name is
Use() (not Disk()) to avoid conflict with the Disk interface type.File Upload Handler
gogo
func (c *UploadController) Store(ctx *appctx.Context) {
ctx.R.ParseMultipartForm(10 << 20) // 10MB max
file, header, err := ctx.R.FormFile("file")
if err != nil {
ctx.Error(400, "No file uploaded")
return
}
defer file.Close()
path := fmt.Sprintf("uploads/%d_%s", time.Now().Unix(), header.Filename)
if err := storage.PutStream(path, file); err != nil {
ctx.Error(500, "Upload failed")
return
}
ctx.Created(map[string]any{
"path": path,
"url": storage.URL(path),
})
}Local Disk
Files are stored relative to STORAGE_LOCAL_ROOT (default: ./storage). Public access: GET /storage/{path} is automatically mounted when STORAGE_DISK=local.
.envgo
STORAGE_LOCAL_ROOT=storage
STORAGE_URL=http://localhost:8080/storageS3 / AWS
.envgo
STORAGE_DISK=s3
S3_BUCKET=my-bucket
S3_REGION=us-east-1
S3_KEY=AKIAIOSFODNN7EXAMPLE
S3_SECRET=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
S3_URL=https://my-bucket.s3.us-east-1.amazonaws.comMinIO (self-hosted S3)
Run locally with Docker:
docker run -p 9000:9000 -p 9001:9001 \
-e MINIO_ROOT_USER=minioadmin \
-e MINIO_ROOT_PASSWORD=minioadmin \
minio/minio server /data --console-address ":9001".envgo
STORAGE_DISK=s3
S3_BUCKET=my-bucket
S3_KEY=minioadmin
S3_SECRET=minioadmin
S3_ENDPOINT=http://localhost:9000
S3_REGION=us-east-1Create the bucket at http://localhost:9001 (MinIO console UI).
Cloudflare R2 / DigitalOcean Spaces
Same as MinIO — just set S3_ENDPOINT to your provider's endpoint URL.
# Cloudflare R2
S3_ENDPOINT=https://<ACCOUNT_ID>.r2.cloudflarestorage.com
# DigitalOcean Spaces
S3_ENDPOINT=https://nyc3.digitaloceanspaces.comCustom Driver
Implement the Disk interface and register it:
gogo
type MyDriver struct{}
func (d *MyDriver) Put(path string, content []byte) error { ... }
// ... implement all 16 Disk interface methods
// Register at boot:
storage.RegisterDisk("mydriver", &MyDriver{})
// Use:
storage.Use("mydriver").Put("file.txt", data)