feat(storage): define FileStore interface abstraction #126

Closed
opened 2026-02-17 16:09:13 +00:00 by forbes · 0 comments
Owner

Summary

Extract the current MinIO-specific code in internal/storage/storage.go into a FileStore interface, enabling pluggable storage backends.

Context

Currently internal/storage/storage.go defines a concrete Storage struct wrapping *minio.Client with no interface abstraction. The Server struct in internal/api/server.go holds a *storage.Storage pointer directly, and all handlers (file_handlers.go, handlers.go) call methods on this concrete type. Nil-checks (if s.storage == nil) gate file operations when storage is unconfigured.

Requirements

Define a FileStore interface in internal/storage/:

type FileStore interface {
    Put(ctx context.Context, key string, reader io.Reader, size int64, contentType string) (*PutResult, error)
    Get(ctx context.Context, key string) (io.ReadCloser, error)
    GetVersion(ctx context.Context, key string, versionID string) (io.ReadCloser, error)
    Delete(ctx context.Context, key string) error
    Exists(ctx context.Context, key string) (bool, error)
    Copy(ctx context.Context, srcKey, dstKey string) error
    PresignPut(ctx context.Context, key string, expiry time.Duration) (*url.URL, error)
    Ping(ctx context.Context) error
}

Implementation steps

  1. Define FileStore interface in a new file internal/storage/interface.go
  2. Verify the existing Storage struct satisfies the interface (it already has all these methods)
  3. Add Exists method to Storage (currently missing — implement via minio.StatObject)
  4. Update Server struct in internal/api/server.go to hold storage.FileStore instead of *storage.Storage
  5. Update NewServer() constructor and all call sites
  6. Update cmd/silod/main.go startup wiring — currently passes *storage.Storage (or nil)
  7. Keep standalone functions FileKey() and ThumbnailKey() as package-level helpers (not on interface)
  8. Extend internal/config/config.go StorageConfig with a Backend field:
    type StorageConfig struct {
        Backend   string `yaml:"backend"` // "minio" | "filesystem"
        Endpoint  string `yaml:"endpoint"`
        // ... existing fields ...
        Filesystem FilesystemConfig `yaml:"filesystem"`
    }
    type FilesystemConfig struct {
        Root string `yaml:"root"`
    }
    

Files to modify

  • internal/storage/storage.go — add Exists method, ensure interface compliance
  • internal/storage/interface.go — new file with FileStore interface
  • internal/api/server.go — change storage *storage.Storage to storage storage.FileStore
  • internal/api/file_handlers.go — no changes needed if using interface (6 handlers: HandlePresignUpload, HandleListItemFiles, HandleAssociateItemFile, HandleDeleteItemFile, HandleSetItemThumbnail)
  • internal/api/handlers.goHandleUploadFile, HandleDownloadFile use s.storage.Put(), s.storage.Get(), s.storage.GetVersion()
  • internal/config/config.go — add Backend and Filesystem config fields
  • cmd/silod/main.go — backend selection logic

Current method signatures on Storage

Method Signature
Put (ctx, key, reader, size, contentType) (*PutResult, error)
Get (ctx, key) (io.ReadCloser, error)
GetVersion (ctx, key, versionID) (io.ReadCloser, error)
Delete (ctx, key) error
Ping (ctx) error
PresignPut (ctx, key, expiry) (*url.URL, error)
Copy (ctx, srcKey, dstKey) error
Bucket () string
FileKey (partNumber, revision) string (standalone)
ThumbnailKey (partNumber, revision) string (standalone)

Acceptance criteria

  • FileStore interface defined and documented
  • Existing MinIO Storage struct implements FileStore
  • Server uses FileStore interface, not concrete type
  • Config supports storage.backend field
  • Existing MinIO behavior completely unchanged — all file operations work as before
  • go build ./... passes with no regressions

Priority

P0 — blocks all other storage migration work

Part of

Storage Migration: MinIO → PostgreSQL + Filesystem

