refactor #6
8 changed files with 281 additions and 126 deletions
|
@ -12,7 +12,7 @@ func main() {
|
||||||
config := config.New()
|
config := config.New()
|
||||||
|
|
||||||
// Конфигурация сервера
|
// Конфигурация сервера
|
||||||
router := router.NewRouter(*config).SetupRoutes()
|
router := router.New(config).Setup()
|
||||||
|
|
||||||
log.Printf("Сервер запущен на порту: %s", config.Port)
|
log.Printf("Сервер запущен на порту: %s", config.Port)
|
||||||
http.ListenAndServe(":"+config.Port, router)
|
http.ListenAndServe(":"+config.Port, router)
|
||||||
|
|
37
internal/common/errors/http-errors.go
Normal file
37
internal/common/errors/http-errors.go
Normal file
|
@ -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",
|
||||||
|
}
|
||||||
|
}
|
98
internal/controllers/taskcontroller/controller.go
Normal file
98
internal/controllers/taskcontroller/controller.go
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
23
internal/middlewares/auth.go
Normal file
23
internal/middlewares/auth.go
Normal file
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,11 +6,6 @@ type FileUpload struct {
|
||||||
FileName string
|
FileName string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ErrResponse struct {
|
|
||||||
Message string `json:"message"`
|
|
||||||
Code int `json:"code"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Task struct {
|
type Task struct {
|
||||||
TaskID string `json:"task_id"`
|
TaskID string `json:"task_id"`
|
||||||
Link string `json:"link"`
|
Link string `json:"link"`
|
||||||
|
|
|
@ -1,136 +1,41 @@
|
||||||
package router
|
package router
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"github.com/go-chi/chi/v5"
|
||||||
"fmt"
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"code.alt-gnome.ru/aides-infra/aides-repo-api/internal/config"
|
"code.alt-gnome.ru/aides-infra/aides-repo-api/internal/config"
|
||||||
"code.alt-gnome.ru/aides-infra/aides-repo-api/internal/models"
|
"code.alt-gnome.ru/aides-infra/aides-repo-api/internal/controllers/taskcontroller"
|
||||||
|
"code.alt-gnome.ru/aides-infra/aides-repo-api/internal/middlewares"
|
||||||
"github.com/go-chi/render"
|
"code.alt-gnome.ru/aides-infra/aides-repo-api/internal/services/taskservice"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5" //Импорт пакета chi для маршрутизации
|
|
||||||
"github.com/go-chi/chi/v5/middleware" // Импорт middleware для логирования
|
|
||||||
)
|
)
|
||||||
|
|
||||||
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 {
|
type Router struct {
|
||||||
Config config.Config
|
config *config.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
// Создает Роутер
|
func New(config *config.Config) *Router {
|
||||||
func NewRouter(cfg config.Config) *Router {
|
return &Router{
|
||||||
return &Router{Config: cfg}
|
config: config,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Метод настройки маршрутов для Роутера
|
func (r *Router) Setup() *chi.Mux {
|
||||||
func (r *Router) SetupRoutes() *chi.Mux {
|
|
||||||
router := chi.NewRouter()
|
router := chi.NewRouter()
|
||||||
router.Use(middleware.Logger)
|
router.Use(middleware.Logger)
|
||||||
// Создаем директорию для загрузки
|
|
||||||
os.MkdirAll(path.Join(r.Config.UploadDir, "out"), os.ModePerm)
|
taskService := taskservice.New(
|
||||||
// Определяем маршрут для загрузки файлов("/upload/{executionID}" путь, по которому будет доступен данный маршрут.
|
r.config,
|
||||||
//Путь включает переменную часть {executionID}, которая позволяет извлекать динамические параметры из URL.)
|
)
|
||||||
router.Post("/upload/{repo}/task/{taskID}", r.uploadHandler)
|
|
||||||
|
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
|
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)
|
|
||||||
}
|
|
||||||
|
|
77
internal/services/taskservice/service.go
Normal file
77
internal/services/taskservice/service.go
Normal file
|
@ -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
|
||||||
|
}
|
20
internal/services/taskservice/utils.go
Normal file
20
internal/services/taskservice/utils.go
Normal file
|
@ -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
|
||||||
|
}
|
Loading…
Reference in a new issue