Skip to content

storage

go
import "github.com/nathabonfim59/pbvex/backend/internal/storage"

Index

Variables

go
var (
    ErrStorageNotFound       = errors.New("storage file not found")
    ErrStorageDeleted        = errors.New("storage file already deleted")
    ErrInvalidStorageID      = errors.New("invalid storage id")
    ErrTokenExpired          = errors.New("upload token expired")
    ErrTokenConsumed         = errors.New("upload token already consumed")
    ErrTokenNotFound         = errors.New("upload token not found")
    ErrTokenClaimFailed      = errors.New("upload token claim failed")
    ErrUploadTooLarge        = errors.New("upload exceeds maximum allowed size")
    ErrContentTypeNotAllowed = errors.New("content type not allowed")
    ErrMalformedContentType  = errors.New("malformed content type")
    ErrInvalidFilename       = errors.New("invalid filename")
    ErrURLTampered           = errors.New("signed url is invalid or tampered")
    ErrURLExpired            = errors.New("signed url expired")
    ErrURLForbidden          = errors.New("signed url does not match caller")
    ErrStorageDataLost       = errors.New("storage file data lost")
    ErrReservationLost       = errors.New("storage upload reservation lost")
)

func AppFromContext

go
func AppFromContext(ctx context.Context) (core.App, bool)

AppFromContext returns the app instance from the context, or nil/false if none.

func GenerateAttempt

go
func GenerateAttempt() (string, error)

GenerateAttempt returns a random nonce for a single upload attempt.

func GenerateStorageID

go
func GenerateStorageID() (string, error)

GenerateStorageID returns an opaque, non-path-leaking, branded storage identifier.

func GenerateToken

go
func GenerateToken() (string, error)

GenerateToken returns a random, unforgeable upload token.

func HashToken

go
func HashToken(token string) string

HashToken returns the SHA-256 digest of a token string.

func ValidateStorageID

go
func ValidateStorageID(id string) error

ValidateStorageID enforces the canonical branded storage id form ("pbv_" + 32 hex). It rejects empty, oversized, path-bearing, and non-branded ids without leaking paths.

func WithApp

go
func WithApp(ctx context.Context, app core.App) context.Context

WithApp returns a context carrying the PocketBase app instance to use for storage operations. This allows operations to participate in an outer transaction.

type AuthContext

AuthContext is the caller identity carried through to storage operations. UserID binds signed URLs and supplies audit metadata; it does not impose automatic file ownership. Application functions remain responsible for deciding who may request a URL or delete a StorageID.

go
type AuthContext struct {
    IsAuthenticated bool
    TokenIdentifier string
    // UserID is retained for source compatibility with existing embedders. New
    // PBVex request paths always use TokenIdentifier.
    UserID string
}

type Config

Config controls storage service behavior and constraints.

go
type Config struct {
    // MaxFileSize is the hard upper bound for a single file upload.
    MaxFileSize int64
    // DefaultUploadTTL is the default expiry for generated upload URLs.
    DefaultUploadTTL time.Duration
    // DefaultClaimTTL is the maximum time an upload attempt may hold a token claim.
    DefaultClaimTTL time.Duration
    // AllowedContentTypes is a list of allowed MIME type patterns. Empty allows all.
    // Each pattern is either an exact type (e.g. "image/png") or a wildcard suffix (e.g. "image/*").
    AllowedContentTypes []string
    // BasePath is the base API path used when building URLs.
    BasePath string
    // BaseURL is the absolute base URL for generated storage URLs. If empty, falls back to AppURL.
    BaseURL string
    // FileStoragePrefix is the object-key prefix used by the filesystem backend.
    FileStoragePrefix string
    // DefaultTokenMaxSize overrides MaxFileSize per token if non-zero.
    DefaultTokenMaxSize int64
    // CleanupInterval is the interval between background cleanup worker passes.
    CleanupInterval time.Duration
    // URLSigningTTL is the default lifetime for signed download URLs.
    URLSigningTTL time.Duration
    // URLSigningMaxTTL is the absolute maximum lifetime a signed URL may request.
    URLSigningMaxTTL time.Duration
    // PublicCacheTTL controls browser and shared-cache freshness for stable public URLs.
    PublicCacheTTL time.Duration
    // KeyRotationInterval controls how often signing keys are rotated.
    KeyRotationInterval time.Duration
    // KeyGracePeriod is how long rotated-out keys stay available for verification.
    KeyGracePeriod time.Duration
    // MaxFiles is the maximum number of active stored files. 0 means unlimited.
    MaxFiles int64
    // UploadLeaseInterval is the validity window of an uploading reservation
    // lease. Active uploads renew it periodically; cleanup reclaims a
    // reservation only once its lease expires without renewal.
    UploadLeaseInterval time.Duration
}

