From 4160885541e8825b813bba1073767e0011c568fe Mon Sep 17 00:00:00 2001 From: Maxim Slipenko Date: Thu, 12 Dec 2024 16:27:07 +0300 Subject: [PATCH] refactor: start clean architecture --- cmd/aides-repo-api/main.go | 2 +- internal/common/errors/http-errors.go | 37 +++++ .../controllers/taskcontroller/controller.go | 98 ++++++++++++ internal/middlewares/auth.go | 23 +++ internal/models/models.go | 5 - internal/router/router.go | 145 +++--------------- internal/services/taskservice/service.go | 77 ++++++++++ internal/services/taskservice/utils.go | 20 +++ 8 files changed, 281 insertions(+), 126 deletions(-) create mode 100644 internal/common/errors/http-errors.go create mode 100644 internal/controllers/taskcontroller/controller.go create mode 100644 internal/middlewares/auth.go create mode 100644 internal/services/taskservice/service.go create mode 100644 internal/services/taskservice/utils.go diff --git a/cmd/aides-repo-api/main.go b/cmd/aides-repo-api/main.go index 6ae3bf4..8ff30a0 100644 --- a/cmd/aides-repo-api/main.go +++ b/cmd/aides-repo-api/main.go @@ -12,7 +12,7 @@ func main() { config := config.New() // Конфигурация сервера - router := router.NewRouter(*config).SetupRoutes() + router := router.New(config).Setup() log.Printf("Сервер запущен на порту: %s", config.Port) http.ListenAndServe(":"+config.Port, router) diff --git a/internal/common/errors/http-errors.go b/internal/common/errors/http-errors.go new file mode 100644 index 0000000..ec771f1 --- /dev/null +++ b/internal/common/errors/http-errors.go @@ -0,0 +1,37 @@ +package errors + +import ( + "net/http" + + "github.com/go-chi/render" +) + +type ErrResponse struct { + Err error `json:"-"` // low-level runtime error + HTTPStatusCode int `json:"-"` // http response status code + + StatusText string `json:"status"` // user-level status message + AppCode int64 `json:"code,omitempty"` // application-specific error code + ErrorText string `json:"error,omitempty"` // application-level error message, for debugging +} + +func (e *ErrResponse) Render(w http.ResponseWriter, r *http.Request) error { + render.Status(r, e.HTTPStatusCode) + return nil +} + +func ErrRender(err error) render.Renderer { + return &ErrResponse{ + Err: err, + HTTPStatusCode: http.StatusUnprocessableEntity, + StatusText: "Error rendering response.", + ErrorText: err.Error(), + } +} + +func ErrUnauthorized() render.Renderer { + return &ErrResponse{ + HTTPStatusCode: http.StatusUnauthorized, + StatusText: "Unauthorized", + } +} diff --git a/internal/controllers/taskcontroller/controller.go b/internal/controllers/taskcontroller/controller.go new file mode 100644 index 0000000..24702e4 --- /dev/null +++ b/internal/controllers/taskcontroller/controller.go @@ -0,0 +1,98 @@ +package taskcontroller + +import ( + "net/http" + + "code.alt-gnome.ru/aides-infra/aides-repo-api/internal/common/errors" + "code.alt-gnome.ru/aides-infra/aides-repo-api/internal/config" + "code.alt-gnome.ru/aides-infra/aides-repo-api/internal/services/taskservice" + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" +) + +type TaskUploadResponse struct { + TaskID string `json:"taskID"` + Repo string `json:"repo"` + StatusText string `json:"status"` +} + +func (rd *TaskUploadResponse) Render(w http.ResponseWriter, r *http.Request) error { + render.Status(r, http.StatusOK) + return nil +} + +type TaskController struct { + config *config.Config + taskService *taskservice.Service +} + +func New(cfg *config.Config, taskService *taskservice.Service) *TaskController { + return &TaskController{ + config: cfg, + taskService: taskService, + } +} + +func (c *TaskController) Upload(w http.ResponseWriter, r *http.Request) { + taskID := chi.URLParam(r, "taskID") + if taskID == "" { + render.Render(w, r, &errors.ErrResponse{ + HTTPStatusCode: http.StatusBadRequest, + StatusText: "taskID is required", + }) + return + } + + err := r.ParseMultipartForm(10240 << 20) + if err != nil { + render.Render(w, r, &errors.ErrResponse{ + HTTPStatusCode: http.StatusBadRequest, + StatusText: "Bad Request", + }) + return + } + + repo := r.FormValue("repo") + if repo == "" { + render.Render(w, r, &errors.ErrResponse{ + HTTPStatusCode: http.StatusBadRequest, + StatusText: "Missing required repo field", + }) + return + } + + files := r.MultipartForm.File["files"] + for _, fileHeader := range files { + if fileHeader.Size > (1024 << 20) { // Limit each file size to 10MB + render.Render(w, r, &errors.ErrResponse{ + HTTPStatusCode: http.StatusBadRequest, + StatusText: "File too large", + }) + return + } + } + + err = c.taskService.Upload(&taskservice.TaskUploadInput{ + TaskID: taskID, + Repo: repo, + Files: files, + }) + if err != nil { + render.Render(w, r, &errors.ErrResponse{ + HTTPStatusCode: http.StatusInternalServerError, + StatusText: "Internal Server Error", + Err: err, + }) + } + + response := TaskUploadResponse{ + TaskID: taskID, + Repo: repo, + StatusText: "Success!", + } + + if err := render.Render(w, r, &response); err != nil { + render.Render(w, r, errors.ErrRender(err)) + return + } +} diff --git a/internal/middlewares/auth.go b/internal/middlewares/auth.go new file mode 100644 index 0000000..06730a7 --- /dev/null +++ b/internal/middlewares/auth.go @@ -0,0 +1,23 @@ +package middlewares + +import ( + "net/http" + + "code.alt-gnome.ru/aides-infra/aides-repo-api/internal/common/errors" + "code.alt-gnome.ru/aides-infra/aides-repo-api/internal/config" + "github.com/go-chi/render" +) + +func CreateAuthGuard(cfg *config.Config) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Получаем значение из заголовка + token := r.Header.Get("Authorization") + if token != "Bearer "+cfg.Token { + render.Render(w, r, errors.ErrUnauthorized()) + return + } + next.ServeHTTP(w, r) + }) + } +} diff --git a/internal/models/models.go b/internal/models/models.go index f923486..5d0d593 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -6,11 +6,6 @@ type FileUpload struct { FileName string } -type ErrResponse struct { - Message string `json:"message"` - Code int `json:"code"` -} - type Task struct { TaskID string `json:"task_id"` Link string `json:"link"` diff --git a/internal/router/router.go b/internal/router/router.go index 928326c..b27a9a2 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -1,136 +1,41 @@ package router import ( - "encoding/json" - "fmt" - "io" - "net/http" - "os" - "path" - "strconv" + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" "code.alt-gnome.ru/aides-infra/aides-repo-api/internal/config" - "code.alt-gnome.ru/aides-infra/aides-repo-api/internal/models" - - "github.com/go-chi/render" - - "github.com/go-chi/chi/v5" //Импорт пакета chi для маршрутизации - "github.com/go-chi/chi/v5/middleware" // Импорт middleware для логирования + "code.alt-gnome.ru/aides-infra/aides-repo-api/internal/controllers/taskcontroller" + "code.alt-gnome.ru/aides-infra/aides-repo-api/internal/middlewares" + "code.alt-gnome.ru/aides-infra/aides-repo-api/internal/services/taskservice" ) -func createSymlink(target, link string) error { - if _, err := os.Lstat(link); err == nil { - if err := os.Remove(link); err != nil { - return fmt.Errorf("failed to remove existing file or symlink: %w", err) - } - } - - if err := os.Symlink(target, link); err != nil { - return fmt.Errorf("failed to create symlink: %w", err) - } - - return nil -} - type Router struct { - Config config.Config + config *config.Config } -// Создает Роутер -func NewRouter(cfg config.Config) *Router { - return &Router{Config: cfg} +func New(config *config.Config) *Router { + return &Router{ + config: config, + } } -// Метод настройки маршрутов для Роутера -func (r *Router) SetupRoutes() *chi.Mux { +func (r *Router) Setup() *chi.Mux { router := chi.NewRouter() router.Use(middleware.Logger) - // Создаем директорию для загрузки - os.MkdirAll(path.Join(r.Config.UploadDir, "out"), os.ModePerm) - // Определяем маршрут для загрузки файлов("/upload/{executionID}" путь, по которому будет доступен данный маршрут. - //Путь включает переменную часть {executionID}, которая позволяет извлекать динамические параметры из URL.) - router.Post("/upload/{repo}/task/{taskID}", r.uploadHandler) + + taskService := taskservice.New( + r.config, + ) + + taskController := taskcontroller.New( + r.config, + taskService, + ) + + router.Route("/task/{taskID}", func(cr chi.Router) { + cr.With(middlewares.CreateAuthGuard(r.config)).Post("/upload", taskController.Upload) + }) + return router } - -// Метод отвечает за загрузку файлов -func (r *Router) uploadHandler(w http.ResponseWriter, req *http.Request) { - // Проверка авторизации - if req.Header.Get("Authorization") != "Bearer "+r.Config.Token { - render.JSON(w, req, models.ErrResponse{Message: "Не авторизованный", Code: http.StatusUnauthorized}) - return - } - // Извлекаем параметр taskID из URL и проверяем его наличие - taskID := chi.URLParam(req, "taskID") - repo := chi.URLParam(req, "repo") - if taskID == "" || repo == "" { - render.JSON(w, req, models.ErrResponse{Message: "Требуются параметры [repo] и [taskID]", Code: http.StatusBadRequest}) - return - } - - // Чтение файлов из запроса - err := req.ParseMultipartForm(r.Config.MaxSizeUpload) // Лимит 100 MB - if err != nil { - render.JSON(w, req, models.ErrResponse{Message: "Парсинг не удался", Code: http.StatusBadRequest}) - return - } - // При успешном парсинге извлекаем файлы - files := req.MultipartForm.File["files"] // Карта где ключами являются имена полей формы, а значениями — массивы заголовков файлов. - localPath := path.Join(repo, "task", taskID) - taskFolderPath := path.Join(r.Config.UploadDir, "extra", localPath) - os.MkdirAll(taskFolderPath, os.ModePerm) - for _, fileHeader := range files { - file, err := fileHeader.Open() - if err != nil { - render.JSON(w, req, models.ErrResponse{Message: "Не удается открыть файл", Code: http.StatusInternalServerError}) - return - } - defer file.Close() - - // Полный путь для файла - filePath := path.Join(taskFolderPath, fileHeader.Filename) - - //Удаляем файл если такой уже существует - if _, err := os.Stat(filePath); err == nil { - err = os.Remove(filePath) - if err != nil { - render.JSON(w, req, models.ErrResponse{Message: "Не удалось удалить файл", Code: http.StatusInternalServerError}) - return - } - } - // Сохранение файла на сервере - outFile, err := os.Create(filePath) - if err != nil { - render.JSON(w, req, models.ErrResponse{Message: "Не удается создать файл", Code: http.StatusInternalServerError}) - return - } - defer outFile.Close() - - _, err = io.Copy(outFile, file) - if err != nil { - render.JSON(w, req, models.ErrResponse{Message: "Не удалось сохранить файл", Code: http.StatusInternalServerError}) - return - } - // Символическая ссылка - targetPath := path.Join("../extra/", localPath, fileHeader.Filename) - symLink := path.Join(r.Config.UploadDir, "out", fileHeader.Filename) - err = createSymlink(targetPath, symLink) - if err != nil { - render.JSON(w, req, models.ErrResponse{Message: "Не удается создать ссылку", Code: http.StatusInternalServerError}) - return - } - - } - - //Ответы в формате JSON - - resp := map[string]string{ - "taskID": taskID, - "repository": repo, - "message": "Файлы успешно загружены", - "fileCount": strconv.Itoa(len(files)), - } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(resp) -} diff --git a/internal/services/taskservice/service.go b/internal/services/taskservice/service.go new file mode 100644 index 0000000..6f0f292 --- /dev/null +++ b/internal/services/taskservice/service.go @@ -0,0 +1,77 @@ +package taskservice + +import ( + "io" + "mime/multipart" + "os" + "path" + + "code.alt-gnome.ru/aides-infra/aides-repo-api/internal/config" +) + +type Service struct { + config *config.Config +} + +type TaskUploadInput struct { + TaskID string + Repo string + + Files []*multipart.FileHeader +} + +func New(cfg *config.Config) *Service { + return &Service{ + config: cfg, + } +} + +func (s *Service) Upload(input *TaskUploadInput) error { + repo := input.Repo + taskID := input.TaskID + files := input.Files + + localPath := path.Join(repo, "task", taskID) + taskFolderPath := path.Join(s.config.UploadDir, "extra", localPath) + os.MkdirAll(taskFolderPath, os.ModePerm) + + for _, fileHeader := range files { + file, err := fileHeader.Open() + if err != nil { + return err + } + defer file.Close() + + // Полный путь для файла + filePath := path.Join(taskFolderPath, fileHeader.Filename) + + //Удаляем файл если такой уже существует + if _, err := os.Stat(filePath); err == nil { + err = os.Remove(filePath) + if err != nil { + return err + } + } + // Сохранение файла на сервере + outFile, err := os.Create(filePath) + if err != nil { + return err + } + defer outFile.Close() + + _, err = io.Copy(outFile, file) + if err != nil { + return err + } + // Символическая ссылка + targetPath := path.Join("../extra/", localPath, fileHeader.Filename) + symLink := path.Join(s.config.UploadDir, "out", fileHeader.Filename) + err = createSymlink(targetPath, symLink) + if err != nil { + return err + } + + } + + return nil +} diff --git a/internal/services/taskservice/utils.go b/internal/services/taskservice/utils.go new file mode 100644 index 0000000..8f5ee72 --- /dev/null +++ b/internal/services/taskservice/utils.go @@ -0,0 +1,20 @@ +package taskservice + +import ( + "fmt" + "os" +) + +func createSymlink(target, link string) error { + if _, err := os.Lstat(link); err == nil { + if err := os.Remove(link); err != nil { + return fmt.Errorf("failed to remove existing file or symlink: %w", err) + } + } + + if err := os.Symlink(target, link); err != nil { + return fmt.Errorf("failed to create symlink: %w", err) + } + + return nil +}