implemented storage
This commit is contained in:
1
go.mod
1
go.mod
@@ -5,6 +5,7 @@ go 1.24
|
|||||||
require (
|
require (
|
||||||
github.com/bogem/id3v2 v1.2.0
|
github.com/bogem/id3v2 v1.2.0
|
||||||
github.com/caarlos0/env/v10 v10.0.0
|
github.com/caarlos0/env/v10 v10.0.0
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.28
|
||||||
)
|
)
|
||||||
|
|
||||||
require golang.org/x/text v0.3.2 // indirect
|
require golang.org/x/text v0.3.2 // indirect
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -2,6 +2,8 @@ github.com/bogem/id3v2 v1.2.0 h1:hKDF+F1gOgQ5r1QmBCEZUk4MveJbKxCeIDSBU7CQ4oI=
|
|||||||
github.com/bogem/id3v2 v1.2.0/go.mod h1:t78PK5AQ56Q47kizpYiV6gtjj3jfxlz87oFpty8DYs8=
|
github.com/bogem/id3v2 v1.2.0/go.mod h1:t78PK5AQ56Q47kizpYiV6gtjj3jfxlz87oFpty8DYs8=
|
||||||
github.com/caarlos0/env/v10 v10.0.0 h1:yIHUBZGsyqCnpTkbjk8asUlx6RFhhEs+h7TOBdgdzXA=
|
github.com/caarlos0/env/v10 v10.0.0 h1:yIHUBZGsyqCnpTkbjk8asUlx6RFhhEs+h7TOBdgdzXA=
|
||||||
github.com/caarlos0/env/v10 v10.0.0/go.mod h1:ZfulV76NvVPw3tm591U4SwL3Xx9ldzBP9aGxzeN7G18=
|
github.com/caarlos0/env/v10 v10.0.0/go.mod h1:ZfulV76NvVPw3tm591U4SwL3Xx9ldzBP9aGxzeN7G18=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
|||||||
123
internal/storage/sqlite.go
Normal file
123
internal/storage/sqlite.go
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
70
internal/storage/sqlite_test.go
Normal file
70
internal/storage/sqlite_test.go
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSQLiteStorage(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Используем in-memory базу данных для тестов
|
||||||
|
storage, err := NewSQLiteStorage(ctx, ":memory:")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create in-memory storage: %v", err)
|
||||||
|
}
|
||||||
|
defer storage.Close()
|
||||||
|
|
||||||
|
t.Run("Set and Get", func(t *testing.T) {
|
||||||
|
yandexID := "12345"
|
||||||
|
telegramID := "file_abcde"
|
||||||
|
|
||||||
|
err := storage.Set(ctx, yandexID, telegramID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Set() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gotID, err := storage.Get(ctx, yandexID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Get() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if gotID != telegramID {
|
||||||
|
t.Errorf("Get() got = %v, want %v", gotID, telegramID)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Get not found", func(t *testing.T) {
|
||||||
|
_, err := storage.Get(ctx, "non_existent_id")
|
||||||
|
if !errors.Is(err, ErrNotFound) {
|
||||||
|
t.Errorf("Expected ErrNotFound, got %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Count", func(t *testing.T) {
|
||||||
|
// Сначала очистим (в in-memory это не нужно, но для полноты)
|
||||||
|
// или просто проверим текущее состояние
|
||||||
|
count, err := storage.Count(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Count() error = %v", err)
|
||||||
|
}
|
||||||
|
if count != 1 { // У нас осталась одна запись с предыдущего теста
|
||||||
|
t.Errorf("Count() got = %d, want 1", count)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавим еще одну запись
|
||||||
|
err = storage.Set(ctx, "67890", "file_fghij")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Set() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newCount, err := storage.Count(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Count() error = %v", err)
|
||||||
|
}
|
||||||
|
if newCount != 2 {
|
||||||
|
t.Errorf("Count() got = %d, want 2", newCount)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user