objects

package
v0.46.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 21, 2025 License: Apache-2.0 Imports: 19 Imported by: 0

README

Object Storage

The pkg/objects module provides a modular, provider‑agnostic object storage layer. It exposes:

  • A minimal set of top‑level aliases so consumers can work with a single package (objects.File, objects.UploadOptions, etc.)
  • A provider interface and concrete providers for S3, Cloudflare R2, local disk, and database
  • A small, focused storage.ObjectService that performs Upload/Download/Delete/Exists using any provider that implements the interface
  • Utilities for MIME detection, safe buffering of uploads, and document parsing

This README is written for engineers integrating object storage into their services or CLIs, and explains how to:

  • Choose and configure providers
  • Upload, download, delete files, and generate presigned URLs
  • Pass provider hints for dynamic selection
  • Understand how dynamic and concurrent provider resolution works in this codebase

Quickstart

Create a provider and use storage.ObjectService to upload and download files. The service is stateless and thread‑safe; it relies on the provider for IO.

package main

import (
    "context"
    "strings"

    "github.com/theopenlane/core/pkg/objects"
    storage "github.com/theopenlane/core/pkg/objects/storage"
    disk "github.com/theopenlane/core/pkg/objects/storage/providers/disk"
)

func main() {
    // 1) Choose a provider (disk example)
    opts := storage.NewProviderOptions(
        storage.WithBucket("./uploads"),    // local folder for disk provider
        storage.WithBasePath("./uploads"),
        storage.WithLocalURL("http://localhost:8080/files"), // optional, for presigned links
    )
    provider, err := disk.NewDiskProvider(opts)
    if err != nil { panic(err) }
    defer provider.Close()

    // 2) Use the object service with the provider
    svc := storage.NewObjectService()

    // 3) Upload
    content := strings.NewReader("hello world")
    file, err := svc.Upload(context.Background(), provider, content, &storage.UploadOptions{
        FileName:    "greeting.txt",
        ContentType: "text/plain; charset=utf-8",
        FileMetadata: storage.FileMetadata{ Key: "uploadFile" },
    })
    if err != nil { panic(err) }

    // 4) Download
    downloaded, err := svc.Download(context.Background(), provider, &storage.File{
        FileMetadata: storage.FileMetadata{ Key: file.Key, Bucket: file.Folder },
    }, &storage.DownloadOptions{})
    if err != nil { panic(err) }
    _ = downloaded // bytes and metadata
}

Key Concepts

  • storage.Provider is the interface every backend implements. It supports Upload, Download, Delete, GetPresignedURL, and Exists.
  • storage.ObjectService is a thin layer over a given provider that implements core operations. It does not resolve providers itself.
  • objects.File holds metadata about files (IDs, names, parent object, provider hints, etc.)
  • storage.UploadOptions and storage.DownloadOptions capture request‑specific inputs (file name, content type, bucket/path, hints, and metadata)
  • storage.ProviderHints lets you steer provider selection in environments that support dynamic resolution
  • ReaderToSeeker, NewBufferedReaderFromReader, and DetectContentType help you safely stream and classify uploads
  • ParseDocument extracts textual or structured content from common files (DOCX, JSON, YAML, plaintext)

Built‑in Providers

All providers implement the same interface and are safe to use concurrently from goroutines. Construct them with provider‑specific options and credentials.

  • Disk (providers/disk)

    • Stores files on local filesystem paths for development/testing
    • Options: WithBucket, WithBasePath, optional WithLocalURL for presigned URLs
    • Example: disk.NewDiskProvider(options)
  • Amazon S3 (providers/s3)

    • Options: WithBucket, WithRegion, WithEndpoint (minio/alt endpoints)
    • Build via NewS3Provider(options, ...) or NewS3ProviderResult(...).Get()
    • Credentials can come from ProviderOptions.Credentials or environment
  • Cloudflare R2 (providers/r2)

    • Similar to S3 in usage, supports account‑specific credentials and endpoints
  • Database (providers/database)

    • Stores file bytes in the database; useful for low‑volume or migration scenarios

Operations

Upload
seeker, _ := objects.ReaderToSeeker(reader)               // optional, ensures re‑readable stream
contentType, _ := storage.DetectContentType(seeker)       // optional, defaults to application/octet-stream for empty inputs