func DefaultConfig

go
func DefaultConfig() Config

DefaultConfig returns sane storage defaults.

func NormalizeConfig

go
func NormalizeConfig(cfg Config) (Config, error)

NormalizeConfig fills missing fields with defaults and validates values.

type ErrorCode

ErrorCode mirrors the deploy error code set for protocol consistency.

go
type ErrorCode string

go
const (
    ErrorCodeBadRequest     ErrorCode = "bad_request"
    ErrorCodeNotFound       ErrorCode = "not_found"
    ErrorCodeUnauthorized   ErrorCode = "unauthorized"
    ErrorCodeForbidden      ErrorCode = "forbidden"
    ErrorCodeInternal       ErrorCode = "internal"
    ErrorCodeUploadExpired  ErrorCode = "upload_expired"
    ErrorCodeUploadConsumed ErrorCode = "upload_consumed"
    ErrorCodeUploadTooLarge ErrorCode = "upload_too_large"
    ErrorCodeInvalidContent ErrorCode = "invalid_content"
    ErrorCodeStorageFull    ErrorCode = "storage_full"
    ErrorCodeUploadPending  ErrorCode = "upload_pending"
)

type FileRecord

FileRecord is the domain model for a stored file.

go
type FileRecord struct {
    StorageID   string
    Sha256      string
    Size        int64
    ContentType string
    FileKey     string
    Filename    string
    CreatedBy   string
    Status      string
    Owner       string
    LeaseUntil  time.Time
    PublicToken string
    Metadata    any
}

type ImageMetadata

go
type ImageMetadata struct {
    Kind      string   `json:"kind"`
    Extension string   `json:"extension"`
    Width     int      `json:"width"`
    Height    int      `json:"height"`
    Thumbs    []string `json:"thumbs"`
    MimeTypes []string `json:"mimeTypes"`
}

type ImagePolicy

go
type ImagePolicy struct {
    Kind      string   `json:"kind"`
    Thumbs    []string `json:"thumbs"`
    MimeTypes []string `json:"mimeTypes"`
}

type Repo

Repo provides persistence access for storage metadata and tokens.

go
type Repo struct{}

func NewRepo

go
func NewRepo() *Repo

NewRepo creates a new storage repository.

func (*Repo) BackfillPublicTokens

go
func (r *Repo) BackfillPublicTokens(ctx context.Context, app core.App) error

BackfillPublicTokens gives files created by older releases stable public tokens.

func (*Repo) ClaimToken

go
func (r *Repo) ClaimToken(ctx context.Context, app core.App, tokenHash, claim string, claimExpiresAt time.Time) (*core.Record, error)

ClaimToken atomically CAS a token from unclaimed to claimed by attempt.

func (*Repo) ConsumeToken

go
func (r *Repo) ConsumeToken(ctx context.Context, app core.App, tokenHash, claim string) error

ConsumeToken atomically consumes a token only if the claim matches.

func (*Repo) CreateFile

go
func (r *Repo) CreateFile(ctx context.Context, app core.App, record FileRecord) (*core.Record, error)

CreateFile creates a storage metadata record.

func (*Repo) CreateToken

go
func (r *Repo) CreateToken(ctx context.Context, app core.App, token TokenRecord) (*core.Record, error)

CreateToken stores a new upload token. TokenHash is the digest that is persisted.

func (*Repo) DeleteExpiredTokens

go
func (r *Repo) DeleteExpiredTokens(ctx context.Context, app core.App, before time.Time) (int64, error)

DeleteExpiredTokens removes tokens whose expiry has passed.

