package gateway import ( "fmt" "io" "net/http" "strings" "time" "github.com/DeBrosOfficial/network/pkg/logging" "go.uber.org/zap" ) // rqliteExportHandler handles GET /v1/rqlite/export // Proxies to the namespace's RQLite /db/backup endpoint to download a raw SQLite snapshot. // Protected by requiresNamespaceOwnership() via the /v1/rqlite/ prefix. func (g *Gateway) rqliteExportHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { writeError(w, http.StatusMethodNotAllowed, "method not allowed") return } rqliteURL := g.rqliteBaseURL() if rqliteURL == "" { writeError(w, http.StatusServiceUnavailable, "RQLite not configured") return } backupURL := rqliteURL + "/db/backup" client := &http.Client{Timeout: 5 * time.Minute} resp, err := client.Get(backupURL) if err != nil { g.logger.ComponentError(logging.ComponentGeneral, "rqlite export: failed to reach RQLite backup endpoint", zap.String("url", backupURL), zap.Error(err)) writeError(w, http.StatusBadGateway, fmt.Sprintf("failed to reach RQLite: %v", err)) return } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) writeError(w, resp.StatusCode, fmt.Sprintf("RQLite backup failed: %s", string(body))) return } w.Header().Set("Content-Type", "application/octet-stream") w.Header().Set("Content-Disposition", "attachment; filename=rqlite-export.db") if resp.ContentLength > 0 { w.Header().Set("Content-Length", fmt.Sprintf("%d", resp.ContentLength)) } w.WriteHeader(http.StatusOK) written, err := io.Copy(w, resp.Body) if err != nil { g.logger.ComponentError(logging.ComponentGeneral, "rqlite export: error streaming backup", zap.Int64("bytes_written", written), zap.Error(err)) return } g.logger.ComponentInfo(logging.ComponentGeneral, "rqlite export completed", zap.Int64("bytes", written)) } // rqliteImportHandler handles POST /v1/rqlite/import // Proxies the request body (raw SQLite binary) to the namespace's RQLite /db/load endpoint. // This is a DESTRUCTIVE operation that replaces the entire database. // Protected by requiresNamespaceOwnership() via the /v1/rqlite/ prefix. func (g *Gateway) rqliteImportHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { writeError(w, http.StatusMethodNotAllowed, "method not allowed") return } rqliteURL := g.rqliteBaseURL() if rqliteURL == "" { writeError(w, http.StatusServiceUnavailable, "RQLite not configured") return } ct := r.Header.Get("Content-Type") if !strings.HasPrefix(ct, "application/octet-stream") { writeError(w, http.StatusBadRequest, "Content-Type must be application/octet-stream") return } loadURL := rqliteURL + "/db/load" proxyReq, err := http.NewRequestWithContext(r.Context(), http.MethodPost, loadURL, r.Body) if err != nil { writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to create proxy request: %v", err)) return } proxyReq.Header.Set("Content-Type", "application/octet-stream") if r.ContentLength > 0 { proxyReq.ContentLength = r.ContentLength } client := &http.Client{Timeout: 5 * time.Minute} resp, err := client.Do(proxyReq) if err != nil { g.logger.ComponentError(logging.ComponentGeneral, "rqlite import: failed to reach RQLite load endpoint", zap.String("url", loadURL), zap.Error(err)) writeError(w, http.StatusBadGateway, fmt.Sprintf("failed to reach RQLite: %v", err)) return } defer resp.Body.Close() body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) if resp.StatusCode != http.StatusOK { writeError(w, resp.StatusCode, fmt.Sprintf("RQLite load failed: %s", string(body))) return } g.logger.ComponentInfo(logging.ComponentGeneral, "rqlite import completed successfully") writeJSON(w, http.StatusOK, map[string]any{ "status": "ok", "message": "database imported successfully", }) } // rqliteBaseURL returns the raw RQLite HTTP URL for proxying native API calls. func (g *Gateway) rqliteBaseURL() string { dsn := g.cfg.RQLiteDSN if dsn == "" { dsn = "http://localhost:5001" } if idx := strings.Index(dsn, "?"); idx != -1 { dsn = dsn[:idx] } return strings.TrimRight(dsn, "/") }