orama/pkg/gateway/rqlite_backup_handler.go

134 lines
4.1 KiB
Go

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, "/")
}