mirror of
https://github.com/DeBrosOfficial/network.git
synced 2026-01-30 05:23:03 +00:00
fixed some more tests
This commit is contained in:
parent
0a7e3ba3c7
commit
903bef14a3
67
e2e/env.go
67
e2e/env.go
@ -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
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
10
pkg/gateway/handlers/cache/delete_handler.go
vendored
10
pkg/gateway/handlers/cache/delete_handler.go
vendored
@ -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
|
||||||
|
|||||||
20
pkg/gateway/handlers/cache/get_handler.go
vendored
20
pkg/gateway/handlers/cache/get_handler.go
vendored
@ -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
|
||||||
|
|||||||
10
pkg/gateway/handlers/cache/list_handler.go
vendored
10
pkg/gateway/handlers/cache/list_handler.go
vendored
@ -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
|
||||||
|
|||||||
20
pkg/gateway/handlers/cache/set_handler.go
vendored
20
pkg/gateway/handlers/cache/set_handler.go
vendored
@ -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
|
||||||
|
|||||||
@ -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))
|
||||||
|
|||||||
@ -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 == "" {
|
||||||
|
|||||||
@ -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"`
|
||||||
|
|||||||
@ -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 := `
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user