fixed some more tests

This commit is contained in:
anonpenguin23 2026-01-22 17:13:08 +02:00
parent 0a7e3ba3c7
commit 903bef14a3
14 changed files with 335 additions and 69 deletions

View File

@ -1009,13 +1009,70 @@ func LoadTestEnv() (*E2ETestEnv, error) {
}
// LoadTestEnvWithNamespace loads test environment with a specific namespace
// It creates a new API key for the specified namespace to ensure proper isolation
func LoadTestEnvWithNamespace(namespace string) (*E2ETestEnv, error) {
env, err := LoadTestEnv()
if err != nil {
return nil, err
gatewayURL := os.Getenv("ORAMA_GATEWAY_URL")
if gatewayURL == "" {
gatewayURL = GetGatewayURL()
}
env.Namespace = namespace
return env, nil
skipCleanup := os.Getenv("ORAMA_SKIP_CLEANUP") == "true"
// Generate a unique wallet address for this namespace
// Using namespace as part of the wallet address for uniqueness
wallet := fmt.Sprintf("0x%x", []byte(namespace+fmt.Sprintf("%d", time.Now().UnixNano())))
if len(wallet) < 42 {
wallet = wallet + strings.Repeat("0", 42-len(wallet))
}
if len(wallet) > 42 {
wallet = wallet[:42]
}
// Create an API key for this namespace via the simple-key endpoint
reqBody := map[string]string{
"wallet": wallet,
"namespace": namespace,
}
bodyBytes, _ := json.Marshal(reqBody)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "POST", gatewayURL+"/v1/auth/simple-key", bytes.NewReader(bodyBytes))
if err != nil {
return nil, fmt.Errorf("failed to create API key request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
client := NewHTTPClient(10 * time.Second)
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to create API key: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
bodyBytes, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("API key creation failed with status %d: %s", resp.StatusCode, string(bodyBytes))
}
var apiKeyResp map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&apiKeyResp); err != nil {
return nil, fmt.Errorf("failed to decode API key response: %w", err)
}
apiKey, ok := apiKeyResp["api_key"].(string)
if !ok || apiKey == "" {
return nil, fmt.Errorf("API key not found in response")
}
return &E2ETestEnv{
GatewayURL: gatewayURL,
APIKey: apiKey,
Namespace: namespace,
HTTPClient: NewHTTPClient(30 * time.Second),
SkipCleanup: skipCleanup,
}, nil
}
// CreateTestDeployment creates a test deployment and returns its ID

View File

