The storage package provides interfaces and implementations for persistent storage of media content, enabling efficient memory management for media-heavy applications.

Package Overview

import "github.com/AltairaLabs/PromptKit/runtime/storage"
import "github.com/AltairaLabs/PromptKit/runtime/storage/local"

The storage system enables automatic externalization of large media content (images, audio, video) from memory to persistent storage. This significantly reduces memory footprint while maintaining transparent access to media.

Core Interface

MediaStorageService

The primary interface for media storage implementations.

type MediaStorageService interface {
    // Store saves media content and returns a reference
    Store(ctx context.Context, media *types.MediaContent, metadata MediaMetadata) (*StorageReference, error)
    
    // Retrieve loads media content from storage
    Retrieve(ctx context.Context, ref *StorageReference) (*types.MediaContent, error)
    
    // Delete removes media content from storage
    Delete(ctx context.Context, ref *StorageReference) error
}

Methods:

Store

Stores media content and returns a reference for later retrieval.

ref, err := storageService.Store(ctx, media, metadata)

Parameters:

Returns:

Behavior:

Retrieve

Loads media content from storage using a reference.

media, err := storageService.Retrieve(ctx, ref)

Parameters:

Returns:

Behavior:

Delete

Removes media content from storage.

err := storageService.Delete(ctx, ref)

Parameters:

Returns:

Behavior:

Types

StorageReference

Reference to stored media content.

type StorageReference struct {
    ID          string            // Unique identifier
    Backend     string            // Storage backend name ("file", "s3", etc.)
    Metadata    map[string]string // Backend-specific metadata
    CreatedAt   time.Time         // Creation timestamp
}

Fields:

Example:

ref := &storage.StorageReference{
    ID:       "abc123-def456-ghi789",
    Backend:  "file",
    Metadata: map[string]string{
        "path": "/var/lib/myapp/media/session-xyz/conv-123/abc123-def456-ghi789.png",
        "org":  "by-session",
    },
    CreatedAt: time.Now(),
}

MediaMetadata

Contextual information for organizing stored media.

type MediaMetadata struct {
    ConversationID string    // Conversation identifier
    SessionID      string    // Session identifier
    RunID          string    // Run identifier (Arena)
    UserID         string    // User identifier
    MessageIndex   int       // Position in conversation
    ContentIndex   int       // Position in message
    CreatedAt      time.Time // When media was created
}

Fields:

Usage:

metadata := storage.MediaMetadata{
    ConversationID: conv.ID,
    SessionID:      conv.SessionID,
    UserID:         conv.UserID,
    MessageIndex:   len(conv.Messages),
    ContentIndex:   i,
    CreatedAt:      time.Now(),
}

FileStore Implementation

Overview

Local filesystem implementation of MediaStorageService with support for deduplication, multiple organization modes, and atomic operations.

import "github.com/AltairaLabs/PromptKit/runtime/storage/local"

fileStore := local.NewFileStore(local.FileStoreConfig{
    BaseDir:             "./media",
    Organization:        local.OrganizationBySession,
    EnableDeduplication: true,
})

Configuration

FileStoreConfig

Configuration for FileStore instances.

type FileStoreConfig struct {
    BaseDir             string         // Root directory for media storage
    Organization        OrganizationMode // How to organize files
    EnableDeduplication bool           // Enable content deduplication
}

Fields:

BaseDir - Root storage directory

Organization - File organization strategy

EnableDeduplication - Content-based deduplication

Organization Modes

Constants defining file organization strategies.

const (
    OrganizationFlat            OrganizationMode = "flat"
    OrganizationByConversation  OrganizationMode = "by-conversation"
    OrganizationBySession       OrganizationMode = "by-session"
    OrganizationByRun           OrganizationMode = "by-run"
)

OrganizationFlat - All files in base directory

media/
  abc123_image.png
  def456_audio.mp3

OrganizationByConversation - Group by conversation

media/
  conv-abc123/
    image1.png
    image2.png
  conv-def456/
    audio.mp3

OrganizationBySession - Group by session (default)

media/
  session-xyz789/
    conv-abc123/
      image1.png
    conv-def456/
      audio.mp3

OrganizationByRun - Group by run ID

media/
  run-20241124-123456/
    session-xyz/
      conv-abc/
        image.png

Methods

NewFileStore

Creates a new FileStore instance.

func NewFileStore(config FileStoreConfig) *FileStore

Parameters:

Returns:

Example:

fileStore := local.NewFileStore(local.FileStoreConfig{
    BaseDir:             "./media",
    Organization:        local.OrganizationBySession,
    EnableDeduplication: true,
})

Behavior:

Deduplication

FileStore supports content-based deduplication using SHA-256 hashing.

How It Works

  1. Store: Hash content with SHA-256
  2. Check: Look up hash in deduplication index
  3. Reuse: If exists, increment reference count
  4. Save: If new, store content and create index entry
  5. Delete: Decrement reference count, delete when zero

Index Format

Stored in .dedup-index.json in BaseDir:

{
  "sha256:abc123...": {
    "hash": "abc123...",
    "path": "session-xyz/conv-123/abc123.png",
    "size": 102400,
    "mime_type": "image/png",
    "ref_count": 3,
    "created_at": "2024-11-24T10:30:00Z",
    "last_accessed": "2024-11-24T12:45:00Z"
  }
}

Benefits

Trade-offs

File Naming

FileStore generates unique filenames using:

{hash}-{uuid}.{extension}

Examples:

Components:

Atomic Operations

FileStore uses atomic writes to prevent corruption:

  1. Write to temporary file (.tmp suffix)
  2. Write metadata to separate file (.meta suffix)
  3. Rename to final name (atomic operation)
  4. Update deduplication index if enabled

This ensures:

Error Handling

FileStore returns specific errors:

var (
    ErrNotFound       = errors.New("media not found")
    ErrPermission     = errors.New("permission denied")
    ErrDiskSpace      = errors.New("insufficient disk space")
    ErrCorrupted      = errors.New("media file corrupted")
    ErrInvalidRef     = errors.New("invalid storage reference")
)

Error Types:

Usage Examples

Basic Usage

import (
    "github.com/AltairaLabs/PromptKit/runtime/storage/local"
    "github.com/AltairaLabs/PromptKit/runtime/types"
)

// Create storage
fileStore := local.NewFileStore(local.FileStoreConfig{
    BaseDir: "./media",
})

// Store media
media := &types.MediaContent{
    Type:     "image",
    MimeType: "image/png",
    Data:     base64ImageData,
}

metadata := storage.MediaMetadata{
    ConversationID: "conv-123",
    SessionID:      "session-xyz",
}

ref, err := fileStore.Store(ctx, media, metadata)
if err != nil {
    log.Fatal(err)
}

// Retrieve media
loaded, err := fileStore.Retrieve(ctx, ref)
if err != nil {
    log.Fatal(err)
}

// Delete media
err = fileStore.Delete(ctx, ref)
if err != nil {
    log.Fatal(err)
}

With SDK

import (
    "github.com/AltairaLabs/PromptKit/sdk"
    "github.com/AltairaLabs/PromptKit/runtime/storage/local"
)

// Create storage
fileStore := local.NewFileStore(local.FileStoreConfig{
    BaseDir:             "./media",
    Organization:        local.OrganizationBySession,
    EnableDeduplication: true,
})

// Enable in SDK
manager, err := sdk.NewConversationManager(
    sdk.WithProvider(provider),
    sdk.WithMediaStorage(fileStore),
)

// Media automatically externalized
conv, _ := manager.NewConversation(ctx, pack, sdk.ConversationConfig{
    UserID:     "user123",
    SessionID:  "session-xyz",
    PromptName: "assistant",
})

resp, _ := conv.Send(ctx, "Generate an image")
// Large images automatically stored via fileStore

Custom Organization

// Flat organization for simple apps
flatStore := local.NewFileStore(local.FileStoreConfig{
    BaseDir:      "./temp-media",
    Organization: local.OrganizationFlat,
})

// By-run for tests
testStore := local.NewFileStore(local.FileStoreConfig{
    BaseDir:      "./test-output/media",
    Organization: local.OrganizationByRun,
})

// By-conversation for analysis
analysisStore := local.NewFileStore(local.FileStoreConfig{
    BaseDir:      "./analysis/media",
    Organization: local.OrganizationByConversation,
})

With Deduplication

// Enable deduplication
dedupStore := local.NewFileStore(local.FileStoreConfig{
    BaseDir:             "./media",
    Organization:        local.OrganizationBySession,
    EnableDeduplication: true,
})

// Store identical media
ref1, _ := dedupStore.Store(ctx, media1, metadata1)
ref2, _ := dedupStore.Store(ctx, media2, metadata2) // Same content

