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 // 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) { func LoadTestEnvWithNamespace(namespace string) (*E2ETestEnv, error) {
env, err := LoadTestEnv() gatewayURL := os.Getenv("ORAMA_GATEWAY_URL")
if err != nil { if gatewayURL == "" {
return nil, err 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 // 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) { func TestNamespaceIsolation_SQLiteDatabases(t *testing.T) {
envA, _ := LoadTestEnvWithNamespace("namespace-a-" + fmt.Sprintf("%d", time.Now().Unix())) envA, err := LoadTestEnvWithNamespace("namespace-a-" + fmt.Sprintf("%d", time.Now().Unix()))
envB, _ := LoadTestEnvWithNamespace("namespace-b-" + 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 // Create database in namespace-a
dbNameA := "users-db-a" dbNameA := "users-db-a"
@ -198,8 +201,11 @@ func TestNamespaceIsolation_SQLiteDatabases(t *testing.T) {
} }
func TestNamespaceIsolation_IPFSContent(t *testing.T) { func TestNamespaceIsolation_IPFSContent(t *testing.T) {
envA, _ := LoadTestEnvWithNamespace("namespace-a-" + fmt.Sprintf("%d", time.Now().Unix())) envA, err := LoadTestEnvWithNamespace("namespace-a-" + fmt.Sprintf("%d", time.Now().Unix()))
envB, _ := LoadTestEnvWithNamespace("namespace-b-" + 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 // Upload file in namespace-a
cidA := UploadTestFile(t, envA, "test-file-a.txt", "Content from 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) { func TestNamespaceIsolation_OlricCache(t *testing.T) {
envA, _ := LoadTestEnvWithNamespace("namespace-a-" + fmt.Sprintf("%d", time.Now().Unix())) envA, err := LoadTestEnvWithNamespace("namespace-a-" + fmt.Sprintf("%d", time.Now().Unix()))
envB, _ := LoadTestEnvWithNamespace("namespace-b-" + 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" keyA := "user-session-123"
valueA := `{"user_id": "alice", "token": "secret-token-a"}` valueA := `{"user_id": "alice", "token": "secret-token-a"}`
t.Run("Namespace-A sets cache key", func(t *testing.T) { t.Run("Namespace-A sets cache key", func(t *testing.T) {
reqBody := map[string]interface{}{ reqBody := map[string]interface{}{
"dmap": dmap,
"key": keyA, "key": keyA,
"value": valueA, "value": valueA,
"ttl": 300, "ttl": "300s",
} }
bodyBytes, _ := json.Marshal(reqBody) 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("Authorization", "Bearer "+envA.APIKey)
req.Header.Set("Content-Type", "application/json") 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) { 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("Authorization", "Bearer "+envB.APIKey)
req.Header.Set("Content-Type", "application/json")
resp, err := envB.HTTPClient.Do(req) resp, err := envB.HTTPClient.Do(req)
require.NoError(t, err, "Should execute request") 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) { 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) bodyBytes, _ := json.Marshal(reqBody)
req, _ := http.NewRequest("POST", envB.GatewayURL+"/v1/cache/delete", bytes.NewReader(bodyBytes)) 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) assert.Contains(t, []int{http.StatusOK, http.StatusNotFound}, resp.StatusCode)
// Verify key still exists for namespace-a // 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("Authorization", "Bearer "+envA.APIKey)
req2.Header.Set("Content-Type", "application/json")
resp2, err := envA.HTTPClient.Do(req2) resp2, err := envA.HTTPClient.Do(req2)
require.NoError(t, err, "Should execute request") 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") assert.Equal(t, http.StatusOK, resp2.StatusCode, "Key should still exist in namespace A")
var result map[string]interface{} var result map[string]interface{}
bodyBytes2, _ := io.ReadAll(resp2.Body) bodyBytes3, _ := io.ReadAll(resp2.Body)
require.NoError(t, json.Unmarshal(bodyBytes2, &result), "Should decode result") 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") 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"}` valueB := `{"user_id": "bob", "token": "secret-token-b"}`
reqBody := map[string]interface{}{ reqBody := map[string]interface{}{
"dmap": dmap,
"key": keyA, // Same key name as namespace-a "key": keyA, // Same key name as namespace-a
"value": valueB, "value": valueB,
"ttl": 300, "ttl": "300s",
} }
bodyBytes, _ := json.Marshal(reqBody) 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("Authorization", "Bearer "+envB.APIKey)
req.Header.Set("Content-Type", "application/json") 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") assert.Equal(t, http.StatusOK, resp.StatusCode, "Should set key in namespace B")
// Verify namespace-a still has their value // 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("Authorization", "Bearer "+envA.APIKey)
req2.Header.Set("Content-Type", "application/json")
resp2, _ := envA.HTTPClient.Do(req2) resp2, _ := envA.HTTPClient.Do(req2)
defer resp2.Body.Close() defer resp2.Body.Close()
@ -401,11 +440,21 @@ func TestNamespaceIsolation_OlricCache(t *testing.T) {
bodyBytesA, _ := io.ReadAll(resp2.Body) bodyBytesA, _ := io.ReadAll(resp2.Body)
require.NoError(t, json.Unmarshal(bodyBytesA, &resultA), "Should decode result A") 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 // 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("Authorization", "Bearer "+envB.APIKey)
req3.Header.Set("Content-Type", "application/json")
resp3, _ := envB.HTTPClient.Do(req3) resp3, _ := envB.HTTPClient.Do(req3)
defer resp3.Body.Close() defer resp3.Body.Close()
@ -414,7 +463,10 @@ func TestNamespaceIsolation_OlricCache(t *testing.T) {
bodyBytesB, _ := io.ReadAll(resp3.Body) bodyBytesB, _ := io.ReadAll(resp3.Body)
require.NoError(t, json.Unmarshal(bodyBytesB, &resultB), "Should decode result B") 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 B can set same key name independently")
t.Logf(" - Namespace A value: %s", valueA) 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") return nil, fmt.Errorf("invalid wallet address format")
} }
// Read namespace (optional) // Read namespace (required)
fmt.Print("Enter namespace (press Enter for 'default'): ") var namespace string
nsInput, err := reader.ReadString('\n') for {
if err != nil { fmt.Print("Enter namespace (required): ")
return nil, fmt.Errorf("failed to read namespace: %w", err) nsInput, err := reader.ReadString('\n')
} if err != nil {
return nil, fmt.Errorf("failed to read namespace: %w", err)
}
namespace := strings.TrimSpace(nsInput) namespace = strings.TrimSpace(nsInput)
if namespace == "" { if namespace != "" {
namespace = "default" break
}
fmt.Println("⚠️ Namespace cannot be empty. Please enter a namespace.")
} }
fmt.Printf("\n✅ Wallet: %s\n", wallet) 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{ ipfsCfg := ipfs.Config{
ClusterAPIURL: ipfsClusterURL, ClusterAPIURL: ipfsClusterURL,
IPFSAPIURL: ipfsAPIURL,
Timeout: ipfsTimeout, 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) ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel() 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() olricCluster := h.olricClient.GetClient()
dm, err := olricCluster.NewDMap(req.DMap) dm, err := olricCluster.NewDMap(namespacedDMap)
if err != nil { if err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to create DMap: %v", err)) writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to create DMap: %v", err))
return 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) ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel() 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() olricCluster := h.olricClient.GetClient()
dm, err := olricCluster.NewDMap(req.DMap) dm, err := olricCluster.NewDMap(namespacedDMap)
if err != nil { if err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to create DMap: %v", err)) writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to create DMap: %v", err))
return return
@ -146,8 +154,16 @@ func (h *CacheHandlers) MultiGetHandler(w http.ResponseWriter, r *http.Request)
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second) ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
defer cancel() 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() olricCluster := h.olricClient.GetClient()
dm, err := olricCluster.NewDMap(req.DMap) dm, err := olricCluster.NewDMap(namespacedDMap)
if err != nil { if err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to create DMap: %v", err)) writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to create DMap: %v", err))
return 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) ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
defer cancel() 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() olricCluster := h.olricClient.GetClient()
dm, err := olricCluster.NewDMap(req.DMap) dm, err := olricCluster.NewDMap(namespacedDMap)
if err != nil { if err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to create DMap: %v", err)) writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to create DMap: %v", err))
return return

View File

@ -7,8 +7,18 @@ import (
"net/http" "net/http"
"strings" "strings"
"time" "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. // 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". // 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.). // 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) ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel() 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() olricCluster := h.olricClient.GetClient()
dm, err := olricCluster.NewDMap(req.DMap) dm, err := olricCluster.NewDMap(namespacedDMap)
if err != nil { if err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to create DMap: %v", err)) writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to create DMap: %v", err))
return return

View File

@ -92,6 +92,7 @@ func (h *StaticDeploymentHandler) HandleUpload(w http.ResponseWriter, r *http.Re
) )
// Extract tarball to temporary directory // Extract tarball to temporary directory
// Create a wrapper directory so IPFS creates a root CID
tmpDir, err := os.MkdirTemp("", "static-deploy-*") tmpDir, err := os.MkdirTemp("", "static-deploy-*")
if err != nil { if err != nil {
h.logger.Error("Failed to create temp directory", zap.Error(err)) 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) 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)) h.logger.Error("Failed to extract tarball", zap.Error(err))
http.Error(w, "Failed to extract tarball", http.StatusInternalServerError) http.Error(w, "Failed to extract tarball", http.StatusInternalServerError)
return 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) addResp, err := h.ipfsClient.AddDirectory(ctx, tmpDir)
if err != nil { if err != nil {
h.logger.Error("Failed to upload to IPFS", zap.Error(err)) h.logger.Error("Failed to upload to IPFS", zap.Error(err))

View File

@ -7,6 +7,7 @@ import (
"os" "os"
"time" "time"
"github.com/DeBrosOfficial/network/pkg/gateway/ctxkeys"
"github.com/DeBrosOfficial/network/pkg/ipfs" "github.com/DeBrosOfficial/network/pkg/ipfs"
"go.uber.org/zap" "go.uber.org/zap"
) )
@ -30,7 +31,11 @@ func NewBackupHandler(sqliteHandler *SQLiteHandler, ipfsClient ipfs.IPFSClient,
// BackupDatabase backs up a database to IPFS // BackupDatabase backs up a database to IPFS
func (h *BackupHandler) BackupDatabase(w http.ResponseWriter, r *http.Request) { func (h *BackupHandler) BackupDatabase(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() 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 { var req struct {
DatabaseName string `json:"database_name"` 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 // ListBackups lists all backups for a database
func (h *BackupHandler) ListBackups(w http.ResponseWriter, r *http.Request) { func (h *BackupHandler) ListBackups(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() 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") databaseName := r.URL.Query().Get("database_name")
if databaseName == "" { if databaseName == "" {

View File

@ -11,6 +11,7 @@ import (
"time" "time"
"github.com/DeBrosOfficial/network/pkg/deployments" "github.com/DeBrosOfficial/network/pkg/deployments"
"github.com/DeBrosOfficial/network/pkg/gateway/ctxkeys"
"github.com/DeBrosOfficial/network/pkg/rqlite" "github.com/DeBrosOfficial/network/pkg/rqlite"
"github.com/google/uuid" "github.com/google/uuid"
"go.uber.org/zap" "go.uber.org/zap"
@ -27,18 +28,31 @@ type SQLiteHandler struct {
// NewSQLiteHandler creates a new SQLite handler // NewSQLiteHandler creates a new SQLite handler
func NewSQLiteHandler(db rqlite.Client, homeNodeManager *deployments.HomeNodeManager, logger *zap.Logger) *SQLiteHandler { 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{ return &SQLiteHandler{
db: db, db: db,
homeNodeManager: homeNodeManager, homeNodeManager: homeNodeManager,
logger: logger, logger: logger,
basePath: "/home/debros/.orama/data/sqlite", basePath: basePath,
} }
} }
// CreateDatabase creates a new SQLite database for a namespace // CreateDatabase creates a new SQLite database for a namespace
func (h *SQLiteHandler) CreateDatabase(w http.ResponseWriter, r *http.Request) { func (h *SQLiteHandler) CreateDatabase(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() 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 { var req struct {
DatabaseName string `json:"database_name"` DatabaseName string `json:"database_name"`

View File

@ -1,12 +1,14 @@
package sqlite package sqlite
import ( import (
"context"
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"net/http" "net/http"
"os" "os"
"strings" "strings"
"github.com/DeBrosOfficial/network/pkg/gateway/ctxkeys"
"go.uber.org/zap" "go.uber.org/zap"
) )
@ -29,7 +31,11 @@ type QueryResponse struct {
// QueryDatabase executes a SQL query on a namespace database // QueryDatabase executes a SQL query on a namespace database
func (h *SQLiteHandler) QueryDatabase(w http.ResponseWriter, r *http.Request) { func (h *SQLiteHandler) QueryDatabase(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() 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 var req QueryRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 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 = ?` 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 { if err != nil {
h.logger.Error("Failed to update database size", zap.Error(err)) 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 // ListDatabases lists all databases for a namespace
func (h *SQLiteHandler) ListDatabases(w http.ResponseWriter, r *http.Request) { func (h *SQLiteHandler) ListDatabases(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() 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 var databases []DatabaseInfo
query := ` query := `

View File

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

View File

@ -33,9 +33,10 @@ type IPFSClient interface {
// Client wraps an IPFS Cluster HTTP API client for storage operations // Client wraps an IPFS Cluster HTTP API client for storage operations
type Client struct { type Client struct {
apiURL string apiURL string
httpClient *http.Client ipfsAPIURL string
logger *zap.Logger httpClient *http.Client
logger *zap.Logger
} }
// Config holds configuration for the IPFS client // Config holds configuration for the IPFS client
@ -44,6 +45,10 @@ type Config struct {
// If empty, defaults to "http://localhost:9094" // If empty, defaults to "http://localhost:9094"
ClusterAPIURL string 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 // Timeout is the timeout for client operations
// If zero, defaults to 60 seconds // If zero, defaults to 60 seconds
Timeout time.Duration Timeout time.Duration
@ -68,6 +73,14 @@ type AddResponse struct {
Size int64 `json:"size"` 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 // PinResponse represents the response from pinning a CID
type PinResponse struct { type PinResponse struct {
Cid string `json:"cid"` Cid string `json:"cid"`
@ -81,6 +94,11 @@ func NewClient(cfg Config, logger *zap.Logger) (*Client, error) {
apiURL = "http://localhost:9094" apiURL = "http://localhost:9094"
} }
ipfsAPIURL := cfg.IPFSAPIURL
if ipfsAPIURL == "" {
ipfsAPIURL = "http://localhost:4501"
}
timeout := cfg.Timeout timeout := cfg.Timeout
if timeout == 0 { if timeout == 0 {
timeout = 60 * time.Second timeout = 60 * time.Second
@ -92,6 +110,7 @@ func NewClient(cfg Config, logger *zap.Logger) (*Client, error) {
return &Client{ return &Client{
apiURL: apiURL, apiURL: apiURL,
ipfsAPIURL: ipfsAPIURL,
httpClient: httpClient, httpClient: httpClient,
logger: logger, logger: logger,
}, nil }, 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 // 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) { func (c *Client) AddDirectory(ctx context.Context, dirPath string) (*AddResponse, error) {
var buf bytes.Buffer var buf bytes.Buffer
writer := multipart.NewWriter(&buf) writer := multipart.NewWriter(&buf)
// Walk directory and add all files to multipart request
var totalSize int64 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 { err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
if err != nil { if err != nil {
return err return err
} }
// Skip directories // Skip directories themselves (IPFS will create them from file paths)
if info.IsDir() { if info.IsDir() {
return nil return nil
} }
// Get relative path // Get relative path from dirPath
relPath, err := filepath.Rel(dirPath, path) relPath, err := filepath.Rel(dirPath, path)
if err != nil { if err != nil {
return fmt.Errorf("failed to get relative path: %w", err) 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)) totalSize += int64(len(data))
fileCount++
// Add file to multipart // Add file to multipart with relative path
part, err := writer.CreateFormFile("file", relPath) part, err := writer.CreateFormFile("file", relPath)
if err != nil { if err != nil {
return fmt.Errorf("failed to create form file: %w", err) 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 return nil, err
} }
if fileCount == 0 {
return nil, fmt.Errorf("no files found in directory")
}
if err := writer.Close(); err != nil { if err := writer.Close(); err != nil {
return nil, fmt.Errorf("failed to close writer: %w", err) return nil, fmt.Errorf("failed to close writer: %w", err)
} }
// Add with wrap-in-directory to create a root directory node // Upload to IPFS daemon (not Cluster) with wrap-in-directory
apiURL := c.apiURL + "/add?wrap-in-directory=true" // 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 { if err != nil {
return nil, fmt.Errorf("failed to create add request: %w", err) 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)) 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) dec := json.NewDecoder(resp.Body)
var last AddResponse var rootCID string
var lastEntry ipfsDaemonAddResponse
for { for {
var chunk AddResponse var chunk ipfsDaemonAddResponse
if err := dec.Decode(&chunk); err != nil { if err := dec.Decode(&chunk); err != nil {
if errors.Is(err, io.EOF) { if errors.Is(err, io.EOF) {
break break
} }
return nil, fmt.Errorf("failed to decode add response: %w", err) 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 == "" { // Use the last entry if no wrapper directory found
return nil, fmt.Errorf("no CID returned from IPFS") 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{ return &AddResponse{
Cid: last.Cid, Cid: rootCID,
Size: totalSize, Size: totalSize,
}, nil }, nil
} }
@ -496,8 +550,9 @@ func (c *Client) Unpin(ctx context.Context, cid string) error {
// Get retrieves content from IPFS by CID // Get retrieves content from IPFS by CID
// Note: This uses the IPFS HTTP API (typically on port 5001), not the Cluster API // 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) { 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 == "" { if ipfsAPIURL == "" {
ipfsAPIURL = "http://localhost:5001" ipfsAPIURL = c.ipfsAPIURL
} }
url := fmt.Sprintf("%s/api/v0/cat?arg=%s", ipfsAPIURL, cid) url := fmt.Sprintf("%s/api/v0/cat?arg=%s", ipfsAPIURL, cid)