file, err := svc.Upload(ctx, provider, seeker, &storage.UploadOptions{
    FileName:    "report.pdf",
    ContentType: contentType,
    Bucket:      "reports",
    FileMetadata: storage.FileMetadata{
        Key:           "uploadFile",
        ProviderHints: &storage.ProviderHints{ PreferredProvider: storage.S3Provider },
    },
})
Download
meta, err := svc.Download(ctx, provider, &storage.File{
    ID: file.ID,
    FileMetadata: storage.FileMetadata{ Key: file.Key, Bucket: file.Folder },
}, &storage.DownloadOptions{ FileName: file.OriginalName })
Presigned URL
url, err := svc.GetPresignedURL(ctx, provider, &storage.File{ FileMetadata: storage.FileMetadata{ Key: file.Key, Bucket: file.Folder } }, &storage.PresignedURLOptions{ Duration: 15 * time.Minute })
Delete & Exists
_ = svc.Delete(ctx, provider, &storage.File{ FileMetadata: storage.FileMetadata{ Key: file.Key, Bucket: file.Folder } }, &storage.DeleteFileOptions{ Reason: "cleanup" })
exists, _ := provider.Exists(ctx, &storage.File{ FileMetadata: storage.FileMetadata{ Key: file.Key, Bucket: file.Folder } })

Storage Structure

Files are stored with the following pattern:

s3://bucket-name/organization-id/file-id/filename.ext
r2://bucket-name/organization-id/file-id/filename.ext
database://default/file-id (database provider uses file ID, not paths)
Example

For organization 01HYQZ5YTVJ0P2R2HF7N3W3MQZ, and file record 01J1FILEXYZABCD5678 uploading report.pdf:

s3://my-bucket/01HYQZ5YTVJ0P2R2HF7N3W3MQZ/01J1FILEXYZABCD5678/report.pdf
Implementation

When a file is uploaded through HandleUploads:

  1. The organization ID is derived from the authenticated context or the persisted file record.
  2. Metadata is persisted, returning the stored file record (including its database ID) and owning organization.
  3. A folder path is built as orgID/fileID
  4. The computed folder is passed as FolderDestination in upload options.
  5. Storage providers use path.Join(FolderDestination, FileName) to construct the object key.

Code: internal/objects/upload/handler.go

entFile, ownerOrgID, err := store.CreateFileRecord(ctx, file)
// ...
folderPath := buildStorageFolderPath(ownerOrgID, file, entFile.ID)
if folderPath != "" {
    uploadOpts.FolderDestination = folderPath
    file.Folder = folderPath
}
Provider Implementation

S3 Provider: pkg/objects/storage/providers/s3/provider.go

func (p *Provider) Upload(ctx context.Context, reader io.Reader, opts *storagetypes.UploadFileOptions) (*storagetypes.UploadedFileMetadata, error) {
    objectKey := opts.FileName
    if opts.FolderDestination != "" {
        objectKey = path.Join(opts.FolderDestination, opts.FileName)
    }
    // Upload with objectKey as the full path
}

R2 Provider: pkg/objects/storage/providers/r2/provider.go

  • Same implementation as S3

Database Provider: pkg/objects/storage/providers/database/provider.go

  • Stores files by file ID directly in database
  • No folder structure concept (files are stored as binary blobs)
Download Flow

When downloading files, the full key (including organization prefix) is stored in the database and used for retrieval:

// File record in database
StoragePath: "01HYQZ5YTVJ0P2R2HF7N3W3MQZ/01J1FILEXYZABCD5678/report.pdf"

// Used for download
provider.Download(ctx, &storagetypes.File{
    Key: file.StoragePath,  // Full path including organization
})

Provider Hints and Dynamic Selection

storage.ProviderHints lets you carry metadata that a resolver can use to choose a storage backend at runtime. Common fields include:

  • KnownProvider or PreferredProvider to suggest a backend (e.g., s3, r2, disk)
  • OrganizationID, IntegrationID, HushID to route per tenant or integration
  • Module and free‑form Metadata for feature‑level routing

Hints flow through UploadOptions.FileMetadata.ProviderHints and are copied into the resulting objects.File. Your resolver can read these hints and select an appropriate provider for each request.

Dynamic & Concurrent Providers: How It Works Here

In this repository, dynamic provider selection and concurrency are handled by an orchestration layer that wraps storage.ObjectService:

  • A resolver (built with the eddy library) looks at request context + ProviderHints to pick the right provider builder and runtime options
  • A client pool caches and reuses provider clients per tenant and integration (ProviderCacheKey) to avoid reconnect churn
  • The orchestrator (internal/objects/Service) then delegates actual IO to storage.ObjectService with the resolved provider
  • This makes provider selection dynamic per request and safe under high concurrency

