- 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>
289 lines
6.7 KiB
Go
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
|
|
} |