Files
2025-06-23 09:03:39 +03:00

124 lines
3.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package storage
import (
"context"
"database/sql"
"errors"
"fmt"
"log/slog"
"os"
"path/filepath"
_ "github.com/mattn/go-sqlite3" // Драйвер для SQLite
)
var (
// ErrNotFound возвращается, когда запись не найдена в хранилище.
ErrNotFound = errors.New("not found")
)
// SQLiteStorage реализует интерфейс interfaces.TrackStorage для SQLite.
type SQLiteStorage struct {
db *sql.DB
}
// NewSQLiteStorage создает и инициализирует новое хранилище SQLite.
// Он также проверяет и создает таблицу, если она не существует.
func NewSQLiteStorage(ctx context.Context, dbPath string) (*SQLiteStorage, error) {
// Убедимся, что директория для файла БД существует
dir := filepath.Dir(dbPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return nil, fmt.Errorf("failed to create data directory: %w", err)
}
db, err := sql.Open("sqlite3", dbPath)
if err != nil {
return nil, fmt.Errorf("failed to open database: %w", err)
}
if err := db.PingContext(ctx); err != nil {
return nil, fmt.Errorf("failed to connect to database: %w", err)
}
// Устанавливаем 1 соединение, т.к. SQLite плохо работает с конкурентной записью.
// Для наших целей этого более чем достаточно.
db.SetMaxOpenConns(1)
storage := &SQLiteStorage{db: db}
if err := storage.initSchema(ctx); err != nil {
return nil, fmt.Errorf("failed to initialize schema: %w", err)
}
slog.Info("SQLite storage initialized successfully", "path", dbPath)
return storage, nil
}
// initSchema создает таблицу для кэша, если она еще не существует.
func (s *SQLiteStorage) initSchema(ctx context.Context) error {
const ddl = `
CREATE TABLE IF NOT EXISTS tracks_cache (
yandex_track_id TEXT PRIMARY KEY,
telegram_file_id TEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);`
_, err := s.db.ExecContext(ctx, ddl)
return err
}
// Get получает telegram_file_id по yandex_track_id.
// Возвращает ErrNotFound, если запись не найдена.
func (s *SQLiteStorage) Get(ctx context.Context, yandexTrackID string) (string, error) {
const op = "storage.sqlite.Get"
stmt, err := s.db.PrepareContext(ctx, "SELECT telegram_file_id FROM tracks_cache WHERE yandex_track_id = ?")
if err != nil {
return "", fmt.Errorf("%s: %w", op, err)
}
defer stmt.Close()
var fileID string
err = stmt.QueryRowContext(ctx, yandexTrackID).Scan(&fileID)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return "", ErrNotFound
}
return "", fmt.Errorf("%s: %w", op, err)
}
return fileID, nil
}
// Set сохраняет новую запись в кэш.
func (s *SQLiteStorage) Set(ctx context.Context, yandexTrackID, telegramFileID string) error {
const op = "storage.sqlite.Set"
stmt, err := s.db.PrepareContext(ctx, "INSERT INTO tracks_cache(yandex_track_id, telegram_file_id) VALUES(?, ?)")
if err != nil {
return fmt.Errorf("%s: %w", op, err)
}
defer stmt.Close()
_, err = stmt.ExecContext(ctx, yandexTrackID, telegramFileID)
if err != nil {
return fmt.Errorf("%s: %w", op, err)
}
return nil
}
// Count возвращает общее количество записей в кэше.
func (s *SQLiteStorage) Count(ctx context.Context) (int, error) {
const op = "storage.sqlite.Count"
var count int
err := s.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM tracks_cache").Scan(&count)
if err != nil {
return 0, fmt.Errorf("%s: %w", op, err)
}
return count, nil
}
// Close закрывает соединение с базой данных.
func (s *SQLiteStorage) Close() error {
return s.db.Close()
}