If you want a similar setup in your own project:

  1. Define a ProviderBuilder for each backend you support, capable of reading configuration + hints and returning a storage.Provider

  2. Use a resolver to map (context, hints) -> (builder + runtime config)

  3. Use a thread‑safe client pool to cache provider instances by a stable key (e.g., org + integration), so concurrent requests reuse clients

  4. Wrap storage.ObjectService to consume the resolved provider. The ObjectService is intentionally small and stateless to make this easy to compose

Within this repo, see internal/objects/service.go and internal/objects/resolver for a reference implementation.

Validation & Readiness

You can validate providers at startup by calling ListBuckets or performing a trivial Exists check. In this repository, we expose helper validators that:

  • Run best‑effort validation for all configured providers to surface misconfiguration early
  • Optionally enforce provider‑specific availability via a per‑provider ensureAvailable flag in configuration

See internal/objects/validators for examples.

Upload Utilities

  • objects.NewBufferedReaderFromReader and objects.ReaderToSeeker wrap streams to support re‑reads for MIME detection and upload retries
  • storage.DetectContentType detects MIME types and safely defaults to application/octet-stream for empty input
  • storage.ParseDocument extracts content from DOCX/JSON/YAML/plaintext for downstream processing

Error Handling

Provider implementations may return backend‑specific errors. The ObjectService surfaces these errors directly. For best UX:

  • Detect content types before upload (or let the service detect for you)
  • Use small in‑memory buffers for common cases; fall back to temp files for large streams
  • Record provider hints so dynamic resolution can route requests consistently

Configuration Reference

The repository includes a strongly typed configuration model (see pkg/objects/storage/types.go) suitable for wiring up providers in servers. Key fields:

  • ProviderConfig: global flags (enabled, keys, size limits), plus a Providers section per backend
  • Providers.S3|CloudflareR2|GCS|Disk|Database: enable flags, credentials, bucket/region/endpoint, and an ensureAvailable boolean for strict startup checks

FAQ

  • Can I upload to multiple providers at once

    • Yes, orchestrate multiple calls to ObjectService.Upload with different providers; the service is stateless and supports concurrent use
  • How do I choose a provider per organization

    • Pass ProviderHints with an OrganizationID and build a resolver that maps orgs/integrations to providers. The internal orchestration layer demonstrates this pattern
  • Do I need to use the internal orchestration layer

    • No. If you already know which provider to use, construct it directly and use storage.ObjectService. The dynamic bits are optional

Documentation

Overview

Package objects provides a clean, modern object storage service with dynamic multi-provider support, context-based client injection, and integration with external credential systems. This package supersedes the original objects package with better separation of concerns and support for per-tenant storage providers.

Index

Constants

View Source
const (
	// MaxInMemorySize is the maximum file size we'll buffer in memory (10MB)
	MaxInMemorySize = 10 * 1024 * 1024
)

Variables

View Source
var (
	// ErrNoStorageProvider is returned when no storage provider is available
	ErrNoStorageProvider = errors.New("no storage provider available")
	// ErrInsufficientProviderInfo is returned when insufficient information to resolve storage client
	ErrInsufficientProviderInfo = errors.New("insufficient information to resolve storage client: need integration_id+hush_id or organization_id")
	// ErrProviderHintsRequired is returned when provider hints are required for file upload
	ErrProviderHintsRequired = errors.New("provider hints required for file upload")
	// ErrMutationIDNotFound is returned when mutation ID is not found
	ErrMutationIDNotFound = errors.New("mutation ID not found")
	// ErrReaderCannotBeNil is returned when a nil reader is provided to BufferedReader
	ErrReaderCannotBeNil = errors.New("reader cannot be nil")
	// ErrFailedToReadData is returned when reading data from a reader fails
	ErrFailedToReadData = errors.New("failed to read data from reader")
	// ErrFileSizeExceedsLimit is returned when file size exceeds the specified limit
	ErrFileSizeExceedsLimit = errors.New("file size exceeds limit")
	// ErrUnsupportedMimeType is returned when an unsupported mime type is uploaded
	ErrUnsupportedMimeType = errors.New("unsupported mime type uploaded")
)

Functions

func AddUpload added in v0.39.0

func AddUpload()

AddUpload increments the upload wait group

func DoneUpload added in v0.39.0

func DoneUpload()

DoneUpload decrements the upload wait group

