From 8e133630d49caf7852aef3730881c6b2395b285e Mon Sep 17 00:00:00 2001 From: Anton Vakhrushev Date: Sat, 9 Aug 2025 15:18:42 +0300 Subject: [PATCH] Refactoring: clean architecture project structure --- .gitignore | 3 + database/database.go | 117 ------------------ .../controller/http}/transcribe.go | 55 ++++---- internal/entity/file.go | 17 +++ internal/entity/job.go | 23 ++++ internal/repo/contracts.go | 16 +++ internal/repo/sqlite/file_sqlite.go | 55 ++++++++ internal/repo/sqlite/transcript_job_sqlite.go | 77 ++++++++++++ main.go | 49 ++++++-- migrations/001_create_files_table.sql | 2 +- .../002_create_transcribe_jobs_table.sql | 12 +- models/models.go | 27 ---- 12 files changed, 265 insertions(+), 188 deletions(-) delete mode 100644 database/database.go rename {handlers => internal/controller/http}/transcribe.go (65%) create mode 100644 internal/entity/file.go create mode 100644 internal/entity/job.go create mode 100644 internal/repo/contracts.go create mode 100644 internal/repo/sqlite/file_sqlite.go create mode 100644 internal/repo/sqlite/transcript_job_sqlite.go delete mode 100644 models/models.go diff --git a/.gitignore b/.gitignore index 4e29a03..e4a9cb7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# App binary +transcriber + # Binaries for programs and plugins *.exe *.exe~ diff --git a/database/database.go b/database/database.go deleted file mode 100644 index 3b41000..0000000 --- a/database/database.go +++ /dev/null @@ -1,117 +0,0 @@ -package database - -import ( - "database/sql" - "fmt" - "log" - - "git.vakhrushev.me/av/transcriber/models" - "github.com/doug-martin/goqu/v9" - _ "github.com/doug-martin/goqu/v9/dialect/sqlite3" - _ "github.com/mattn/go-sqlite3" - "github.com/pressly/goose/v3" -) - -type DB struct { - conn *sql.DB - gq *goqu.Database -} - -func New(dbPath string) (*DB, error) { - conn, err := sql.Open("sqlite3", dbPath) - if err != nil { - return nil, fmt.Errorf("failed to open database: %w", err) - } - - if err := conn.Ping(); err != nil { - return nil, fmt.Errorf("failed to ping database: %w", err) - } - - gq := goqu.New("sqlite3", conn) - - db := &DB{ - conn: conn, - gq: gq, - } - - return db, nil -} - -func (db *DB) RunMigrations(migrationsDir string) error { - if err := goose.SetDialect("sqlite3"); err != nil { - return fmt.Errorf("failed to set goose dialect: %w", err) - } - - if err := goose.Up(db.conn, migrationsDir); err != nil { - return fmt.Errorf("failed to run migrations: %w", err) - } - - log.Println("Migrations completed successfully") - return nil -} - -func (db *DB) Close() error { - return db.conn.Close() -} - -func (db *DB) CreateFile(file *models.File) error { - query := db.gq.Insert("files").Rows(file) - sql, args, err := query.ToSQL() - if err != nil { - return fmt.Errorf("failed to build query: %w", err) - } - - _, err = db.conn.Exec(sql, args...) - if err != nil { - return fmt.Errorf("failed to insert file: %w", err) - } - - return nil -} - -func (db *DB) CreateTranscribeJob(job *models.TranscribeJob) error { - query := db.gq.Insert("transcribe_jobs").Rows(job) - sql, args, err := query.ToSQL() - if err != nil { - return fmt.Errorf("failed to build query: %w", err) - } - - _, err = db.conn.Exec(sql, args...) - if err != nil { - return fmt.Errorf("failed to insert transcribe job: %w", err) - } - - return nil -} - -func (db *DB) GetFileByID(id string) (*models.File, error) { - query := db.gq.From("files").Where(goqu.C("id").Eq(id)) - sql, args, err := query.ToSQL() - if err != nil { - return nil, fmt.Errorf("failed to build query: %w", err) - } - - var file models.File - err = db.conn.QueryRow(sql, args...).Scan(&file.ID, &file.Type, &file.Size, &file.CreatedAt) - if err != nil { - return nil, fmt.Errorf("failed to get file: %w", err) - } - - return &file, nil -} - -func (db *DB) GetTranscribeJobByID(id string) (*models.TranscribeJob, error) { - query := db.gq.From("transcribe_jobs").Where(goqu.C("id").Eq(id)) - sql, args, err := query.ToSQL() - if err != nil { - return nil, fmt.Errorf("failed to build query: %w", err) - } - - var job models.TranscribeJob - err = db.conn.QueryRow(sql, args...).Scan(&job.ID, &job.Status, &job.FileID, &job.CreatedAt, &job.UpdatedAt) - if err != nil { - return nil, fmt.Errorf("failed to get transcribe job: %w", err) - } - - return &job, nil -} diff --git a/handlers/transcribe.go b/internal/controller/http/transcribe.go similarity index 65% rename from handlers/transcribe.go rename to internal/controller/http/transcribe.go index a71a148..6087fc7 100644 --- a/handlers/transcribe.go +++ b/internal/controller/http/transcribe.go @@ -1,4 +1,4 @@ -package handlers +package http import ( "fmt" @@ -8,27 +8,27 @@ import ( "path/filepath" "time" - "git.vakhrushev.me/av/transcriber/database" - "git.vakhrushev.me/av/transcriber/models" + "git.vakhrushev.me/av/transcriber/internal/entity" + "git.vakhrushev.me/av/transcriber/internal/repo" "github.com/gin-gonic/gin" "github.com/google/uuid" ) type TranscribeHandler struct { - db *database.DB + jobRepo repo.TranscriptJobRepository + fileRepo repo.FileRepository } -func NewTranscribeHandler(db *database.DB) *TranscribeHandler { - return &TranscribeHandler{db: db} +func NewTranscribeHandler(jobRepo repo.TranscriptJobRepository, fileRepo repo.FileRepository) *TranscribeHandler { + return &TranscribeHandler{jobRepo: jobRepo, fileRepo: fileRepo} } type TranscribeResponse struct { - JobID string `json:"job_id"` - FileID string `json:"file_id"` - Status string `json:"status"` + JobID string `json:"job_id"` + State string `json:"status"` } -func (h *TranscribeHandler) UploadAndTranscribe(c *gin.Context) { +func (h *TranscribeHandler) CreateTranscribeJob(c *gin.Context) { // Получаем файл из формы file, header, err := c.Request.FormFile("audio") if err != nil { @@ -38,7 +38,7 @@ func (h *TranscribeHandler) UploadAndTranscribe(c *gin.Context) { defer file.Close() // Генерируем UUID для файла - fileID := uuid.New().String() + fileId := uuid.New().String() // Определяем расширение файла ext := filepath.Ext(header.Filename) @@ -47,7 +47,7 @@ func (h *TranscribeHandler) UploadAndTranscribe(c *gin.Context) { } // Создаем путь для сохранения файла - fileName := fmt.Sprintf("%s%s", fileID, ext) + fileName := fmt.Sprintf("%s%s", fileId, ext) filePath := filepath.Join("data", "files", fileName) // Создаем файл на диске @@ -66,14 +66,14 @@ func (h *TranscribeHandler) UploadAndTranscribe(c *gin.Context) { } // Создаем запись в таблице files - fileRecord := &models.File{ - ID: fileID, - Type: header.Header.Get("Content-Type"), + fileRecord := &entity.File{ + Id: fileId, + Storage: entity.StorageLocal, Size: size, CreatedAt: time.Now(), } - if err := h.db.CreateFile(fileRecord); err != nil { + if err := h.fileRepo.Create(fileRecord); err != nil { // Удаляем файл если не удалось создать запись в БД os.Remove(filePath) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save file record"}) @@ -81,34 +81,33 @@ func (h *TranscribeHandler) UploadAndTranscribe(c *gin.Context) { } // Создаем запись в таблице transcribe_jobs - jobID := uuid.New().String() - job := &models.TranscribeJob{ - ID: jobID, - Status: models.StatusPending, - FileID: fileID, + jobId := uuid.New().String() + job := &entity.TranscribeJob{ + Id: jobId, + State: entity.StateCreated, + FileID: &fileId, + IsError: false, CreatedAt: time.Now(), - UpdatedAt: time.Now(), } - if err := h.db.CreateTranscribeJob(job); err != nil { + if err := h.jobRepo.Create(job); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create transcribe job"}) return } // Возвращаем успешный ответ response := TranscribeResponse{ - JobID: jobID, - FileID: fileID, - Status: models.StatusPending, + JobID: job.Id, + State: job.State, } c.JSON(http.StatusCreated, response) } -func (h *TranscribeHandler) GetJobStatus(c *gin.Context) { +func (h *TranscribeHandler) GetTranscribeJobStatus(c *gin.Context) { jobID := c.Param("id") - job, err := h.db.GetTranscribeJobByID(jobID) + job, err := h.jobRepo.GetByID(jobID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Job not found"}) return diff --git a/internal/entity/file.go b/internal/entity/file.go new file mode 100644 index 0000000..41a3d1c --- /dev/null +++ b/internal/entity/file.go @@ -0,0 +1,17 @@ +package entity + +import ( + "time" +) + +const ( + StorageLocal = "local" + StorageS3 = "s3" +) + +type File struct { + Id string + Storage string + Size int64 + CreatedAt time.Time +} diff --git a/internal/entity/job.go b/internal/entity/job.go new file mode 100644 index 0000000..b7da8c9 --- /dev/null +++ b/internal/entity/job.go @@ -0,0 +1,23 @@ +package entity + +import ( + "time" +) + +type TranscribeJob struct { + Id string + State string + FileID *string + IsError bool + ErrorText *string + Worker *string + AcquiredAt *time.Time + CreatedAt time.Time +} + +const ( + StateCreated = "created" + StateConverted = "converted" + StateUploaded = "uploaded" + StatusFailed = "failed" +) diff --git a/internal/repo/contracts.go b/internal/repo/contracts.go new file mode 100644 index 0000000..f2866a2 --- /dev/null +++ b/internal/repo/contracts.go @@ -0,0 +1,16 @@ +package repo + +import "git.vakhrushev.me/av/transcriber/internal/entity" + +type FileRepository interface { + Create(file *entity.File) error + GetByID(id string) (*entity.File, error) +} + +type TranscriptJobRepository interface { + Create(job *entity.TranscribeJob) error + GetByID(id string) (*entity.TranscribeJob, error) +} + +type ObjectStorage interface { +} diff --git a/internal/repo/sqlite/file_sqlite.go b/internal/repo/sqlite/file_sqlite.go new file mode 100644 index 0000000..dd792d7 --- /dev/null +++ b/internal/repo/sqlite/file_sqlite.go @@ -0,0 +1,55 @@ +package sqlite + +import ( + "database/sql" + "fmt" + + "git.vakhrushev.me/av/transcriber/internal/entity" + "github.com/doug-martin/goqu/v9" +) + +type FileRepository struct { + db *sql.DB + gq *goqu.Database +} + +func NewFileRepository(conn *sql.DB, gq *goqu.Database) *FileRepository { + return &FileRepository{conn, gq} +} + +func (repo *FileRepository) Create(file *entity.File) error { + record := goqu.Record{ + "id": file.Id, + "storage": file.Storage, + "size": file.Size, + "created_at": file.CreatedAt, + } + query := repo.gq.Insert("files").Rows(record) + sql, args, err := query.ToSQL() + if err != nil { + return fmt.Errorf("failed to build query: %w", err) + } + + _, err = repo.db.Exec(sql, args...) + if err != nil { + return fmt.Errorf("failed to insert file: %w", err) + } + + return nil +} + +func (repo *FileRepository) GetByID(id string) (*entity.File, error) { + query := repo.gq.From("files").Select("id", "storage", "size", "created_at").Where(goqu.C("id").Eq(id)) + sql, args, err := query.ToSQL() + if err != nil { + return nil, fmt.Errorf("failed to build query: %w", err) + } + + var file entity.File + err = repo.db.QueryRow(sql, args...).Scan(&file.Id, &file.Storage, &file.Size, &file.CreatedAt) + if err != nil { + return nil, fmt.Errorf("failed to get file: %w", err) + } + + return &file, nil +} diff --git a/internal/repo/sqlite/transcript_job_sqlite.go b/internal/repo/sqlite/transcript_job_sqlite.go new file mode 100644 index 0000000..76487cd --- /dev/null +++ b/internal/repo/sqlite/transcript_job_sqlite.go @@ -0,0 +1,77 @@ +package sqlite + +import ( + "database/sql" + "fmt" + + "git.vakhrushev.me/av/transcriber/internal/entity" + "github.com/doug-martin/goqu/v9" +) + +type TranscriptJobRepository struct { + db *sql.DB + gq *goqu.Database +} + +func NewTranscriptJobRepository(db *sql.DB, gq *goqu.Database) *TranscriptJobRepository { + return &TranscriptJobRepository{db, gq} +} + +func (repo *TranscriptJobRepository) Create(job *entity.TranscribeJob) error { + record := goqu.Record{ + "id": job.Id, + "state": job.State, + "file_id": job.FileID, + "is_error": job.IsError, + "error_text": job.ErrorText, + "worker": job.Worker, + "acquired_at": job.AcquiredAt, + "created_at": job.CreatedAt, + } + query := repo.gq.Insert("transcribe_jobs").Rows(record) + sql, args, err := query.ToSQL() + if err != nil { + return fmt.Errorf("failed to build query: %w", err) + } + + _, err = repo.db.Exec(sql, args...) + if err != nil { + return fmt.Errorf("failed to insert transcribe job: %w", err) + } + + return nil +} + +func (repo *TranscriptJobRepository) GetByID(id string) (*entity.TranscribeJob, error) { + query := repo.gq.From("transcribe_jobs").Select( + "id", + "state", + "file_id", + "is_error", + "error_text", + "worker", + "acquired_at", + "created_at", + ).Where(goqu.C("id").Eq(id)) + sql, args, err := query.ToSQL() + if err != nil { + return nil, fmt.Errorf("failed to build query: %w", err) + } + + var job entity.TranscribeJob + err = repo.db.QueryRow(sql, args...).Scan( + &job.Id, + &job.State, + &job.FileID, + &job.IsError, + &job.ErrorText, + &job.Worker, + &job.AcquiredAt, + &job.CreatedAt, + ) + if err != nil { + return nil, fmt.Errorf("failed to get transcribe job: %w", err) + } + + return &job, nil +} diff --git a/main.go b/main.go index 09d29db..e1961a0 100644 --- a/main.go +++ b/main.go @@ -1,12 +1,18 @@ package main import ( + "database/sql" + "fmt" "log" "os" - "git.vakhrushev.me/av/transcriber/database" - "git.vakhrushev.me/av/transcriber/handlers" + "git.vakhrushev.me/av/transcriber/internal/controller/http" + "git.vakhrushev.me/av/transcriber/internal/repo/sqlite" + "github.com/doug-martin/goqu/v9" + _ "github.com/doug-martin/goqu/v9/dialect/sqlite3" "github.com/gin-gonic/gin" + _ "github.com/mattn/go-sqlite3" + "github.com/pressly/goose/v3" ) func main() { @@ -15,29 +21,37 @@ func main() { log.Fatal("Failed to create data/files directory:", err) } - // Инициализируем базу данных - db, err := database.New("data/transcriber.db") + db, err := sql.Open("sqlite3", "data/transcriber.db") if err != nil { - log.Fatal("Failed to initialize database:", err) + log.Fatalf("failed to open database: %v", err) } defer db.Close() + if err := db.Ping(); err != nil { + log.Fatalf("failed to ping database: %v", err) + } + + gq := goqu.New("sqlite3", db) + // Запускаем миграции - if err := db.RunMigrations("migrations"); err != nil { + if err := RunMigrations(db, "migrations"); err != nil { log.Fatal("Failed to run migrations:", err) } + fileRepo := sqlite.NewFileRepository(db, gq) + jobRepo := sqlite.NewTranscriptJobRepository(db, gq) + + // Инициализируем обработчики + transcribeHandler := http.NewTranscribeHandler(jobRepo, fileRepo) + // Создаем Gin роутер r := gin.Default() - // Инициализируем обработчики - transcribeHandler := handlers.NewTranscribeHandler(db) - // Настраиваем роуты api := r.Group("/api") { - api.POST("/transcribe", transcribeHandler.UploadAndTranscribe) - api.GET("/transcribe/:id", transcribeHandler.GetJobStatus) + api.POST("/transcribe/audio", transcribeHandler.CreateTranscribeJob) + api.GET("/transcribe/:id", transcribeHandler.GetTranscribeJobStatus) } // Добавляем middleware для обработки больших файлов @@ -56,3 +70,16 @@ func main() { log.Fatal("Failed to start server:", err) } } + +func RunMigrations(db *sql.DB, migrationsDir string) error { + if err := goose.SetDialect("sqlite3"); err != nil { + return fmt.Errorf("failed to set goose dialect: %w", err) + } + + if err := goose.Up(db, migrationsDir); err != nil { + return fmt.Errorf("failed to run migrations: %w", err) + } + + log.Println("Migrations completed successfully") + return nil +} diff --git a/migrations/001_create_files_table.sql b/migrations/001_create_files_table.sql index dbef4fb..335818b 100644 --- a/migrations/001_create_files_table.sql +++ b/migrations/001_create_files_table.sql @@ -1,7 +1,7 @@ -- +goose Up CREATE TABLE files ( id TEXT PRIMARY KEY, - type TEXT NOT NULL, + storage TEXT NOT NULL, size INTEGER NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); diff --git a/migrations/002_create_transcribe_jobs_table.sql b/migrations/002_create_transcribe_jobs_table.sql index bdbe8e1..b0185b7 100644 --- a/migrations/002_create_transcribe_jobs_table.sql +++ b/migrations/002_create_transcribe_jobs_table.sql @@ -1,10 +1,14 @@ -- +goose Up CREATE TABLE transcribe_jobs ( id TEXT PRIMARY KEY, - status TEXT NOT NULL DEFAULT 'pending', - file_id TEXT NOT NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + state TEXT NOT NULL, + file_id TEXT, + is_error BOOLEAN NOT NULL, + error_text TEXT, + worker TEXT, + acquired_at DATETIME, + created_at DATETIME NOT NULL, + FOREIGN KEY (file_id) REFERENCES files(id) ); diff --git a/models/models.go b/models/models.go deleted file mode 100644 index e9d669d..0000000 --- a/models/models.go +++ /dev/null @@ -1,27 +0,0 @@ -package models - -import ( - "time" -) - -type File struct { - ID string `db:"id" json:"id"` - Type string `db:"type" json:"type"` - Size int64 `db:"size" json:"size"` - CreatedAt time.Time `db:"created_at" json:"created_at"` -} - -type TranscribeJob struct { - ID string `db:"id" json:"id"` - Status string `db:"status" json:"status"` - FileID string `db:"file_id" json:"file_id"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - UpdatedAt time.Time `db:"updated_at" json:"updated_at"` -} - -const ( - StatusPending = "pending" - StatusProcessing = "processing" - StatusCompleted = "completed" - StatusFailed = "failed" -)