refactor #6

Merged
maks1ms merged 3 commits from refactor into main 2024-12-12 13:27:39 +00:00
9 changed files with 309 additions and 143 deletions

View file

@ -4,19 +4,15 @@ import (
"log"
"net/http"
"code.alt-gnome.ru/aides-infra/aides-repo-api/internal/models"
"code.alt-gnome.ru/aides-infra/aides-repo-api/internal/config"
"code.alt-gnome.ru/aides-infra/aides-repo-api/internal/router"
"github.com/caarlos0/env/v11"
)
func main() {
var config models.Config
if err := env.Parse(&config); err != nil {
log.Fatalf("ошибка при парсинге переменных %v", err)
}
config := config.New()
// Конфигурация сервера
router := router.NewRouter(config).SetupRoutes()
router := router.New(config).Setup()
log.Printf("Сервер запущен на порту: %s", config.Port)
http.ListenAndServe(":"+config.Port, router)

View 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",
}
}

24
internal/config/config.go Normal file
View file

@ -0,0 +1,24 @@
package config
import (
"log"
"github.com/caarlos0/env/v11"
)
type Config struct {
Token string `env:"TOKEN"`
UploadDir string `env:"UPLOAD_DIR" envDefault:"./uploads"`
Port string `env:"PORT" envDefault:"8080"`
MaxSizeUpload int64 `env:"MAX_SIZE_UPLOAD" envDefault:"104857600"` //100 MB
}
func New() *Config {
config := new(Config)
if err := env.Parse(config); err != nil {
log.Fatalf("ошибка при парсинге переменных %v", err)
}
return config
}

View 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
}
}

View 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)
})
}
}

View file

@ -1,26 +1,11 @@
package models
type Config struct {
Token string `env:"TOKEN"`
Repo string `env:"REPO"`
UploadDir string `env:"UPLOAD_DIR" envDefault:"./uploads"`
TaskDir string `env:"TASK_DIR" envDefault:"./tasks"`
SymLink string `env:"SYM_LINK"`
Port string `env:"PORT" envDefault:"8080"`
MaxSizeUpload int64 `env:"MAX_SIZE_UPLOAD" envDefault:"104857600"` //100 MB
}
type FileUpload struct {
TaskID string
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"`

View file

@ -1,135 +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/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/config"
"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 models.Config
config *config.Config
}
// Создает Роутер
func NewRouter(cfg models.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)
}

View 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
}

View 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
}