func GetFileIDsFromContext

func GetFileIDsFromContext(ctx context.Context) []string

GetFileIDsFromContext returns the file IDs from the context that are associated with the request

func InferReaderSize added in v0.38.1

func InferReaderSize(r io.Reader) (int64, bool)

InferReaderSize attempts to determine the total size of the provided reader without modifying its current position. It returns the reported size and true when available.

func ParseFilesFromSource added in v0.39.0

func ParseFilesFromSource[T FileSource](source T, keys ...string) (map[string][]File, error)

ParseFilesFromSource extracts files from any source using generics

func ProcessFilesForMutation added in v0.39.0

func ProcessFilesForMutation[T Mutation](ctx context.Context, mutation T, key string, parentType ...string) (context.Context, error)

ProcessFilesForMutation is a generic helper for ent hooks that: 1. Gets files from context using the provided key 2. Updates files with parent information from mutation 3. Updates context with modified files This replaces the pattern of individual checkXXXFile functions

func ReaderToSeeker

func ReaderToSeeker(r io.Reader) (io.ReadSeeker, error)

ReaderToSeeker function takes an io.Reader as input and returns an io.ReadSeeker which can be used to upload files to the object storage If the reader is already a ReadSeeker (e.g., BufferedReader from injectFileUploader), it returns it directly. For files under MaxInMemorySize (10MB), it uses in-memory buffering for efficiency. For larger files, it falls back to temporary file storage.

func RemoveFileFromContext added in v0.3.1

func RemoveFileFromContext(ctx context.Context, f File) context.Context

RemoveFileFromContext removes the file from the context based on the file ID

func UpdateFileInContextByKey added in v0.3.1

func UpdateFileInContextByKey(ctx context.Context, key string, f File) context.Context

UpdateFileInContextByKey updates the file in the context based on the key and the file ID

func WaitForUploads added in v0.39.0

func WaitForUploads()

WaitForUploads waits for all in-flight uploads to complete

func WriteFilesToContext

func WriteFilesToContext(ctx context.Context, f Files) context.Context

WriteFilesToContext retrieves any existing files from the context, appends the new files to the existing files map based on the form field name, then returns a new context with the updated files map stored in it

Types

type BufferedReader added in v0.38.1

type BufferedReader struct {
	// contains filtered or unexported fields
}

BufferedReader wraps file data and provides both Reader and ReadSeeker interfaces

func NewBufferedReader added in v0.38.1

func NewBufferedReader(data []byte) *BufferedReader

NewBufferedReader creates a BufferedReader from raw data

func NewBufferedReaderFromReader added in v0.38.1

func NewBufferedReaderFromReader(r io.Reader) (*BufferedReader, error)

NewBufferedReaderFromReader creates a BufferedReader from an io.Reader This is the robust method for handling inbound file data that can work with all providers It buffers files up to MaxInMemorySize - if the file exceeds this, it returns an error indicating the caller should use disk-based buffering instead

func NewBufferedReaderFromReaderWithLimit added in v0.38.1

func NewBufferedReaderFromReaderWithLimit(r io.Reader, maxSize int64) (*BufferedReader, error)

NewBufferedReaderFromReaderWithLimit creates a BufferedReader from an io.Reader with a size limit

func (*BufferedReader) Close added in v0.38.1

func (br *BufferedReader) Close() error

Close implements io.Closer (no-op for memory buffer)

func (*BufferedReader) Data added in v0.38.1

func (br *BufferedReader) Data() []byte

Data returns a copy of the underlying data

func (*BufferedReader) NewReadSeeker added in v0.38.1

func (br *BufferedReader) NewReadSeeker() io.ReadSeeker

NewReadSeeker creates a new independent ReadSeeker from the buffered data

func (*BufferedReader) NewReader added in v0.38.1

func (br *BufferedReader) NewReader() io.Reader

NewReader creates a new independent reader from the buffered data

func (*BufferedReader) Read added in v0.38.1

func (br *BufferedReader) Read(p []byte) (n int, err error)

Read implements io.Reader

func (*BufferedReader) Reset added in v0.38.1

func (br *BufferedReader) Reset()

Reset resets the reader to the beginning

func (*BufferedReader) Seek added in v0.38.1

func (br *BufferedReader) Seek(offset int64, whence int) (int64, error)

Seek implements io.Seeker

func (*BufferedReader) Size added in v0.38.1

func (br *BufferedReader) Size() int64

Size returns the total size of the buffered data

