storage
import "github.com/nathabonfim59/pbvex/backend/internal/storage"Index
- Variables
- func AppFromContext(ctx context.Context) (core.App, bool)
- func GenerateAttempt() (string, error)
- func GenerateStorageID() (string, error)
- func GenerateToken() (string, error)
- func HashToken(token string) string
- func ValidateStorageID(id string) error
- func WithApp(ctx context.Context, app core.App) context.Context
- type AuthContext
- type Config
- type ErrorCode
- type FileRecord
- type ImageMetadata
- type ImagePolicy
- type Repo
- func NewRepo() *Repo
- func (r *Repo) BackfillPublicTokens(ctx context.Context, app core.App) error
- func (r *Repo) ClaimToken(ctx context.Context, app core.App, tokenHash, claim string, claimExpiresAt time.Time) (*core.Record, error)
- func (r *Repo) ConsumeToken(ctx context.Context, app core.App, tokenHash, claim string) error
- func (r *Repo) CreateFile(ctx context.Context, app core.App, record FileRecord) (*core.Record, error)
- func (r *Repo) CreateToken(ctx context.Context, app core.App, token TokenRecord) (*core.Record, error)
- func (r *Repo) DeleteExpiredTokens(ctx context.Context, app core.App, before time.Time) (int64, error)
- func (r *Repo) DeleteUploadingIfLeaseExpired(ctx context.Context, app core.App, id, owner string, before time.Time) (bool, error)
- func (r *Repo) GetActiveFilesCount(ctx context.Context, app core.App) (int64, error)
- func (r *Repo) GetFile(ctx context.Context, app core.App, storageID string) (*core.Record, error)
- func (r *Repo) GetFileByIDAnyStatus(ctx context.Context, app core.App, storageID string) (*core.Record, error)
- func (r *Repo) GetFileByKey(ctx context.Context, app core.App, fileKey string) (*core.Record, error)
- func (r *Repo) GetFileByPublicToken(ctx context.Context, app core.App, token string) (*core.Record, error)
- func (r *Repo) GetFilesByStatus(ctx context.Context, app core.App, status string) ([]*core.Record, error)
- func (r *Repo) GetTokenByHash(ctx context.Context, app core.App, tokenHash string) (*core.Record, error)
- func (r *Repo) GetTokenByHashAnyState(ctx context.Context, app core.App, tokenHash string) (*core.Record, error)
- func (r *Repo) GetTokenByStorageID(ctx context.Context, app core.App, storageID string) (*core.Record, error)
- func (r *Repo) HardDeleteFile(ctx context.Context, app core.App, storageID string) (string, error)
- func (r *Repo) MarkFileStatus(ctx context.Context, app core.App, storageID, status string) (*core.Record, error)
- func (r *Repo) ReleaseClaim(ctx context.Context, app core.App, tokenHash, claim string) error
- func (r *Repo) ReleaseReservation(ctx context.Context, app core.App, storageID, owner string) error
- func (r *Repo) RenewUploadLease(ctx context.Context, app core.App, storageID, owner string, until time.Time) error
- func (r *Repo) TransitionUploadingToStaged(ctx context.Context, app core.App, storageID, owner, sha string, size int64, fileKey, contentType string, metadata any) error
- type Service
- func NewService(app core.App, repo *Repo, config Config) (*Service, error)
- func (s *Service) Delete(ctx context.Context, storageID string) error
- func (s *Service) Download(w http.ResponseWriter, r *http.Request, storageID string, auth AuthContext) error
- func (s *Service) DownloadPublic(w http.ResponseWriter, r *http.Request, token string) error
- func (s *Service) GenerateImageUploadURL(ctx context.Context, auth AuthContext, policy ImagePolicy) (string, error)
- func (s *Service) GenerateUploadURL(ctx context.Context, auth AuthContext) (string, error)
- func (s *Service) GetCapabilityURL(ctx context.Context, storageID string) (string, error)
- func (s *Service) GetMetadata(ctx context.Context, storageID string) (map[string]any, error)
- func (s *Service) GetPublicURL(ctx context.Context, storageID string) (string, error)
- func (s *Service) GetURL(ctx context.Context, storageID string, auth AuthContext) (string, error)
- func (s *Service) RunCleanup() error
- func (s *Service) Start() error
- func (s *Service) Stop() error
- func (s *Service) Upload(ctx context.Context, token string, body io.Reader, contentType, filename string, headerSize int64) (string, error)
- func (s *Service) WarmActive() error
- type TokenRecord
- type UploadError
Variables
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
func AppFromContext(ctx context.Context) (core.App, bool)AppFromContext returns the app instance from the context, or nil/false if none.
func GenerateAttempt
func GenerateAttempt() (string, error)GenerateAttempt returns a random nonce for a single upload attempt.
func GenerateStorageID
func GenerateStorageID() (string, error)GenerateStorageID returns an opaque, non-path-leaking, branded storage identifier.
func GenerateToken
func GenerateToken() (string, error)GenerateToken returns a random, unforgeable upload token.
func HashToken
func HashToken(token string) stringHashToken returns the SHA-256 digest of a token string.
func ValidateStorageID
func ValidateStorageID(id string) errorValidateStorageID 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
func WithApp(ctx context.Context, app core.App) context.ContextWithApp 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.
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.
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
func DefaultConfig() ConfigDefaultConfig returns sane storage defaults.
func NormalizeConfig
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.
type ErrorCode stringconst (
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.
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
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
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.
type Repo struct{}func NewRepo
func NewRepo() *RepoNewRepo creates a new storage repository.
func (*Repo) BackfillPublicTokens
func (r *Repo) BackfillPublicTokens(ctx context.Context, app core.App) errorBackfillPublicTokens gives files created by older releases stable public tokens.
func (*Repo) ClaimToken
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
func (r *Repo) ConsumeToken(ctx context.Context, app core.App, tokenHash, claim string) errorConsumeToken atomically consumes a token only if the claim matches.
func (*Repo) CreateFile
func (r *Repo) CreateFile(ctx context.Context, app core.App, record FileRecord) (*core.Record, error)CreateFile creates a storage metadata record.
func (*Repo) CreateToken
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
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
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
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
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
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
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
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
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
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
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
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
func (r *Repo) HardDeleteFile(ctx context.Context, app core.App, storageID string) (string, error)HardDeleteFile removes a file metadata record.
func (*Repo) MarkFileStatus
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
func (r *Repo) ReleaseClaim(ctx context.Context, app core.App, tokenHash, claim string) errorReleaseClaim clears a claim for an attempt if the token is not consumed.
func (*Repo) ReleaseReservation
func (r *Repo) ReleaseReservation(ctx context.Context, app core.App, storageID, owner string) errorReleaseReservation 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
func (r *Repo) RenewUploadLease(ctx context.Context, app core.App, storageID, owner string, until time.Time) errorRenewUploadLease 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
func (r *Repo) TransitionUploadingToStaged(ctx context.Context, app core.App, storageID, owner, sha string, size int64, fileKey, contentType string, metadata any) errorTransitionUploadingToStaged 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.
type Service struct {
// contains filtered or unexported fields
}func NewService
func NewService(app core.App, repo *Repo, config Config) (*Service, error)NewService creates a new storage service.
func (*Service) Delete
func (s *Service) Delete(ctx context.Context, storageID string) errorDelete 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
func (s *Service) Download(w http.ResponseWriter, r *http.Request, storageID string, auth AuthContext) errorDownload serves a stored file for GET/HEAD requests.
func (*Service) DownloadPublic
func (s *Service) DownloadPublic(w http.ResponseWriter, r *http.Request, token string) errorDownloadPublic serves a stable public storage URL without caller authentication.
func (*Service) GenerateImageUploadURL
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
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
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
func (s *Service) GetMetadata(ctx context.Context, storageID string) (map[string]any, error)GetMetadata returns persisted metadata for a storage object.
func (*Service) GetPublicURL
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
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
func (s *Service) RunCleanup() errorRunCleanup executes a single cleanup pass synchronously. Useful for tests.
func (*Service) Start
func (s *Service) Start() errorStart begins the background cleanup worker. It is safe to call multiple times.
func (*Service) Stop
func (s *Service) Stop() errorStop halts the background cleanup worker and waits for the current pass to finish.
func (*Service) Upload
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
func (s *Service) WarmActive() errorWarmActive pre-loads signing keys and any other runtime state.
type TokenRecord
TokenRecord is the domain model for an upload token.
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.
type UploadError struct {
Code ErrorCode
Message string
Err error
}func (*UploadError) Error
func (e *UploadError) Error() stringfunc (*UploadError) Unwrap
func (e *UploadError) Unwrap() errorGenerated by gomarkdoc