// Only one copy stored, both refs work
loaded1, _ := dedupStore.Retrieve(ctx, ref1)
loaded2, _ := dedupStore.Retrieve(ctx, ref2)

// Delete requires both refs deleted
dedupStore.Delete(ctx, ref1) // Decrements ref count
dedupStore.Delete(ctx, ref2) // Actually deletes file

Production Configuration

// Production setup
productionStore := local.NewFileStore(local.FileStoreConfig{
    BaseDir:             "/var/lib/myapp/media",
    Organization:        local.OrganizationBySession,
    EnableDeduplication: true,
})

// With SDK
manager, err := sdk.NewConversationManager(
    sdk.WithProvider(provider),
    sdk.WithMediaStorage(productionStore),
    sdk.WithConfig(sdk.ManagerConfig{
        MediaSizeThresholdKB: 100,  // Externalize > 100KB
        MediaDefaultPolicy:   "retain",
    }),
)

Custom Storage Backends

Implementing MediaStorageService

Create custom backends for S3, GCS, Azure Blob Storage, etc:

type S3Store struct {
    bucket string
    client *s3.Client
}

func (s *S3Store) Store(ctx context.Context, media *types.MediaContent, metadata storage.MediaMetadata) (*storage.StorageReference, error) {
    // Generate unique key
    key := fmt.Sprintf("%s/%s/%s", metadata.SessionID, metadata.ConversationID, uuid.New())
    
    // Upload to S3
    _, err := s.client.PutObject(ctx, &s3.PutObjectInput{
        Bucket: aws.String(s.bucket),
        Key:    aws.String(key),
        Body:   bytes.NewReader([]byte(media.Data)),
    })
    if err != nil {
        return nil, err
    }
    
    // Return reference
    return &storage.StorageReference{
        ID:      key,
        Backend: "s3",
        Metadata: map[string]string{
            "bucket": s.bucket,
            "region": s.client.Options().Region,
        },
        CreatedAt: time.Now(),
    }, nil
}

func (s *S3Store) Retrieve(ctx context.Context, ref *storage.StorageReference) (*types.MediaContent, error) {
    // Download from S3
    result, err := s.client.GetObject(ctx, &s3.GetObjectInput{
        Bucket: aws.String(ref.Metadata["bucket"]),
        Key:    aws.String(ref.ID),
    })
    if err != nil {
        return nil, err
    }
    defer result.Body.Close()
    
    // Read data
    data, err := io.ReadAll(result.Body)
    if err != nil {
        return nil, err
    }
    
    // Return media
    return &types.MediaContent{
        Data:     base64.StdEncoding.EncodeToString(data),
        MimeType: aws.ToString(result.ContentType),
    }, nil
}

func (s *S3Store) Delete(ctx context.Context, ref *storage.StorageReference) error {
    _, err := s.client.DeleteObject(ctx, &s3.DeleteObjectInput{
        Bucket: aws.String(ref.Metadata["bucket"]),
        Key:    aws.String(ref.ID),
    })
    return err
}

Usage with Custom Backend

// Create custom store
s3Store := &S3Store{
    bucket: "myapp-media",
    client: s3Client,
}

// Use with SDK
manager, err := sdk.NewConversationManager(
    sdk.WithProvider(provider),
    sdk.WithMediaStorage(s3Store),
)

Best Practices

Do

Don’t

Performance Considerations

FileStore Performance

Optimization Tips

  1. Lower threshold for more externalization (less memory)
  2. Disable dedup for unique media (avoid hash overhead)
  3. Use SSD for media storage (faster I/O)
  4. Mount volumes in containers (persistence)
  5. Monitor metrics (disk I/O, space usage)

Troubleshooting

Common Issues

Permission Denied:

# Fix permissions
chmod 755 /var/lib/myapp/media
chown -R appuser:appuser /var/lib/myapp/media

Disk Space Full:

# Check usage
du -sh ./media

# Clean old sessions
find ./media -type d -name "session-*" -mtime +30 -delete

Media Not Found:

// Check reference validity
if ref == nil || ref.ID == "" {
    log.Printf("Invalid reference")
}

// Verify file exists
path := filepath.Join(baseDir, ref.Metadata["path"])
if _, err := os.Stat(path); os.IsNotExist(err) {
    log.Printf("File missing: %s", path)
}

Deduplication Index Corrupted:

# Rebuild index
rm ./media/.dedup-index.json
# Restart application (rebuilds from metadata files)

See Also