type DownloadOptions added in v0.39.0

type DownloadOptions = storage.DownloadOptions

File aliases storage.File so callers can reference a single top-level type.

type File

type File = storage.File

File aliases storage.File so callers can reference a single top-level type.

func FilesFromContextWithKey

func FilesFromContextWithKey(ctx context.Context, key string) ([]File, error)

FilesFromContextWithKey returns all files that have been uploaded during the request and sorts by the provided form field

type FileContextKey

type FileContextKey struct {
	Files Files
}

FileContextKey is the context key for the files This is the key that is used to store the files in the context, which is then used to retrieve the files in subsequent parts of the request this is different than the `key` in the multipart form, which is the form field name that the file was uploaded with

type FileMetadata added in v0.39.0

type FileMetadata = storage.FileMetadata

File aliases storage.File so callers can reference a single top-level type.

type FileSource added in v0.39.0

type FileSource interface {
	~map[string]any | ~*http.Request | ~*multipart.Form
}

FileSource represents any source that can provide file uploads. The tilde (~) allows for types that are identical or aliases to the specified types.

type Files

type Files = storage.Files

File aliases storage.File so callers can reference a single top-level type.

func FilesFromContext

func FilesFromContext(ctx context.Context) (Files, error)

FilesFromContext returns all files that have been uploaded during the request

type GenericMutationAdapter added in v0.39.0

type GenericMutationAdapter[T any] struct {
	// contains filtered or unexported fields
}

GenericMutationAdapter adapts existing ent GenericMutation interface to our Mutation interface

func (*GenericMutationAdapter[T]) ID added in v0.39.0

func (a *GenericMutationAdapter[T]) ID() (string, error)

ID implements the Mutation interface

func (*GenericMutationAdapter[T]) Type added in v0.39.0

func (a *GenericMutationAdapter[T]) Type() string

Type implements the Mutation interface

type LenReader added in v0.38.1

type LenReader interface {
	Len() int
}

LenReader describes readers that expose remaining length semantics (e.g. *bytes.Reader).

type Mutation added in v0.39.0

type Mutation interface {
	ID() (string, error)
	Type() string
}

Mutation represents any ent mutation that can provide ID and Type

func NewGenericMutationAdapter added in v0.39.0

func NewGenericMutationAdapter[T any](mutation T, idFunc func(T) (string, bool), typeFunc func(T) string) Mutation

NewGenericMutationAdapter creates an adapter for existing ent mutations

type ParentObject added in v0.3.1

type ParentObject = storage.ParentObject

File aliases storage.File so callers can reference a single top-level type.

type ProviderHints added in v0.39.0

type ProviderHints = storage.ProviderHints

File aliases storage.File so callers can reference a single top-level type.

type SizedReader added in v0.38.1

type SizedReader interface {
	Size() int64
}

SizedReader describes readers that can report their size without consuming the stream.

type StatReader added in v0.38.1

type StatReader interface {
	Stat() (fs.FileInfo, error)
}

StatReader describes readers backed by file descriptors that can return stat information.

type UploadOptions added in v0.39.0

type UploadOptions = storage.UploadOptions

File aliases storage.File so callers can reference a single top-level type.

type ValidationFunc

type ValidationFunc func(f File) error

ValidationFunc is a type that can be used to dynamically validate a file

func ChainValidators

func ChainValidators(validators ...ValidationFunc) ValidationFunc

ChainValidators returns a validator that accepts multiple validating criteria

func MimeTypeValidator

func MimeTypeValidator(validMimeTypes ...string) ValidationFunc

MimeTypeValidator makes sure we only accept a valid mimetype. It takes in an array of supported mimes MimeTypeValidator is a validator factory that ensures the file's content type matches one of the provided types When validation fails it wraps ErrUnsupportedMimeType and includes a normalized mime type without charset parameters

Directories

Path Synopsis
providers/disk
Package disk is the local disk storage provider for objects service
Package disk is the local disk storage provider for objects service
providers/r2
Package r2 is the Cloudflare R2 storage provider for objects service
Package r2 is the Cloudflare R2 storage provider for objects service
providers/s3
Package s3 is the AWS S3 storage provider for objects service
Package s3 is the AWS S3 storage provider for objects service
proxy
Package proxy implements a storage proxy that provides presigned URL generation
Package proxy implements a storage proxy that provides presigned URL generation
types
Package storagetypes contains types used across the storage package
Package storagetypes contains types used across the storage package

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL