Files
bzzz/pkg/ucxi/storage.go
anthonyrawlins b207f32d9e Implement UCXL Protocol Foundation (Phase 1)
- Add complete UCXL address parser with BNF grammar validation
- Implement temporal navigation system with bounds checking
- Create UCXI HTTP server with REST-like operations
- Add comprehensive test suite with 87 passing tests
- Integrate with existing BZZZ architecture (opt-in via config)
- Support semantic addressing with wildcards and version control

Core Features:
- UCXL address format: ucxl://agent:role@project:task/temporal/path
- Temporal segments: *^, ~~N, ^^N, *~, *~N with navigation logic
- UCXI endpoints: GET/PUT/POST/DELETE/ANNOUNCE operations
- Production-ready with error handling and graceful shutdown

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-08 07:38:04 +10:00

289 lines
6.7 KiB
Go

package ucxi
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"sync"
)
// BasicContentStorage provides a basic file-system based implementation of ContentStorage
type BasicContentStorage struct {
basePath string
mutex sync.RWMutex
}
// NewBasicContentStorage creates a new basic content storage
func NewBasicContentStorage(basePath string) (*BasicContentStorage, error) {
// Ensure base directory exists
if err := os.MkdirAll(basePath, 0755); err != nil {
return nil, fmt.Errorf("failed to create storage directory: %w", err)
}
return &BasicContentStorage{
basePath: basePath,
}, nil
}
// Store stores content with the given key
func (s *BasicContentStorage) Store(ctx context.Context, key string, content *Content) error {
if key == "" {
return fmt.Errorf("key cannot be empty")
}
if content == nil {
return fmt.Errorf("content cannot be nil")
}
s.mutex.Lock()
defer s.mutex.Unlock()
// Generate file path
filePath := s.getFilePath(key)
// Ensure directory exists
dir := filepath.Dir(filePath)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("failed to create directory %s: %w", dir, err)
}
// Calculate checksum if not provided
if content.Checksum == "" {
hash := sha256.Sum256(content.Data)
content.Checksum = hex.EncodeToString(hash[:])
}
// Serialize content to JSON
data, err := json.MarshalIndent(content, "", " ")
if err != nil {
return fmt.Errorf("failed to serialize content: %w", err)
}
// Write to file
if err := ioutil.WriteFile(filePath, data, 0644); err != nil {
return fmt.Errorf("failed to write content file: %w", err)
}
return nil
}
// Retrieve retrieves content by key
func (s *BasicContentStorage) Retrieve(ctx context.Context, key string) (*Content, error) {
if key == "" {
return nil, fmt.Errorf("key cannot be empty")
}
s.mutex.RLock()
defer s.mutex.RUnlock()
filePath := s.getFilePath(key)
// Check if file exists
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return nil, fmt.Errorf("content not found for key: %s", key)
}
// Read file
data, err := ioutil.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("failed to read content file: %w", err)
}
// Deserialize content
var content Content
if err := json.Unmarshal(data, &content); err != nil {
return nil, fmt.Errorf("failed to deserialize content: %w", err)
}
// Verify checksum if available
if content.Checksum != "" {
hash := sha256.Sum256(content.Data)
expectedChecksum := hex.EncodeToString(hash[:])
if content.Checksum != expectedChecksum {
return nil, fmt.Errorf("content checksum mismatch")
}
}
return &content, nil
}
// Delete deletes content by key
func (s *BasicContentStorage) Delete(ctx context.Context, key string) error {
if key == "" {
return fmt.Errorf("key cannot be empty")
}
s.mutex.Lock()
defer s.mutex.Unlock()
filePath := s.getFilePath(key)
// Check if file exists
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return fmt.Errorf("content not found for key: %s", key)
}
// Remove file
if err := os.Remove(filePath); err != nil {
return fmt.Errorf("failed to delete content file: %w", err)
}
// Try to remove empty directories
s.cleanupEmptyDirs(filepath.Dir(filePath))
return nil
}
// List lists all keys with the given prefix
func (s *BasicContentStorage) List(ctx context.Context, prefix string) ([]string, error) {
s.mutex.RLock()
defer s.mutex.RUnlock()
var keys []string
// Walk through storage directory
err := filepath.Walk(s.basePath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// Skip directories
if info.IsDir() {
return nil
}
// Skip non-JSON files
if !strings.HasSuffix(path, ".json") {
return nil
}
// Convert file path back to key
relPath, err := filepath.Rel(s.basePath, path)
if err != nil {
return err
}
// Remove .json extension
key := strings.TrimSuffix(relPath, ".json")
// Convert file path separators back to key format
key = strings.ReplaceAll(key, string(filepath.Separator), "/")
// Check prefix match
if prefix == "" || strings.HasPrefix(key, prefix) {
keys = append(keys, key)
}
return nil
})
if err != nil {
return nil, fmt.Errorf("failed to list storage contents: %w", err)
}
return keys, nil
}
// getFilePath converts a storage key to a file path
func (s *BasicContentStorage) getFilePath(key string) string {
// Sanitize key by replacing potentially problematic characters
sanitized := strings.ReplaceAll(key, ":", "_")
sanitized = strings.ReplaceAll(sanitized, "@", "_at_")
sanitized = strings.ReplaceAll(sanitized, "/", string(filepath.Separator))
return filepath.Join(s.basePath, sanitized+".json")
}
// cleanupEmptyDirs removes empty directories up the tree
func (s *BasicContentStorage) cleanupEmptyDirs(dir string) {
// Don't remove the base directory
if dir == s.basePath {
return
}
// Try to remove directory if empty
if err := os.Remove(dir); err == nil {
// Successfully removed, try parent
s.cleanupEmptyDirs(filepath.Dir(dir))
}
}
// GetStorageStats returns statistics about the storage
func (s *BasicContentStorage) GetStorageStats() (map[string]interface{}, error) {
s.mutex.RLock()
defer s.mutex.RUnlock()
var fileCount int
var totalSize int64
err := filepath.Walk(s.basePath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && strings.HasSuffix(path, ".json") {
fileCount++
totalSize += info.Size()
}
return nil
})
if err != nil {
return nil, fmt.Errorf("failed to calculate storage stats: %w", err)
}
return map[string]interface{}{
"file_count": fileCount,
"total_size": totalSize,
"base_path": s.basePath,
}, nil
}
// Exists checks if content exists for the given key
func (s *BasicContentStorage) Exists(ctx context.Context, key string) (bool, error) {
if key == "" {
return false, fmt.Errorf("key cannot be empty")
}
filePath := s.getFilePath(key)
s.mutex.RLock()
defer s.mutex.RUnlock()
_, err := os.Stat(filePath)
if os.IsNotExist(err) {
return false, nil
}
if err != nil {
return false, fmt.Errorf("failed to check file existence: %w", err)
}
return true, nil
}
// Clear removes all content from storage
func (s *BasicContentStorage) Clear(ctx context.Context) error {
s.mutex.Lock()
defer s.mutex.Unlock()
// Remove all contents of base directory
entries, err := ioutil.ReadDir(s.basePath)
if err != nil {
return fmt.Errorf("failed to read storage directory: %w", err)
}
for _, entry := range entries {
path := filepath.Join(s.basePath, entry.Name())
if err := os.RemoveAll(path); err != nil {
return fmt.Errorf("failed to remove %s: %w", path, err)
}
}
return nil
}