You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

176 lines
4.7 KiB
Go

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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)
}