@ -112,8 +112,11 @@ func TestNamespaceIsolation_Deployments(t *testing.T) {
}
func TestNamespaceIsolation_SQLiteDatabases(t *testing.T) {
envA, _ := LoadTestEnvWithNamespace("namespace-a-" + fmt.Sprintf("%d", time.Now().Unix()))
envB, _ := LoadTestEnvWithNamespace("namespace-b-" + fmt.Sprintf("%d", time.Now().Unix()))
envA, err := LoadTestEnvWithNamespace("namespace-a-" + fmt.Sprintf("%d", time.Now().Unix()))
require.NoError(t, err, "Should create test environment for namespace-a")
envB, err := LoadTestEnvWithNamespace("namespace-b-" + fmt.Sprintf("%d", time.Now().Unix()))
require.NoError(t, err, "Should create test environment for namespace-b")
// Create database in namespace-a
dbNameA := "users-db-a"
@ -198,8 +201,11 @@ func TestNamespaceIsolation_SQLiteDatabases(t *testing.T) {
}
func TestNamespaceIsolation_IPFSContent(t *testing.T) {
envA, _ := LoadTestEnvWithNamespace("namespace-a-" + fmt.Sprintf("%d", time.Now().Unix()))
envB, _ := LoadTestEnvWithNamespace("namespace-b-" + fmt.Sprintf("%d", time.Now().Unix()))
envA, err := LoadTestEnvWithNamespace("namespace-a-" + fmt.Sprintf("%d", time.Now().Unix()))
require.NoError(t, err, "Should create test environment for namespace-a")
envB, err := LoadTestEnvWithNamespace("namespace-b-" + fmt.Sprintf("%d", time.Now().Unix()))
require.NoError(t, err, "Should create test environment for namespace-b")
// Upload file in namespace-a
cidA := UploadTestFile(t, envA, "test-file-a.txt", "Content from namespace A")
@ -294,21 +300,26 @@ func TestNamespaceIsolation_IPFSContent(t *testing.T) {
}
func TestNamespaceIsolation_OlricCache(t *testing.T) {
envA, _ := LoadTestEnvWithNamespace("namespace-a-" + fmt.Sprintf("%d", time.Now().Unix()))
envB, _ := LoadTestEnvWithNamespace("namespace-b-" + fmt.Sprintf("%d", time.Now().Unix()))
envA, err := LoadTestEnvWithNamespace("namespace-a-" + fmt.Sprintf("%d", time.Now().Unix()))
require.NoError(t, err, "Should create test environment for namespace-a")
envB, err := LoadTestEnvWithNamespace("namespace-b-" + fmt.Sprintf("%d", time.Now().Unix()))
require.NoError(t, err, "Should create test environment for namespace-b")
dmap := "test-cache"
keyA := "user-session-123"
valueA := `{"user_id": "alice", "token": "secret-token-a"}`
t.Run("Namespace-A sets cache key", func(t *testing.T) {
reqBody := map[string]interface{}{
"dmap": dmap,
"key": keyA,
"value": valueA,
"ttl": 300,
"ttl": "300s",
}
bodyBytes, _ := json.Marshal(reqBody)
req, _ := http.NewRequest("POST", envA.GatewayURL+"/v1/cache/set", bytes.NewReader(bodyBytes))
req, _ := http.NewRequest("POST", envA.GatewayURL+"/v1/cache/put", bytes.NewReader(bodyBytes))
req.Header.Set("Authorization", "Bearer "+envA.APIKey)
req.Header.Set("Content-Type", "application/json")
@ -322,8 +333,15 @@ func TestNamespaceIsolation_OlricCache(t *testing.T) {
})
t.Run("Namespace-B cannot GET Namespace-A cache key", func(t *testing.T) {
req, _ := http.NewRequest("GET", envB.GatewayURL+"/v1/cache/get?key="+keyA, nil)
reqBody := map[string]interface{}{
"dmap": dmap,
"key": keyA,
}
bodyBytes, _ := json.Marshal(reqBody)
req, _ := http.NewRequest("POST", envB.GatewayURL+"/v1/cache/get", bytes.NewReader(bodyBytes))
req.Header.Set("Authorization", "Bearer "+envB.APIKey)
req.Header.Set("Content-Type", "application/json")
resp, err := envB.HTTPClient.Do(req)
require.NoError(t, err, "Should execute request")
@ -336,7 +354,10 @@ func TestNamespaceIsolation_OlricCache(t *testing.T) {
})
t.Run("Namespace-B cannot DELETE Namespace-A cache key", func(t *testing.T) {
reqBody := map[string]string{"key": keyA}
reqBody := map[string]string{
"dmap": dmap,
"key": keyA,
}
bodyBytes, _ := json.Marshal(reqBody)
req, _ := http.NewRequest("POST", envB.GatewayURL+"/v1/cache/delete", bytes.NewReader(bodyBytes))
@ -351,8 +372,15 @@ func TestNamespaceIsolation_OlricCache(t *testing.T) {
assert.Contains(t, []int{http.StatusOK, http.StatusNotFound}, resp.StatusCode)
// Verify key still exists for namespace-a
req2, _ := http.NewRequest("GET", envA.GatewayURL+"/v1/cache/get?key="+keyA, nil)
reqBody2 := map[string]interface{}{
"dmap": dmap,
"key": keyA,
}
bodyBytes2, _ := json.Marshal(reqBody2)
req2, _ := http.NewRequest("POST", envA.GatewayURL+"/v1/cache/get", bytes.NewReader(bodyBytes2))
req2.Header.Set("Authorization", "Bearer "+envA.APIKey)
req2.Header.Set("Content-Type", "application/json")
resp2, err := envA.HTTPClient.Do(req2)
require.NoError(t, err, "Should execute request")
@ -361,10 +389,13 @@ func TestNamespaceIsolation_OlricCache(t *testing.T) {
assert.Equal(t, http.StatusOK, resp2.StatusCode, "Key should still exist in namespace A")
var result map[string]interface{}
bodyBytes2, _ := io.ReadAll(resp2.Body)
require.NoError(t, json.Unmarshal(bodyBytes2, &result), "Should decode result")
bodyBytes3, _ := io.ReadAll(resp2.Body)
require.NoError(t, json.Unmarshal(bodyBytes3, &result), "Should decode result")
assert.Equal(t, valueA, result["value"], "Value should match")
// Parse expected JSON string for comparison
var expectedValue map[string]interface{}
json.Unmarshal([]byte(valueA), &expectedValue)
assert.Equal(t, expectedValue, result["value"], "Value should match")
t.Logf("✓ Namespace B cannot DELETE Namespace A cache key")
})
@ -374,13 +405,14 @@ func TestNamespaceIsolation_OlricCache(t *testing.T) {
valueB := `{"user_id": "bob", "token": "secret-token-b"}`
reqBody := map[string]interface{}{
"dmap": dmap,
"key": keyA, // Same key name as namespace-a
"value": valueB,
"ttl": 300,
"ttl": "300s",
}
bodyBytes, _ := json.Marshal(reqBody)
req, _ := http.NewRequest("POST", envB.GatewayURL+"/v1/cache/set", bytes.NewReader(bodyBytes))
req, _ := http.NewRequest("POST", envB.GatewayURL+"/v1/cache/put", bytes.NewReader(bodyBytes))
req.Header.Set("Authorization", "Bearer "+envB.APIKey)
req.Header.Set("Content-Type", "application/json")
@ -391,8 +423,15 @@ func TestNamespaceIsolation_OlricCache(t *testing.T) {
assert.Equal(t, http.StatusOK, resp.StatusCode, "Should set key in namespace B")
// Verify namespace-a still has their value
req2, _ := http.NewRequest("GET", envA.GatewayURL+"/v1/cache/get?key="+keyA, nil)
reqBody2 := map[string]interface{}{
"dmap": dmap,
"key": keyA,
}
bodyBytes2, _ := json.Marshal(reqBody2)
req2, _ := http.NewRequest("POST", envA.GatewayURL+"/v1/cache/get", bytes.NewReader(bodyBytes2))
req2.Header.Set("Authorization", "Bearer "+envA.APIKey)
req2.Header.Set("Content-Type", "application/json")
resp2, _ := envA.HTTPClient.Do(req2)
defer resp2.Body.Close()
@ -401,11 +440,21 @@ func TestNamespaceIsolation_OlricCache(t *testing.T) {
bodyBytesA, _ := io.ReadAll(resp2.Body)
require.NoError(t, json.Unmarshal(bodyBytesA, &resultA), "Should decode result A")
assert.Equal(t, valueA, resultA["value"], "Namespace A value should be unchanged")
// Parse expected JSON string for comparison
var expectedValueA map[string]interface{}
json.Unmarshal([]byte(valueA), &expectedValueA)
assert.Equal(t, expectedValueA, resultA["value"], "Namespace A value should be unchanged")
// Verify namespace-b has their different value
req3, _ := http.NewRequest("GET", envB.GatewayURL+"/v1/cache/get?key="+keyA, nil)
reqBody3 := map[string]interface{}{
"dmap": dmap,
"key": keyA,
}
bodyBytes3, _ := json.Marshal(reqBody3)
req3, _ := http.NewRequest("POST", envB.GatewayURL+"/v1/cache/get", bytes.NewReader(bodyBytes3))
req3.Header.Set("Authorization", "Bearer "+envB.APIKey)
req3.Header.Set("Content-Type", "application/json")
resp3, _ := envB.HTTPClient.Do(req3)
defer resp3.Body.Close()
@ -414,7 +463,10 @@ func TestNamespaceIsolation_OlricCache(t *testing.T) {
bodyBytesB, _ := io.ReadAll(resp3.Body)
require.NoError(t, json.Unmarshal(bodyBytesB, &resultB), "Should decode result B")
assert.Equal(t, valueB, resultB["value"], "Namespace B value should be different")
// Parse expected JSON string for comparison
var expectedValueB map[string]interface{}
json.Unmarshal([]byte(valueB), &expectedValueB)
assert.Equal(t, expectedValueB, resultB["value"], "Namespace B value should be different")
t.Logf("✓ Namespace B can set same key name independently")
t.Logf(" - Namespace A value: %s", valueA)

View File

@ -43,16 +43,20 @@ func PerformSimpleAuthentication(gatewayURL string) (*Credentials, error) {
return nil, fmt.Errorf("invalid wallet address format")
}
// Read namespace (optional)
fmt.Print("Enter namespace (press Enter for 'default'): ")
// Read namespace (required)
var namespace string
for {
fmt.Print("Enter namespace (required): ")
nsInput, err := reader.ReadString('\n')
if err != nil {
return nil, fmt.Errorf("failed to read namespace: %w", err)
}
namespace := strings.TrimSpace(nsInput)
if namespace == "" {
namespace = "default"
namespace = strings.TrimSpace(nsInput)
if namespace != "" {
break
}
fmt.Println("⚠️ Namespace cannot be empty. Please enter a namespace.")
}
fmt.Printf("\n✅ Wallet: %s\n", wallet)

View File

@ -283,6 +283,7 @@ func initializeIPFS(logger *logging.ColoredLogger, cfg *Config, deps *Dependenci
ipfsCfg := ipfs.Config{
ClusterAPIURL: ipfsClusterURL,
IPFSAPIURL: ipfsAPIURL,
Timeout: ipfsTimeout,
}

View File

@ -55,8 +55,16 @@ func (h *CacheHandlers) DeleteHandler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
// Namespace isolation: prefix dmap with namespace
namespace := getNamespaceFromContext(ctx)
if namespace == "" {
writeError(w, http.StatusUnauthorized, "namespace not found in context")
return
}
namespacedDMap := fmt.Sprintf("%s:%s", namespace, req.DMap)
olricCluster := h.olricClient.GetClient()
dm, err := olricCluster.NewDMap(req.DMap)
dm, err := olricCluster.NewDMap(namespacedDMap)
if err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to create DMap: %v", err))
return

View File

@ -57,8 +57,16 @@ func (h *CacheHandlers) GetHandler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
// Namespace isolation: prefix dmap with namespace
namespace := getNamespaceFromContext(ctx)
if namespace == "" {
writeError(w, http.StatusUnauthorized, "namespace not found in context")
return
}
namespacedDMap := fmt.Sprintf("%s:%s", namespace, req.DMap)
olricCluster := h.olricClient.GetClient()
dm, err := olricCluster.NewDMap(req.DMap)
dm, err := olricCluster.NewDMap(namespacedDMap)
if err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to create DMap: %v", err))
return
@ -146,8 +154,16 @@ func (h *CacheHandlers) MultiGetHandler(w http.ResponseWriter, r *http.Request)
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
defer cancel()
// Namespace isolation: prefix dmap with namespace
namespace := getNamespaceFromContext(ctx)
if namespace == "" {
writeError(w, http.StatusUnauthorized, "namespace not found in context")
return
}
namespacedDMap := fmt.Sprintf("%s:%s", namespace, req.DMap)
olricCluster := h.olricClient.GetClient()
dm, err := olricCluster.NewDMap(req.DMap)
dm, err := olricCluster.NewDMap(namespacedDMap)
if err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to create DMap: %v", err))
return

View File

@ -54,8 +54,16 @@ func (h *CacheHandlers) ScanHandler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
defer cancel()
// Namespace isolation: prefix dmap with namespace
namespace := getNamespaceFromContext(ctx)
if namespace == "" {
writeError(w, http.StatusUnauthorized, "namespace not found in context")
return
}
namespacedDMap := fmt.Sprintf("%s:%s", namespace, req.DMap)
olricCluster := h.olricClient.GetClient()
dm, err := olricCluster.NewDMap(req.DMap)
dm, err := olricCluster.NewDMap(namespacedDMap)
if err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to create DMap: %v", err))
return

View File

@ -7,8 +7,18 @@ import (
"net/http"
"strings"
"time"
"github.com/DeBrosOfficial/network/pkg/gateway/ctxkeys"
)
// getNamespaceFromContext extracts the namespace from the request context
func getNamespaceFromContext(ctx context.Context) string {
if ns, ok := ctx.Value(ctxkeys.NamespaceOverride).(string); ok {
return ns
}
return ""
}
// SetHandler handles cache PUT/SET requests for storing a key-value pair in a distributed map.
// It expects a JSON body with "dmap", "key", and "value" fields, and optionally "ttl".
// The value can be any JSON-serializable type (string, number, object, array, etc.).
@ -60,8 +70,16 @@ func (h *CacheHandlers) SetHandler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
// Namespace isolation: prefix dmap with namespace
namespace := getNamespaceFromContext(ctx)
if namespace == "" {
writeError(w, http.StatusUnauthorized, "namespace not found in context")
return
}
namespacedDMap := fmt.Sprintf("%s:%s", namespace, req.DMap)
olricCluster := h.olricClient.GetClient()
dm, err := olricCluster.NewDMap(req.DMap)
dm, err := olricCluster.NewDMap(namespacedDMap)
if err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to create DMap: %v", err))
return

View File

@ -92,6 +92,7 @@ func (h *StaticDeploymentHandler) HandleUpload(w http.ResponseWriter, r *http.Re
)
// Extract tarball to temporary directory
// Create a wrapper directory so IPFS creates a root CID
tmpDir, err := os.MkdirTemp("", "static-deploy-*")
if err != nil {
h.logger.Error("Failed to create temp directory", zap.Error(err))
@ -100,13 +101,21 @@ func (h *StaticDeploymentHandler) HandleUpload(w http.ResponseWriter, r *http.Re
}
defer os.RemoveAll(tmpDir)
if err := extractTarball(file, tmpDir); err != nil {
// Extract into a subdirectory called "site" so we get a root directory CID
siteDir := filepath.Join(tmpDir, "site")
if err := os.MkdirAll(siteDir, 0755); err != nil {
h.logger.Error("Failed to create site directory", zap.Error(err))
http.Error(w, "Failed to process tarball", http.StatusInternalServerError)
return
}
if err := extractTarball(file, siteDir); err != nil {
h.logger.Error("Failed to extract tarball", zap.Error(err))
http.Error(w, "Failed to extract tarball", http.StatusInternalServerError)
return
}
// Upload extracted directory to IPFS
// Upload the parent directory (tmpDir) to IPFS, which will create a CID for the "site" subdirectory
addResp, err := h.ipfsClient.AddDirectory(ctx, tmpDir)
if err != nil {
h.logger.Error("Failed to upload to IPFS", zap.Error(err))

View File

@ -7,6 +7,7 @@ import (
"os"
"time"
"github.com/DeBrosOfficial/network/pkg/gateway/ctxkeys"
"github.com/DeBrosOfficial/network/pkg/ipfs"
"go.uber.org/zap"
)
@ -30,7 +31,11 @@ func NewBackupHandler(sqliteHandler *SQLiteHandler, ipfsClient ipfs.IPFSClient,
// BackupDatabase backs up a database to IPFS
func (h *BackupHandler) BackupDatabase(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
namespace := ctx.Value("namespace").(string)
namespace, ok := ctx.Value(ctxkeys.NamespaceOverride).(string)
if !ok || namespace == "" {
http.Error(w, "Namespace not found in context", http.StatusUnauthorized)
return
}
var req struct {
DatabaseName string `json:"database_name"`
@ -137,7 +142,11 @@ func (h *BackupHandler) recordBackup(ctx context.Context, dbID, cid string) {
// ListBackups lists all backups for a database
func (h *BackupHandler) ListBackups(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
namespace := ctx.Value("namespace").(string)
namespace, ok := ctx.Value(ctxkeys.NamespaceOverride).(string)
if !ok || namespace == "" {
http.Error(w, "Namespace not found in context", http.StatusUnauthorized)
return
}
databaseName := r.URL.Query().Get("database_name")
if databaseName == "" {

View File

@ -11,6 +11,7 @@ import (
"time"
"github.com/DeBrosOfficial/network/pkg/deployments"
"github.com/DeBrosOfficial/network/pkg/gateway/ctxkeys"
"github.com/DeBrosOfficial/network/pkg/rqlite"
"github.com/google/uuid"
"go.uber.org/zap"
@ -27,18 +28,31 @@ type SQLiteHandler struct {
// NewSQLiteHandler creates a new SQLite handler
func NewSQLiteHandler(db rqlite.Client, homeNodeManager *deployments.HomeNodeManager, logger *zap.Logger) *SQLiteHandler {
// Use user's home directory for cross-platform compatibility
homeDir, err := os.UserHomeDir()
if err != nil {
logger.Error("Failed to get user home directory", zap.Error(err))
homeDir = os.Getenv("HOME")
}
basePath := filepath.Join(homeDir, ".orama", "sqlite")
return &SQLiteHandler{
db: db,
homeNodeManager: homeNodeManager,
logger: logger,
basePath: "/home/debros/.orama/data/sqlite",
basePath: basePath,
}
}
// CreateDatabase creates a new SQLite database for a namespace
func (h *SQLiteHandler) CreateDatabase(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
namespace := ctx.Value("namespace").(string)
namespace, ok := ctx.Value(ctxkeys.NamespaceOverride).(string)
if !ok || namespace == "" {
http.Error(w, "Namespace not found in context", http.StatusUnauthorized)
return
}
var req struct {
DatabaseName string `json:"database_name"`

View File

@ -1,12 +1,14 @@
package sqlite
import (
"context"
"database/sql"
"encoding/json"
"net/http"
"os"
"strings"
"github.com/DeBrosOfficial/network/pkg/gateway/ctxkeys"
"go.uber.org/zap"
)
@ -29,7 +31,11 @@ type QueryResponse struct {
// QueryDatabase executes a SQL query on a namespace database
func (h *SQLiteHandler) QueryDatabase(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
namespace := ctx.Value("namespace").(string)
namespace, ok := ctx.Value(ctxkeys.NamespaceOverride).(string)
if !ok || namespace == "" {
http.Error(w, "Namespace not found in context", http.StatusUnauthorized)
return
}
var req QueryRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
@ -179,7 +185,8 @@ func (h *SQLiteHandler) updateDatabaseSize(namespace, databaseName, filePath str
}
query := `UPDATE namespace_sqlite_databases SET size_bytes = ? WHERE namespace = ? AND database_name = ?`
_, err = h.db.Exec(nil, query, stat.Size(), namespace, databaseName)
ctx := context.Background()
_, err = h.db.Exec(ctx, query, stat.Size(), namespace, databaseName)
if err != nil {
h.logger.Error("Failed to update database size", zap.Error(err))
}
@ -199,7 +206,11 @@ type DatabaseInfo struct {
// ListDatabases lists all databases for a namespace
func (h *SQLiteHandler) ListDatabases(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
namespace := ctx.Value("namespace").(string)
namespace, ok := ctx.Value(ctxkeys.NamespaceOverride).(string)
if !ok || namespace == "" {
http.Error(w, "Namespace not found in context", http.StatusUnauthorized)
return
}
var databases []DatabaseInfo
query := `

View File

@ -459,9 +459,13 @@ func (g *Gateway) domainRoutingMiddleware(next http.Handler) http.Handler {
// Try to find deployment by domain
deployment, err := g.getDeploymentByDomain(r.Context(), host)
if err != nil || deployment == nil {
// Not a deployment domain, continue to normal routing
next.ServeHTTP(w, r)
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
if deployment == nil {
// Domain matches .orama.network but no deployment found
http.NotFound(w, r)
return
}

View File

@ -34,6 +34,7 @@ type IPFSClient interface {
// Client wraps an IPFS Cluster HTTP API client for storage operations
type Client struct {
apiURL string
ipfsAPIURL string
httpClient *http.Client
logger *zap.Logger
}
@ -44,6 +45,10 @@ type Config struct {
// If empty, defaults to "http://localhost:9094"
ClusterAPIURL string
// IPFSAPIURL is the base URL for IPFS daemon API (e.g., "http://localhost:4501")
// Used for operations that require IPFS daemon directly (like directory uploads)
IPFSAPIURL string
// Timeout is the timeout for client operations
// If zero, defaults to 60 seconds
Timeout time.Duration
@ -68,6 +73,14 @@ type AddResponse struct {
Size int64 `json:"size"`
}
// ipfsDaemonAddResponse represents the response from IPFS daemon's /add endpoint
// The daemon returns Size as a string, unlike Cluster which returns it as int64
type ipfsDaemonAddResponse struct {
Name string `json:"Name"`
Hash string `json:"Hash"` // Daemon uses "Hash" instead of "Cid"
Size string `json:"Size"` // Daemon returns size as string
}
// PinResponse represents the response from pinning a CID
type PinResponse struct {
Cid string `json:"cid"`
@ -81,6 +94,11 @@ func NewClient(cfg Config, logger *zap.Logger) (*Client, error) {
apiURL = "http://localhost:9094"
}
ipfsAPIURL := cfg.IPFSAPIURL
if ipfsAPIURL == "" {
ipfsAPIURL = "http://localhost:4501"
}
timeout := cfg.Timeout
if timeout == 0 {
timeout = 60 * time.Second
@ -92,6 +110,7 @@ func NewClient(cfg Config, logger *zap.Logger) (*Client, error) {
return &Client{
apiURL: apiURL,
ipfsAPIURL: ipfsAPIURL,
httpClient: httpClient,
logger: logger,
}, nil
@ -240,23 +259,26 @@ func (c *Client) Add(ctx context.Context, reader io.Reader, name string) (*AddRe
}
// AddDirectory adds all files in a directory to IPFS and returns the root directory CID
// Uses IPFS daemon's multipart upload to preserve directory structure
func (c *Client) AddDirectory(ctx context.Context, dirPath string) (*AddResponse, error) {
var buf bytes.Buffer
writer := multipart.NewWriter(&buf)
// Walk directory and add all files to multipart request
var totalSize int64
var fileCount int
// Walk directory and add all files to multipart request
err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// Skip directories
// Skip directories themselves (IPFS will create them from file paths)
if info.IsDir() {
return nil
}
// Get relative path
// Get relative path from dirPath
relPath, err := filepath.Rel(dirPath, path)
if err != nil {
return fmt.Errorf("failed to get relative path: %w", err)
@ -269,8 +291,9 @@ func (c *Client) AddDirectory(ctx context.Context, dirPath string) (*AddResponse
}
totalSize += int64(len(data))
fileCount++
// Add file to multipart
// Add file to multipart with relative path
part, err := writer.CreateFormFile("file", relPath)
if err != nil {
return fmt.Errorf("failed to create form file: %w", err)
@ -287,14 +310,19 @@ func (c *Client) AddDirectory(ctx context.Context, dirPath string) (*AddResponse
return nil, err
}
if fileCount == 0 {
return nil, fmt.Errorf("no files found in directory")
}
if err := writer.Close(); err != nil {
return nil, fmt.Errorf("failed to close writer: %w", err)
}
// Add with wrap-in-directory to create a root directory node
apiURL := c.apiURL + "/add?wrap-in-directory=true"
// Upload to IPFS daemon (not Cluster) with wrap-in-directory
// This creates a UnixFS directory structure
ipfsDaemonURL := c.ipfsAPIURL + "/api/v0/add?wrap-in-directory=true"
req, err := http.NewRequestWithContext(ctx, "POST", apiURL, &buf)
req, err := http.NewRequestWithContext(ctx, "POST", ipfsDaemonURL, &buf)
if err != nil {
return nil, fmt.Errorf("failed to create add request: %w", err)
}
@ -312,27 +340,53 @@ func (c *Client) AddDirectory(ctx context.Context, dirPath string) (*AddResponse
return nil, fmt.Errorf("add failed with status %d: %s", resp.StatusCode, string(body))
}
// Read NDJSON responses - the last one will be the root directory
// Read NDJSON responses
// IPFS daemon returns entries for each file and subdirectory
// The last entry should be the root directory (or deepest subdirectory if no wrapper)
dec := json.NewDecoder(resp.Body)
var last AddResponse
var rootCID string
var lastEntry ipfsDaemonAddResponse
for {
var chunk AddResponse
var chunk ipfsDaemonAddResponse
if err := dec.Decode(&chunk); err != nil {
if errors.Is(err, io.EOF) {
break
}
return nil, fmt.Errorf("failed to decode add response: %w", err)
}
last = chunk
lastEntry = chunk
// With wrap-in-directory, the entry with empty name is the wrapper directory
if chunk.Name == "" {
rootCID = chunk.Hash
}
}
if last.Cid == "" {
return nil, fmt.Errorf("no CID returned from IPFS")
// Use the last entry if no wrapper directory found
if rootCID == "" {
rootCID = lastEntry.Hash
}
if rootCID == "" {
return nil, fmt.Errorf("no root CID returned from IPFS daemon")
}
c.logger.Debug("Directory uploaded to IPFS",
zap.String("root_cid", rootCID),
zap.Int("file_count", fileCount),
zap.Int64("total_size", totalSize))
// Pin to cluster for distribution
_, err = c.Pin(ctx, rootCID, "", 1)
if err != nil {
c.logger.Warn("Failed to pin directory to cluster",
zap.String("cid", rootCID),
zap.Error(err))
}
return &AddResponse{
Cid: last.Cid,
Cid: rootCID,
Size: totalSize,
}, nil
}
@ -496,8 +550,9 @@ func (c *Client) Unpin(ctx context.Context, cid string) error {
// Get retrieves content from IPFS by CID
// Note: This uses the IPFS HTTP API (typically on port 5001), not the Cluster API
func (c *Client) Get(ctx context.Context, cid string, ipfsAPIURL string) (io.ReadCloser, error) {
// Use the client's configured IPFS API URL if not provided
if ipfsAPIURL == "" {
ipfsAPIURL = "http://localhost:5001"
ipfsAPIURL = c.ipfsAPIURL
}
url := fmt.Sprintf("%s/api/v0/cat?arg=%s", ipfsAPIURL, cid)