package bulk import ( "encoding/csv" "encoding/json" "fmt" "io" "net/http" "strings" "time" "github.com/henrygd/beszel/internal/entities/domain" "github.com/henrygd/beszel/internal/entities/monitor" "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/apis" "github.com/pocketbase/pocketbase/core" ) // APIHandler handles bulk import/export requests type APIHandler struct { app core.App } // NewAPIHandler creates a new bulk API handler func NewAPIHandler(app core.App) *APIHandler { return &APIHandler{app: app} } // RegisterRoutes registers bulk API routes func (h *APIHandler) RegisterRoutes(se *core.ServeEvent) { api := se.Router.Group("/api/beszel/bulk") api.Bind(apis.RequireAuth()) // Import endpoints api.POST("/import/domains", h.importDomains) api.POST("/import/monitors", h.importMonitors) // Export endpoints api.GET("/export/domains", h.exportDomains) api.GET("/export/monitors", h.exportMonitors) } // ImportResult represents the result of an import operation type ImportResult struct { Success int `json:"success"` Failed int `json:"failed"` Errors []string `json:"errors,omitempty"` } // importDomains handles bulk domain import from CSV func (h *APIHandler) importDomains(e *core.RequestEvent) error { authRecord := e.Auth if authRecord == nil { return e.UnauthorizedError("unauthorized", nil) } // Parse multipart form if err := e.Request.ParseMultipartForm(10 << 20); err != nil { // 10 MB max return e.BadRequestError("failed to parse form", err) } file, _, err := e.Request.FormFile("file") if err != nil { return e.BadRequestError("missing file", err) } defer file.Close() reader := csv.NewReader(file) // Skip header _, _ = reader.Read() collection, err := h.app.FindCollectionByNameOrId("domains") if err != nil { return e.InternalServerError("failed to find collection", err) } result := ImportResult{} for { record, err := reader.Read() if err == io.EOF { break } if err != nil { result.Failed++ result.Errors = append(result.Errors, fmt.Sprintf("Read error: %v", err)) continue } if len(record) < 1 || record[0] == "" { result.Failed++ result.Errors = append(result.Errors, "Empty domain name") continue } domainName := strings.TrimSpace(record[0]) // Check if domain already exists existing, _ := h.app.FindFirstRecordByFilter("domains", "domain_name = {:domain} && user = {:user}", dbx.Params{"domain": domainName, "user": authRecord.Id}) if existing != nil { result.Failed++ result.Errors = append(result.Errors, fmt.Sprintf("Domain already exists: %s", domainName)) continue } // Create domain record newRecord := core.NewRecord(collection) newRecord.Set("domain_name", domainName) newRecord.Set("user", authRecord.Id) newRecord.Set("status", domain.DomainStatusUnknown) // Optional fields if len(record) > 1 && record[1] != "" { newRecord.Set("tags", strings.Split(record[1], ",")) } if len(record) > 2 && record[2] != "" { newRecord.Set("notes", record[2]) } if len(record) > 3 && record[3] != "" { newRecord.Set("auto_renew", record[3] == "true" || record[3] == "yes") } if err := h.app.Save(newRecord); err != nil { result.Failed++ result.Errors = append(result.Errors, fmt.Sprintf("Failed to save %s: %v", domainName, err)) } else { result.Success++ } } return e.JSON(http.StatusOK, result) } // importMonitors handles bulk monitor import from CSV func (h *APIHandler) importMonitors(e *core.RequestEvent) error { authRecord := e.Auth if authRecord == nil { return e.UnauthorizedError("unauthorized", nil) } if err := e.Request.ParseMultipartForm(10 << 20); err != nil { return e.BadRequestError("failed to parse form", err) } file, _, err := e.Request.FormFile("file") if err != nil { return e.BadRequestError("missing file", err) } defer file.Close() reader := csv.NewReader(file) // Skip header _, _ = reader.Read() collection, err := h.app.FindCollectionByNameOrId("monitors") if err != nil { return e.InternalServerError("failed to find collection", err) } result := ImportResult{} for { record, err := reader.Read() if err == io.EOF { break } if err != nil { result.Failed++ result.Errors = append(result.Errors, fmt.Sprintf("Read error: %v", err)) continue } if len(record) < 2 || record[0] == "" || record[1] == "" { result.Failed++ result.Errors = append(result.Errors, "Missing name or URL") continue } name := strings.TrimSpace(record[0]) url := strings.TrimSpace(record[1]) // Check if monitor already exists existing, _ := h.app.FindFirstRecordByFilter("monitors", "name = {:name} && user = {:user}", dbx.Params{"name": name, "user": authRecord.Id}) if existing != nil { result.Failed++ result.Errors = append(result.Errors, fmt.Sprintf("Monitor already exists: %s", name)) continue } // Create monitor record newRecord := core.NewRecord(collection) newRecord.Set("name", name) newRecord.Set("url", url) newRecord.Set("user", authRecord.Id) newRecord.Set("status", "unknown") newRecord.Set("type", monitor.TypeHTTP) newRecord.Set("interval", 60) newRecord.Set("retries", 3) // Optional fields if len(record) > 2 && record[2] != "" { newRecord.Set("type", record[2]) } if len(record) > 3 && record[3] != "" { if interval, err := parseInt(record[3]); err == nil { newRecord.Set("interval", interval) } } if len(record) > 4 && record[4] != "" { if retries, err := parseInt(record[4]); err == nil { newRecord.Set("retries", retries) } } if err := h.app.Save(newRecord); err != nil { result.Failed++ result.Errors = append(result.Errors, fmt.Sprintf("Failed to save %s: %v", name, err)) } else { result.Success++ } } return e.JSON(http.StatusOK, result) } // exportDomains exports domains as JSON func (h *APIHandler) exportDomains(e *core.RequestEvent) error { authRecord := e.Auth if authRecord == nil { return e.UnauthorizedError("unauthorized", nil) } records, err := h.app.FindAllRecords("domains", dbx.NewExp("user = {:user}", dbx.Params{"user": authRecord.Id}), ) if err != nil { return e.InternalServerError("failed to fetch domains", err) } domains := make([]map[string]interface{}, 0, len(records)) for _, record := range records { domains = append(domains, map[string]interface{}{ "domain_name": record.GetString("domain_name"), "status": record.GetString("status"), "expiry_date": record.GetDateTime("expiry_date").Time(), "registrar_name": record.GetString("registrar_name"), "ssl_issuer": record.GetString("ssl_issuer"), "ssl_valid_to": record.GetDateTime("ssl_valid_to").Time(), "ipv4_addresses": record.Get("ipv4_addresses"), "ipv6_addresses": record.Get("ipv6_addresses"), "name_servers": record.Get("name_servers"), "mx_records": record.Get("mx_records"), "tags": record.Get("tags"), "auto_renew": record.GetBool("auto_renew"), "notes": record.GetString("notes"), "created": record.GetDateTime("created").Time(), "updated": record.GetDateTime("updated").Time(), }) } e.Response.Header().Set("Content-Type", "application/json") e.Response.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=domains_export_%s.json", time.Now().Format("2006-01-02"))) return json.NewEncoder(e.Response).Encode(domains) } // exportMonitors exports monitors as JSON func (h *APIHandler) exportMonitors(e *core.RequestEvent) error { authRecord := e.Auth if authRecord == nil { return e.UnauthorizedError("unauthorized", nil) } records, err := h.app.FindAllRecords("monitors", dbx.NewExp("user = {:user}", dbx.Params{"user": authRecord.Id}), ) if err != nil { return e.InternalServerError("failed to fetch monitors", err) } monitors := make([]map[string]interface{}, 0, len(records)) for _, record := range records { monitors = append(monitors, map[string]interface{}{ "name": record.GetString("name"), "url": record.GetString("url"), "type": record.GetString("type"), "status": record.GetString("status"), "interval": record.GetInt("interval"), "retries": record.GetInt("retries"), "last_response_time": record.GetInt("last_response_time"), "uptime_stats": record.GetString("uptime_stats"), "tags": record.Get("tags"), "created": record.GetDateTime("created").Time(), "updated": record.GetDateTime("updated").Time(), }) } e.Response.Header().Set("Content-Type", "application/json") e.Response.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=monitors_export_%s.json", time.Now().Format("2006-01-02"))) return json.NewEncoder(e.Response).Encode(monitors) } func parseInt(s string) (int, error) { var result int _, err := fmt.Sscanf(s, "%d", &result) return result, err }