278 lines
9.9 KiB
Go
278 lines
9.9 KiB
Go
package sqlite
|
||
|
||
import (
|
||
"context"
|
||
"database/sql"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"gitea.unprism.ru/KRBL/Agate/store"
|
||
_ "github.com/mattn/go-sqlite3"
|
||
"os"
|
||
"path/filepath"
|
||
"time"
|
||
)
|
||
|
||
const (
|
||
createSnapshotsTableSQL = `
|
||
CREATE TABLE IF NOT EXISTS snapshots (
|
||
id TEXT PRIMARY KEY,
|
||
name TEXT NOT NULL,
|
||
parent_id TEXT,
|
||
creation_time DATETIME NOT NULL,
|
||
files_json TEXT -- Храним список файлов как JSON blob
|
||
);`
|
||
)
|
||
|
||
// sqliteStore реализует интерфейс store.MetadataStore с использованием SQLite.
|
||
type sqliteStore struct {
|
||
db *sql.DB
|
||
}
|
||
|
||
// NewSQLiteStore создает и инициализирует новое хранилище метаданных на SQLite.
|
||
func NewSQLiteStore(dbPath string) (store.MetadataStore, error) {
|
||
// Убедимся, что директория для файла БД существует
|
||
dir := filepath.Dir(dbPath)
|
||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||
return nil, fmt.Errorf("failed to create directory for sqlite db %s: %w", dir, err)
|
||
}
|
||
|
||
// Открываем или создаем файл БД SQLite
|
||
// Опции _journal=WAL и _busy_timeout=5000 могут улучшить производительность при конкурентном доступе
|
||
dsn := fmt.Sprintf("file:%s?_journal=WAL&_busy_timeout=5000&_foreign_keys=on", dbPath)
|
||
db, err := sql.Open("sqlite3", dsn)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to open sqlite database %s: %w", dbPath, err)
|
||
}
|
||
|
||
// Проверяем соединение
|
||
if err := db.PingContext(context.Background()); err != nil { // Используем Background т.к. это инициализация
|
||
db.Close()
|
||
return nil, fmt.Errorf("failed to ping sqlite database %s: %w", dbPath, err)
|
||
}
|
||
|
||
// Применяем схему БД
|
||
if err := createSchema(context.Background(), db); err != nil { // Используем Background т.к. это инициализация
|
||
db.Close()
|
||
return nil, fmt.Errorf("failed to apply sqlite schema: %w", err)
|
||
}
|
||
|
||
return &sqliteStore{db: db}, nil
|
||
}
|
||
|
||
// createSchema создает необходимые таблицы, если они не существуют.
|
||
func createSchema(ctx context.Context, db *sql.DB) error {
|
||
_, err := db.ExecContext(ctx, createSnapshotsTableSQL)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to execute schema creation: %w", err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// Close закрывает соединение с БД.
|
||
func (s *sqliteStore) Close() error {
|
||
if s.db != nil {
|
||
return s.db.Close()
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// SaveSnapshotMetadata сохраняет метаданные снапшота в БД.
|
||
func (s *sqliteStore) SaveSnapshotMetadata(ctx context.Context, snap store.Snapshot) error {
|
||
// Сериализуем список файлов в JSON
|
||
filesJSON, err := json.Marshal(snap.Files)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to marshal files to JSON for snapshot %s: %w", snap.ID, err)
|
||
}
|
||
|
||
// Используем INSERT OR REPLACE для атомарной вставки или обновления
|
||
query := `
|
||
INSERT OR REPLACE INTO snapshots (id, name, parent_id, creation_time, files_json)
|
||
VALUES (?, ?, ?, ?, ?);
|
||
`
|
||
_, err = s.db.ExecContext(ctx, query,
|
||
snap.ID,
|
||
snap.Name,
|
||
sql.NullString{String: snap.ParentID, Valid: snap.ParentID != ""}, // Используем NullString для опционального parent_id
|
||
snap.CreationTime.UTC(), // Сохраняем в UTC
|
||
string(filesJSON),
|
||
)
|
||
|
||
if err != nil {
|
||
return fmt.Errorf("failed to save snapshot %s metadata: %w", snap.ID, err)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// GetSnapshotMetadata извлекает метаданные снапшота из БД.
|
||
func (s *sqliteStore) GetSnapshotMetadata(ctx context.Context, snapshotID string) (*store.Snapshot, error) {
|
||
query := `
|
||
SELECT id, name, parent_id, creation_time, files_json
|
||
FROM snapshots
|
||
WHERE id = ?;
|
||
`
|
||
row := s.db.QueryRowContext(ctx, query, snapshotID)
|
||
|
||
var snap store.Snapshot
|
||
var parentID sql.NullString
|
||
var filesJSON string
|
||
var creationTimeStr string // Читаем время как строку, потом парсим
|
||
|
||
err := row.Scan(
|
||
&snap.ID,
|
||
&snap.Name,
|
||
&parentID,
|
||
&creationTimeStr,
|
||
&filesJSON,
|
||
)
|
||
|
||
if err != nil {
|
||
if errors.Is(err, sql.ErrNoRows) {
|
||
// Если запись не найдена, возвращаем кастомную ошибку
|
||
return nil, store.ErrNotFound
|
||
}
|
||
return nil, fmt.Errorf("failed to query snapshot %s: %w", snapshotID, err)
|
||
}
|
||
|
||
// Устанавливаем ParentID, если он не NULL
|
||
snap.ParentID = parentID.String
|
||
|
||
// Парсим время из строки (SQLite хранит DATETIME как текст)
|
||
// Используем формат, совместимый с SQLite и Go's time.RFC3339Nano, или стандартный SQLite формат
|
||
// 'YYYY-MM-DD HH:MM:SS' - требует парсинга с нужным layout
|
||
// Проще сохранять и читать как RFC3339 или Unix timestamp. Мы сохраняли как UTC().
|
||
// Попробуем стандартный формат SQLite
|
||
const sqliteLayout = "2006-01-02 15:04:05" // Стандартный формат SQLite DATETIME без таймзоны
|
||
t, parseErr := time.Parse(sqliteLayout, creationTimeStr)
|
||
if parseErr != nil {
|
||
// Попробуем формат с долями секунд, если первый не сработал
|
||
const sqliteLayoutWithMs = "2006-01-02 15:04:05.999999999"
|
||
t, parseErr = time.Parse(sqliteLayoutWithMs, creationTimeStr)
|
||
if parseErr != nil {
|
||
// Попробуем RFC3339, если сохраняли как UTC().Format(time.RFC3339)
|
||
t, parseErr = time.Parse(time.RFC3339, creationTimeStr)
|
||
if parseErr != nil {
|
||
return nil, fmt.Errorf("failed to parse creation time '%s' for snapshot %s: %w", creationTimeStr, snapshotID, parseErr)
|
||
}
|
||
}
|
||
}
|
||
snap.CreationTime = t.UTC() // Сохраняем как UTC
|
||
|
||
// Десериализуем список файлов из JSON
|
||
if err := json.Unmarshal([]byte(filesJSON), &snap.Files); err != nil {
|
||
return nil, fmt.Errorf("failed to unmarshal files JSON for snapshot %s: %w", snapshotID, err)
|
||
}
|
||
|
||
return &snap, nil
|
||
}
|
||
|
||
// ListSnapshotsMetadata retrieves basic information about snapshots with filtering and pagination.
|
||
func (s *sqliteStore) ListSnapshotsMetadata(ctx context.Context, opts store.ListOptions) ([]store.SnapshotInfo, error) {
|
||
// Build the query with optional filtering
|
||
var query string
|
||
var args []interface{}
|
||
|
||
if opts.FilterByName != "" {
|
||
query = `SELECT id, name, parent_id, creation_time FROM snapshots WHERE name LIKE ? ORDER BY creation_time DESC`
|
||
args = append(args, "%"+opts.FilterByName+"%")
|
||
} else {
|
||
query = `SELECT id, name, parent_id, creation_time FROM snapshots ORDER BY creation_time DESC`
|
||
}
|
||
|
||
// Add pagination if specified
|
||
if opts.Limit > 0 {
|
||
query += " LIMIT ?"
|
||
args = append(args, opts.Limit)
|
||
|
||
if opts.Offset > 0 {
|
||
query += " OFFSET ?"
|
||
args = append(args, opts.Offset)
|
||
}
|
||
}
|
||
|
||
// Execute the query
|
||
rows, err := s.db.QueryContext(ctx, query, args...)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to query snapshots: %w", err)
|
||
}
|
||
defer rows.Close()
|
||
|
||
var snapshots []store.SnapshotInfo
|
||
|
||
// Iterate through the results
|
||
for rows.Next() {
|
||
var info store.SnapshotInfo
|
||
var parentID sql.NullString
|
||
var creationTimeStr string
|
||
|
||
if err := rows.Scan(&info.ID, &info.Name, &parentID, &creationTimeStr); err != nil {
|
||
return nil, fmt.Errorf("failed to scan snapshot row: %w", err)
|
||
}
|
||
|
||
// Set parent ID if not NULL
|
||
if parentID.Valid {
|
||
info.ParentID = parentID.String
|
||
}
|
||
|
||
// Parse creation time
|
||
const sqliteLayout = "2006-01-02 15:04:05" // Standard SQLite DATETIME format without timezone
|
||
t, parseErr := time.Parse(sqliteLayout, creationTimeStr)
|
||
if parseErr != nil {
|
||
// Try format with milliseconds if the first one didn't work
|
||
const sqliteLayoutWithMs = "2006-01-02 15:04:05.999999999"
|
||
t, parseErr = time.Parse(sqliteLayoutWithMs, creationTimeStr)
|
||
if parseErr != nil {
|
||
// Try RFC3339 if saved as UTC().Format(time.RFC3339)
|
||
t, parseErr = time.Parse(time.RFC3339, creationTimeStr)
|
||
if parseErr != nil {
|
||
return nil, fmt.Errorf("failed to parse creation time '%s' for snapshot %s: %w", creationTimeStr, info.ID, parseErr)
|
||
}
|
||
}
|
||
}
|
||
info.CreationTime = t.UTC() // Store as UTC
|
||
|
||
snapshots = append(snapshots, info)
|
||
}
|
||
|
||
if err := rows.Err(); err != nil {
|
||
return nil, fmt.Errorf("error iterating snapshot rows: %w", err)
|
||
}
|
||
|
||
// If no snapshots found, return an empty slice
|
||
if len(snapshots) == 0 {
|
||
return []store.SnapshotInfo{}, nil
|
||
}
|
||
|
||
return snapshots, nil
|
||
}
|
||
|
||
// DeleteSnapshotMetadata удаляет метаданные снапшота.
|
||
func (s *sqliteStore) DeleteSnapshotMetadata(ctx context.Context, snapshotID string) error {
|
||
query := `DELETE FROM snapshots WHERE id = ?;`
|
||
result, err := s.db.ExecContext(ctx, query, snapshotID)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to delete snapshot %s metadata: %w", snapshotID, err)
|
||
}
|
||
|
||
// Проверяем, была ли запись реально удалена (опционально)
|
||
_, err = result.RowsAffected()
|
||
if err != nil {
|
||
// Не критично, если не можем получить количество удаленных строк
|
||
fmt.Printf("Warning: could not get rows affected after deleting snapshot %s: %v\n", snapshotID, err)
|
||
}
|
||
// fmt.Printf("Deleted %d metadata rows for snapshot %s\n", rowsAffected, snapshotID)
|
||
|
||
return nil // Не возвращаем ошибку, если запись не найдена
|
||
}
|
||
|
||
// UpdateSnapshotParentID обновляет ParentID для указанного снапшота.
|
||
func (s *sqliteStore) UpdateSnapshotParentID(ctx context.Context, snapshotID, newParentID string) error {
|
||
query := `UPDATE snapshots SET parent_id = ? WHERE id = ?;`
|
||
_, err := s.db.ExecContext(ctx, query, newParentID, snapshotID)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to update parent ID for snapshot %s: %w", snapshotID, err)
|
||
}
|
||
return nil
|
||
}
|