func (*Repo) DeleteUploadingIfLeaseExpired

go
func (r *Repo) DeleteUploadingIfLeaseExpired(ctx context.Context, app core.App, id, owner string, before time.Time) (bool, error)

DeleteUploadingIfLeaseExpired atomically hard-deletes an uploading reservation only if its lease has expired (leaseUntil < before) AND it is still owned by the snapshotted owner. The atomic status+owner+lease guard means a concurrent renewal that extended the lease, or an owner takeover, causes this to affect zero rows, so cleanup cannot reclaim an actively-renewed or re-owned upload. Returns true when the reservation was reclaimed.

func (*Repo) GetActiveFilesCount

go
func (r *Repo) GetActiveFilesCount(ctx context.Context, app core.App) (int64, error)

GetActiveFilesCount returns the number of file records that consume storage capacity (uploading, staged, active, or deleting). Deleted records are not counted.

func (*Repo) GetFile

go
func (r *Repo) GetFile(ctx context.Context, app core.App, storageID string) (*core.Record, error)

GetFile returns a non-deleted storage file metadata record by storageId.

func (*Repo) GetFileByIDAnyStatus

go
func (r *Repo) GetFileByIDAnyStatus(ctx context.Context, app core.App, storageID string) (*core.Record, error)

GetFileByIDAnyStatus returns a file record by storage ID regardless of status.

func (*Repo) GetFileByKey

go
func (r *Repo) GetFileByKey(ctx context.Context, app core.App, fileKey string) (*core.Record, error)

GetFileByKey returns a non-deleted file by its backend file key.

func (*Repo) GetFileByPublicToken

go
func (r *Repo) GetFileByPublicToken(ctx context.Context, app core.App, token string) (*core.Record, error)

GetFileByPublicToken returns an active storage file for its stable public token.

func (*Repo) GetFilesByStatus

go
func (r *Repo) GetFilesByStatus(ctx context.Context, app core.App, status string) ([]*core.Record, error)

GetFilesByStatus returns all file records with the given status.

func (*Repo) GetTokenByHash

go
func (r *Repo) GetTokenByHash(ctx context.Context, app core.App, tokenHash string) (*core.Record, error)

GetTokenByHash returns a non-consumed token by its digest.

func (*Repo) GetTokenByHashAnyState

go
func (r *Repo) GetTokenByHashAnyState(ctx context.Context, app core.App, tokenHash string) (*core.Record, error)

GetTokenByHashAnyState returns a token by its digest regardless of consumed state. It is used to classify a failed claim into expired, consumed, or in-use so that callers can surface a precise error instead of a generic rejection.

func (*Repo) GetTokenByStorageID

go
func (r *Repo) GetTokenByStorageID(ctx context.Context, app core.App, storageID string) (*core.Record, error)

GetTokenByStorageID returns any token for the given storage ID.

func (*Repo) HardDeleteFile

go
func (r *Repo) HardDeleteFile(ctx context.Context, app core.App, storageID string) (string, error)

HardDeleteFile removes a file metadata record.

func (*Repo) MarkFileStatus

go
func (r *Repo) MarkFileStatus(ctx context.Context, app core.App, storageID, status string) (*core.Record, error)

MarkFileStatus updates the status of a file record and returns its file key.

func (*Repo) ReleaseClaim

go
func (r *Repo) ReleaseClaim(ctx context.Context, app core.App, tokenHash, claim string) error

ReleaseClaim clears a claim for an attempt if the token is not consumed.

func (*Repo) ReleaseReservation

go
func (r *Repo) ReleaseReservation(ctx context.Context, app core.App, storageID, owner string) error

ReleaseReservation hard-deletes an uploading reservation owned by owner. The CAS guard makes it idempotent and safe against a record already reclaimed or committed by another path.

func (*Repo) RenewUploadLease

go
func (r *Repo) RenewUploadLease(ctx context.Context, app core.App, storageID, owner string, until time.Time) error

RenewUploadLease atomically extends the lease on an uploading reservation owned by owner. It is a CAS: the update only applies while the record is still uploading and owned by owner, so it cannot clobber a record that cleanup reclaimed or commit transitioned. It returns ErrReservationLost when the reservation no longer matches.