## Summary Extract the current MinIO-specific code in `internal/storage/storage.go` into a `FileStore` interface, enabling pluggable storage backends. ## Context Currently `internal/storage/storage.go` defines a concrete `Storage` struct wrapping `*minio.Client` with no interface abstraction. The `Server` struct in `internal/api/server.go` holds a `*storage.Storage` pointer directly, and all handlers (`file_handlers.go`, `handlers.go`) call methods on this concrete type. Nil-checks (`if s.storage == nil`) gate file operations when storage is unconfigured. ## Requirements Define a `FileStore` interface in `internal/storage/`: ```go type FileStore interface { Put(ctx context.Context, key string, reader io.Reader, size int64, contentType string) (*PutResult, error) Get(ctx context.Context, key string) (io.ReadCloser, error) GetVersion(ctx context.Context, key string, versionID string) (io.ReadCloser, error) Delete(ctx context.Context, key string) error Exists(ctx context.Context, key string) (bool, error) Copy(ctx context.Context, srcKey, dstKey string) error PresignPut(ctx context.Context, key string, expiry time.Duration) (*url.URL, error) Ping(ctx context.Context) error } ``` ### Implementation steps 1. Define `FileStore` interface in a new file `internal/storage/interface.go` 2. Verify the existing `Storage` struct satisfies the interface (it already has all these methods) 3. Add `Exists` method to `Storage` (currently missing — implement via `minio.StatObject`) 4. Update `Server` struct in `internal/api/server.go` to hold `storage.FileStore` instead of `*storage.Storage` 5. Update `NewServer()` constructor and all call sites 6. Update `cmd/silod/main.go` startup wiring — currently passes `*storage.Storage` (or nil) 7. Keep standalone functions `FileKey()` and `ThumbnailKey()` as package-level helpers (not on interface) 8. Extend `internal/config/config.go` `StorageConfig` with a `Backend` field: ```go type StorageConfig struct { Backend string `yaml:"backend"` // "minio" | "filesystem" Endpoint string `yaml:"endpoint"` // ... existing fields ... Filesystem FilesystemConfig `yaml:"filesystem"` } type FilesystemConfig struct { Root string `yaml:"root"` } ``` ### Files to modify - `internal/storage/storage.go` — add `Exists` method, ensure interface compliance - `internal/storage/interface.go` — new file with `FileStore` interface - `internal/api/server.go` — change `storage *storage.Storage` to `storage storage.FileStore` - `internal/api/file_handlers.go` — no changes needed if using interface (6 handlers: `HandlePresignUpload`, `HandleListItemFiles`, `HandleAssociateItemFile`, `HandleDeleteItemFile`, `HandleSetItemThumbnail`) - `internal/api/handlers.go` — `HandleUploadFile`, `HandleDownloadFile` use `s.storage.Put()`, `s.storage.Get()`, `s.storage.GetVersion()` - `internal/config/config.go` — add `Backend` and `Filesystem` config fields - `cmd/silod/main.go` — backend selection logic ### Current method signatures on `Storage` | Method | Signature | |--------|-----------| | `Put` | `(ctx, key, reader, size, contentType) (*PutResult, error)` | | `Get` | `(ctx, key) (io.ReadCloser, error)` | | `GetVersion` | `(ctx, key, versionID) (io.ReadCloser, error)` | | `Delete` | `(ctx, key) error` | | `Ping` | `(ctx) error` | | `PresignPut` | `(ctx, key, expiry) (*url.URL, error)` | | `Copy` | `(ctx, srcKey, dstKey) error` | | `Bucket` | `() string` | | `FileKey` | `(partNumber, revision) string` (standalone) | | `ThumbnailKey` | `(partNumber, revision) string` (standalone) | ## Acceptance criteria - [ ] `FileStore` interface defined and documented - [ ] Existing MinIO `Storage` struct implements `FileStore` - [ ] `Server` uses `FileStore` interface, not concrete type - [ ] Config supports `storage.backend` field - [ ] Existing MinIO behavior completely unchanged — all file operations work as before - [ ] `go build ./...` passes with no regressions ## Priority P0 — blocks all other storage migration work ## Part of Storage Migration: MinIO → PostgreSQL + Filesystem
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: kindred/silo#126