package main // Программа для экспорта таблицы pocketbase в excel файл // Вся конфигурация читается из // Программ экспортирует только чистые данные, без форматирования. import ( "encoding/json" "fmt" "io" "log" "net/http" "os" "regexp" "strings" "github.com/xuri/excelize/v2" "gopkg.in/yaml.v3" ) var VERSION = "1.0.0" // Config описывает структуру yaml-файла type Config struct { PocketBase struct { URL string `yaml:"url"` Collection string `yaml:"collection"` Filter string `yaml:"filter"` Sort string `yaml:"sort"` AuthToken string `yaml:"authToken"` } `yaml:"pocketbase"` OutputFile string `yaml:"outputFile"` Sheet string `yaml:"sheet"` StartRow int `yaml:"startRow"` Columns map[string]string `yaml:"columns"` } // --- Чтение YAML с извлечением комментариев --- func readConfigWithComments(path string) (Config, map[string]string, error) { data, err := os.ReadFile(path) if err != nil { return Config{}, nil, err } var cfg Config if err := yaml.Unmarshal(data, &cfg); err != nil { return Config{}, nil, err } re := regexp.MustCompile(`(?m)^\s*([A-Z]+):\s*([^\s#]+)\s*#\s*(.+)$`) comments := make(map[string]string) matches := re.FindAllStringSubmatch(string(data), -1) for _, m := range matches { col := strings.TrimSpace(m[1]) comment := strings.TrimSpace(m[3]) comments[col] = comment } return cfg, comments, nil } // --- Получение данных из PocketBase REST API --- func fetchPocketBase(cfg Config) ([]map[string]interface{}, error) { all := make([]map[string]interface{}, 0) perPage := 1000 page := 1 for { url := fmt.Sprintf("%s/api/collections/%s/records", cfg.PocketBase.URL, cfg.PocketBase.Collection) req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, err } q := req.URL.Query() q.Add("perPage", fmt.Sprintf("%d", perPage)) q.Add("page", fmt.Sprintf("%d", page)) if cfg.PocketBase.Filter != "" { q.Add("filter", cfg.PocketBase.Filter) } req.URL.RawQuery = q.Encode() if cfg.PocketBase.AuthToken != "" { req.Header.Set("Authorization", cfg.PocketBase.AuthToken) } resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } if resp.StatusCode != 200 { body, _ := io.ReadAll(resp.Body) resp.Body.Close() return nil, fmt.Errorf("ошибка PocketBase %d: %s", resp.StatusCode, string(body)) } var parsed struct { Page int `json:"page"` PerPage int `json:"perPage"` TotalItems int `json:"totalItems"` Items []map[string]interface{} `json:"items"` } if err := json.NewDecoder(resp.Body).Decode(&parsed); err != nil { resp.Body.Close() return nil, err } resp.Body.Close() all = append(all, parsed.Items...) if len(parsed.Items) < perPage { break // последняя страница } page++ } return all, nil } // --- Основная логика --- func main() { if len(os.Args) < 2 { fmt.Printf("usage: pocketbase-excel.exe \n") os.Exit(0) } cfg, comments, err := readConfigWithComments(os.Args[1]) if err != nil { log.Fatalf("Ошибка чтения yaml: %v", err) } fmt.Printf("Файл настроек прочитан: %s\n", os.Args[1]) data, err := fetchPocketBase(cfg) if err != nil { log.Fatalf("Ошибка запроса к PocketBase: %v", err) } if len(data) == 0 { log.Fatalf("PocketBase вернул 0 записей") } f := excelize.NewFile() sheet := cfg.Sheet f.NewSheet(sheet) f.DeleteSheet("Sheet1") // --- Заголовки --- for col, field := range cfg.Columns { header := comments[col] if header == "" { header = field } cell := fmt.Sprintf("%s%d", col, cfg.StartRow-1) _ = f.SetCellValue(sheet, cell, header) } // --- Данные --- for i, rec := range data { rowNum := cfg.StartRow + i for col, field := range cfg.Columns { val, ok := rec[field] if !ok { continue } cell := fmt.Sprintf("%s%d", col, rowNum) _ = f.SetCellValue(sheet, cell, val) } } if err := f.SaveAs(cfg.OutputFile); err != nil { log.Fatalf("Ошибка сохранения %s: %v", cfg.OutputFile, err) } fmt.Printf("✅ %d записей из PocketBase экспортировано в %s\n", len(data), cfg.OutputFile) }