|
|
package main
|
|
|
|
|
|
// Программа для экспорта таблицы pocketbase в excel файл
|
|
|
// Вся конфигурация читается из <config.yaml>
|
|
|
// Программ экспортирует только чистые данные, без форматирования.
|
|
|
|
|
|
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 <CONFIG.YAML>\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)
|
|
|
}
|