feat(modpacks): implement simple zip importer and API

This commit is contained in:
2025-06-18 12:41:27 +03:00
parent 5e609017f0
commit ca182d6d6f
9 changed files with 274 additions and 45 deletions

View File

@@ -0,0 +1,49 @@
// File: backend/internal/database/modpack_repository.go
package database
import (
"context"
"gitea.mrixs.me/minecraft-platform/backend/internal/models"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
type ModpackRepository struct {
DB *pgxpool.Pool // <--- ИЗМЕНЕНИЕ
}
// CreateModpackTx создает модпак и все его файлы в одной транзакции.
func (r *ModpackRepository) CreateModpackTx(ctx context.Context, modpack *models.Modpack, files []models.ModpackFile) error {
tx, err := r.DB.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
var modpackID int
err = tx.QueryRow(ctx,
"INSERT INTO modpacks (name, display_name, minecraft_version) VALUES ($1, $2, $3) RETURNING id",
modpack.Name, modpack.DisplayName, modpack.MinecraftVersion,
).Scan(&modpackID)
if err != nil {
return err
}
rows := make([][]interface{}, len(files))
for i, f := range files {
rows[i] = []interface{}{modpackID, f.RelativePath, f.FileHash, f.FileSize, f.DownloadURL}
}
_, err = tx.CopyFrom(
ctx,
pgx.Identifier{"modpack_files"},
[]string{"modpack_id", "relative_path", "file_hash", "file_size", "download_url"},
pgx.CopyFromRows(rows),
)
if err != nil {
return err
}
return tx.Commit(ctx)
}

View File

@@ -1,29 +1,31 @@
package database
import (
"database/sql"
"context"
"log"
"os"
_ "github.com/jackc/pgx/v5/stdlib"
"github.com/jackc/pgx/v5/pgxpool"
)
// Connect устанавливает соединение с базой данных PostgreSQL
func Connect() *sql.DB {
// Connect устанавливает соединение с базой данных PostgreSQL, используя пул соединений pgx.
func Connect() *pgxpool.Pool {
connStr := os.Getenv("DATABASE_URL")
if connStr == "" {
log.Fatal("DATABASE_URL environment variable is not set")
}
db, err := sql.Open("pgx", connStr)
// Создаем пул соединений
pool, err := pgxpool.New(context.Background(), connStr)
if err != nil {
log.Fatalf("Unable to connect to database: %v\n", err)
log.Fatalf("Unable to create connection pool: %v\n", err)
}
if err = db.Ping(); err != nil {
// Проверяем, что соединение действительно установлено
if err = pool.Ping(context.Background()); err != nil {
log.Fatalf("Unable to ping database: %v\n", err)
}
log.Println("Successfully connected to PostgreSQL!")
return db
log.Println("Successfully connected to PostgreSQL using pgxpool!")
return pool
}

View File

@@ -2,18 +2,18 @@ package database
import (
"context"
"database/sql"
"gitea.mrixs.me/minecraft-platform/backend/internal/models"
"github.com/jackc/pgx/v5/pgxpool"
)
type ServerRepository struct {
DB *sql.DB
DB *pgxpool.Pool
}
// GetAllEnabledServers возвращает все активные серверы для опроса.
func (r *ServerRepository) GetAllEnabledServers(ctx context.Context) ([]*models.GameServer, error) {
rows, err := r.DB.QueryContext(ctx, "SELECT id, name, address FROM game_servers WHERE is_enabled = TRUE")
rows, err := r.DB.Query(ctx, "SELECT id, name, address FROM game_servers WHERE is_enabled = TRUE")
if err != nil {
return nil, err
}
@@ -37,7 +37,7 @@ func (r *ServerRepository) UpdateServerStatus(ctx context.Context, id int, statu
status_json = $1, last_polled_at = NOW(), motd = $2, player_count = $3,
max_players = $4, version_name = $5, ping_backend_server = $6
WHERE id = $7`
_, err := r.DB.ExecContext(ctx, query,
_, err := r.DB.Exec(ctx, query,
status.StatusJSON, status.Motd, status.PlayerCount, status.MaxPlayers,
status.VersionName, status.Ping, id)
return err
@@ -49,7 +49,7 @@ func (r *ServerRepository) GetAllWithStatus(ctx context.Context) ([]*models.Game
SELECT id, name, address, is_enabled, last_polled_at, motd,
player_count, max_players, version_name, ping_backend_server
FROM game_servers WHERE is_enabled = TRUE ORDER BY name`
rows, err := r.DB.QueryContext(ctx, query)
rows, err := r.DB.Query(ctx, query)
if err != nil {
return nil, err
}

View File

@@ -7,6 +7,8 @@ import (
"gitea.mrixs.me/minecraft-platform/backend/internal/models"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
var (
@@ -14,21 +16,20 @@ var (
)
type UserRepository struct {
DB *sql.DB
DB *pgxpool.Pool
}
// CreateUserTx создает нового пользователя и его профиль в рамках одной транзакции
func (r *UserRepository) CreateUserTx(ctx context.Context, user *models.User) error {
tx, err := r.DB.BeginTx(ctx, nil)
tx, err := r.DB.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback()
defer tx.Rollback(ctx)
var exists bool
err = tx.QueryRowContext(ctx,
"SELECT EXISTS(SELECT 1 FROM users WHERE username = $1 OR email = $2)",
user.Username, user.Email).Scan(&exists)
err = tx.QueryRow(ctx,
"SELECT EXISTS(SELECT 1 FROM users WHERE username = $1 OR email = $2)").Scan(&exists)
if err != nil {
return err
}
@@ -37,20 +38,17 @@ func (r *UserRepository) CreateUserTx(ctx context.Context, user *models.User) er
}
var newUserID int
err = tx.QueryRowContext(ctx,
"INSERT INTO users (uuid, username, email, password_hash, role) VALUES ($1, $2, $3, $4, $5) RETURNING id",
user.UUID, user.Username, user.Email, user.PasswordHash, user.Role,
).Scan(&newUserID)
err = tx.QueryRow(ctx, "INSERT INTO users (uuid, username, email, password_hash, role) VALUES ($1, $2, $3, $4, $5) RETURNING id").Scan(&newUserID)
if err != nil {
return err
}
_, err = tx.ExecContext(ctx, "INSERT INTO profiles (user_id) VALUES ($1)", newUserID)
_, err = tx.Exec(ctx, "INSERT INTO profiles (user_id) VALUES ($1)")
if err != nil {
return err
}
return tx.Commit()
return tx.Commit(ctx)
}
var (
@@ -64,12 +62,12 @@ func (r *UserRepository) GetUserByUsername(ctx context.Context, username string)
var userUUID string
query := "SELECT id, uuid, username, email, password_hash, role FROM users WHERE username = $1"
err := r.DB.QueryRowContext(ctx, query, username).Scan(
err := r.DB.QueryRow(ctx, query, username).Scan(
&user.ID, &userUUID, &user.Username, &user.Email, &user.PasswordHash, &user.Role,
)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrUserNotFound
}
return nil, err
@@ -82,7 +80,7 @@ func (r *UserRepository) GetUserByUsername(ctx context.Context, username string)
// CreateAccessToken сохраняет новый токен доступа в базу данных.
func (r *UserRepository) CreateAccessToken(ctx context.Context, userID int, accessToken, clientToken string) error {
query := "INSERT INTO access_tokens (user_id, access_token, client_token) VALUES ($1, $2, $3)"
_, err := r.DB.ExecContext(ctx, query, userID, accessToken, clientToken)
_, err := r.DB.Exec(ctx, query, userID, accessToken, clientToken)
return err
}
@@ -98,12 +96,12 @@ func (r *UserRepository) GetProfileByUUID(ctx context.Context, userUUID uuid.UUI
JOIN profiles p ON u.id = p.user_id
WHERE u.uuid = $1`
err := r.DB.QueryRowContext(ctx, query, userUUID).Scan(
err := r.DB.QueryRow(ctx, query, userUUID).Scan(
&user.ID, &user.Username, &skinHash, &capeHash,
)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
if errors.Is(err, pgx.ErrNoRows) {
return nil, nil, ErrUserNotFound
}
return nil, nil, err
@@ -136,7 +134,7 @@ func (r *UserRepository) ValidateAccessToken(ctx context.Context, token string,
WHERE at.access_token = $1 AND u.uuid = $2
)`
err := r.DB.QueryRowContext(ctx, query, token, userUUID).Scan(&exists)
err := r.DB.QueryRow(ctx, query, token, userUUID).Scan(&exists)
if err != nil {
return err
}
@@ -151,16 +149,12 @@ func (r *UserRepository) ValidateAccessToken(ctx context.Context, token string,
// UpdateSkinHash обновляет хеш скина для пользователя.
func (r *UserRepository) UpdateSkinHash(ctx context.Context, userID int, skinHash string) error {
query := "UPDATE profiles SET skin_hash = $1, updated_at = NOW() WHERE user_id = $2"
result, err := r.DB.ExecContext(ctx, query, skinHash, userID)
result, err := r.DB.Exec(ctx, query, skinHash, userID)
if err != nil {
return err
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}
if rowsAffected == 0 {
if result.RowsAffected() == 0 {
return ErrUserNotFound
}
@@ -173,12 +167,12 @@ func (r *UserRepository) GetUserByLogin(ctx context.Context, login string) (*mod
var userUUID string
query := "SELECT id, uuid, username, email, password_hash, role, created_at, updated_at FROM users WHERE username = $1 OR email = $1"
err := r.DB.QueryRowContext(ctx, query, login).Scan(
err := r.DB.QueryRow(ctx, query, login).Scan(
&user.ID, &userUUID, &user.Username, &user.Email, &user.PasswordHash, &user.Role, &user.CreatedAt, &user.UpdatedAt,
)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrUserNotFound
}
return nil, err