func (*Repo) TransitionUploadingToStaged

go
func (r *Repo) TransitionUploadingToStaged(ctx context.Context, app core.App, storageID, owner, sha string, size int64, fileKey, contentType string, metadata any) error

TransitionUploadingToStaged atomically moves a reservation from uploading to staged with the finalized metadata. The CAS (status=uploading AND owner) ensures it cannot transition a record that cleanup reclaimed or another owner took. Returns ErrReservationLost when the reservation no longer matches.

type Service

Service is the application layer for file storage.

go
type Service struct {
    // contains filtered or unexported fields
}

func NewService

go
func NewService(app core.App, repo *Repo, config Config) (*Service, error)

NewService creates a new storage service.

func (*Service) Delete

go
func (s *Service) Delete(ctx context.Context, storageID string) error

Delete removes a stored file and its metadata. When called inside a transaction, it marks the file as deleting and schedules the irreversible blob deletion in TxInfo.OnComplete after successful commit.

func (*Service) Download

go
func (s *Service) Download(w http.ResponseWriter, r *http.Request, storageID string, auth AuthContext) error

Download serves a stored file for GET/HEAD requests.

func (*Service) DownloadPublic

go
func (s *Service) DownloadPublic(w http.ResponseWriter, r *http.Request, token string) error

DownloadPublic serves a stable public storage URL without caller authentication.

func (*Service) GenerateImageUploadURL

go
func (s *Service) GenerateImageUploadURL(ctx context.Context, auth AuthContext, policy ImagePolicy) (string, error)

GenerateImageUploadURL creates an upload URL bound to a schema image policy.

func (*Service) GenerateUploadURL

go
func (s *Service) GenerateUploadURL(ctx context.Context, auth AuthContext) (string, error)

GenerateUploadURL returns a short-lived, single-use URL for uploading a file.

func (*Service) GetCapabilityURL

go
func (s *Service) GetCapabilityURL(ctx context.Context, storageID string) (string, error)

GetCapabilityURL returns a signed short-lived bearer URL that does not require caller authentication.

func (*Service) GetMetadata

go
func (s *Service) GetMetadata(ctx context.Context, storageID string) (map[string]any, error)

GetMetadata returns persisted metadata for a storage object.

func (*Service) GetPublicURL

go
func (s *Service) GetPublicURL(ctx context.Context, storageID string) (string, error)

GetPublicURL returns the stable public bearer URL for a stored file.

func (*Service) GetURL

go
func (s *Service) GetURL(ctx context.Context, storageID string, auth AuthContext) (string, error)

GetURL returns a signed short-lived download URL for the storage ID, or an empty string if missing/deleted.

func (*Service) RunCleanup

go
func (s *Service) RunCleanup() error

RunCleanup executes a single cleanup pass synchronously. Useful for tests.

func (*Service) Start

go
func (s *Service) Start() error

Start begins the background cleanup worker. It is safe to call multiple times.

func (*Service) Stop

go
func (s *Service) Stop() error

Stop halts the background cleanup worker and waits for the current pass to finish.

func (*Service) Upload

go
func (s *Service) Upload(ctx context.Context, token string, body io.Reader, contentType, filename string, headerSize int64) (string, error)

Upload streams and persists a file from an upload token. The commit creates a staged file record, then OnComplete moves the staged blob to the final key and marks the file active. If the transaction fails, the claim is released and the staged blob is removed.

func (*Service) WarmActive

go
func (s *Service) WarmActive() error

WarmActive pre-loads signing keys and any other runtime state.

type TokenRecord

TokenRecord is the domain model for an upload token.

go
type TokenRecord struct {
    TokenHash    string
    StorageID    string
    ExpiresAt    time.Time
    CreatedBy    string
    MaxSize      int64
    AllowedTypes []string
    Filename     string
    Policy       any
}

type UploadError

UploadError is a typed validation error for storage uploads.

go
type UploadError struct {
    Code    ErrorCode
    Message string
    Err     error
}

func (*UploadError) Error

go
func (e *UploadError) Error() string

func (*UploadError) Unwrap

go
func (e *UploadError) Unwrap() error

Generated by gomarkdoc

Generated API reference. Source of truth is the codebase.