244 lines
8.6 KiB
Go
244 lines
8.6 KiB
Go
package sqlite
|
||
|
||
import (
|
||
"context"
|
||
"database/sql"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"gitea.unprism.ru/KRBL/Agate"
|
||
"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, agate.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 извлекает краткую информацию обо всех снапшотах.
|
||
func (s *sqliteStore) ListSnapshotsMetadata(ctx context.Context) ([]store.SnapshotInfo, error) {
|
||
// Simplified implementation to debug the issue
|
||
fmt.Println("ListSnapshotsMetadata called")
|
||
|
||
// Get all snapshot IDs first
|
||
query := `SELECT id FROM snapshots ORDER BY creation_time DESC;`
|
||
fmt.Println("Executing query:", query)
|
||
|
||
rows, err := s.db.QueryContext(ctx, query)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to query snapshot IDs: %w", err)
|
||
}
|
||
defer rows.Close()
|
||
|
||
var snapshots []store.SnapshotInfo
|
||
|
||
// For each ID, get the full snapshot details
|
||
for rows.Next() {
|
||
var id string
|
||
if err := rows.Scan(&id); err != nil {
|
||
return nil, fmt.Errorf("failed to scan snapshot ID: %w", err)
|
||
}
|
||
|
||
// Get the full snapshot details
|
||
snapshot, err := s.GetSnapshotMetadata(ctx, id)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to get snapshot details for ID %s: %w", id, err)
|
||
}
|
||
|
||
// Convert to SnapshotInfo
|
||
info := store.SnapshotInfo{
|
||
ID: snapshot.ID,
|
||
Name: snapshot.Name,
|
||
ParentID: snapshot.ParentID,
|
||
CreationTime: snapshot.CreationTime,
|
||
}
|
||
|
||
snapshots = append(snapshots, info)
|
||
}
|
||
|
||
if err := rows.Err(); err != nil {
|
||
return nil, fmt.Errorf("error iterating snapshot IDs: %w", err)
|
||
}
|
||
|
||
// If no snapshots found, return an empty slice
|
||
if len(snapshots) == 0 {
|
||
fmt.Println("No snapshots found")
|
||
return []store.SnapshotInfo{}, nil
|
||
}
|
||
|
||
fmt.Printf("Found %d snapshots\n", len(snapshots))
|
||
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 // Не возвращаем ошибку, если запись не найдена
|
||
}
|