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