Files
healthlog/internal/store/store.go
T
av 41f2f05f2f добавлен приём пакетов Health Auto Export
- POST /api/v1/ingest: токен, лимит тела, gzip, durable-запись тела в сырой
  архив, учёт доставки в SQLite
- код ответа отражает доставку, а не разбор: битый JSON — 400, непонятое
  содержимое — 200, данные уже сохранены и доразберутся позже
- сохраняется полный набор заголовков запроса с вычисткой секретов по имени
  и по совпадению значения с токеном
2026-08-01 12:37:17 +03:00

85 lines
2.7 KiB
Go

// Package store — SQLite-витрина healthlog: доставки и (позже) разобранные
// точки. Витрина производна от сырого архива и пересобирается из него.
package store
import (
"embed"
"fmt"
"net/url"
"time"
"github.com/jmoiron/sqlx"
"github.com/pressly/goose/v3"
_ "modernc.org/sqlite" // чистый Go-драйвер SQLite, без cgo
)
//go:embed migrations/*.sql
var migrationsFS embed.FS
// Store — доступ к витрине.
type Store struct {
db *sqlx.DB
}
// Open открывает БД по пути и накатывает миграции.
func Open(dbPath string) (*Store, error) {
db, err := sqlx.Connect("sqlite", dsn(dbPath))
if err != nil {
return nil, fmt.Errorf("open sqlite %q: %w", dbPath, err)
}
if err := migrate(db); err != nil {
_ = db.Close()
return nil, err
}
return &Store{db: db}, nil
}
// Close закрывает соединение с БД.
func (s *Store) Close() error {
if err := s.db.Close(); err != nil {
return fmt.Errorf("close sqlite: %w", err)
}
return nil
}
// dsn собирает строку подключения: WAL для параллельного чтения во время
// записи, busy_timeout — чтобы конкурентная запись ждала, а не падала.
func dsn(path string) string {
q := url.Values{}
q.Add("_pragma", "journal_mode(WAL)")
q.Add("_pragma", "busy_timeout(5000)")
q.Add("_pragma", "foreign_keys(on)")
return "file:" + path + "?" + q.Encode()
}
func migrate(db *sqlx.DB) error {
goose.SetBaseFS(migrationsFS)
goose.SetLogger(goose.NopLogger())
if err := goose.SetDialect("sqlite3"); err != nil {
return fmt.Errorf("goose dialect: %w", err)
}
if err := goose.Up(db.DB, "migrations"); err != nil {
return fmt.Errorf("goose up: %w", err)
}
return nil
}
// Now — единая точка генерации времени: UTC, секундная точность.
// Секунды дают фиксированную ширину RFC 3339, а значит лексикографическая
// сортировка TEXT совпадает с хронологией.
func Now() time.Time { return time.Now().UTC().Truncate(time.Second) }
// FormatTime приводит время к каноническому виду хранения: RFC 3339, UTC.
func FormatTime(t time.Time) string { return t.UTC().Format(time.RFC3339) }
// ParseTime разбирает метку времени из БД.
func ParseTime(s string) (time.Time, error) {
t, err := time.Parse(time.RFC3339, s)
if err != nil {
return time.Time{}, fmt.Errorf("parse time %q: %w", s, err)
}
return t.UTC(), nil
}