From e9585bef5071fb949d90a75f875c63a599e87b7e Mon Sep 17 00:00:00 2001 From: mrrchristmas Date: Wed, 4 Dec 2024 14:46:42 +0300 Subject: [PATCH] Initial commit --- .gitignore | 132 ++++++++++++++++++++++++++++++++++++++ .idea/.gitignore | 8 +++ .idea/aides-repo-api.iml | 9 +++ .idea/modules.xml | 8 +++ .idea/vcs.xml | 6 ++ Dockerfile | 15 +++++ go.mod | 9 +++ go.sum | 6 ++ main.go | 23 +++++++ models/models.go | 21 +++++++ router/router.go | 133 +++++++++++++++++++++++++++++++++++++++ 11 files changed, 370 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/aides-repo-api.iml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 Dockerfile create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 models/models.go create mode 100644 router/router.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c7348ad --- /dev/null +++ b/.gitignore @@ -0,0 +1,132 @@ +### Go ### + +# Двоичные файлы для программ и плагинов +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Тестовый двоичный файл, созданный с помощью `go test -c` +*.test + +# Вывод инструмента покрытия кода go, в частности при использовании с LiteIDE +*.out + +# Каталоги зависимостей (удалите комментарий ниже, чтобы включить их) +# vendor/ + +# Файл рабочей области Go +go.work + +### GoLand ### + +# Пользовательские настройки +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Файлы, относящиеся к пользователю AWS +.idea/**/aws.xml + +# Сгенерированные файлы +.idea/**/contentModel.xml + +# Конфиденциальные или часто изменяемые файлы +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle и Maven с автоматическим импортом +# При использовании Gradle или Maven с автоматическим импортом следует исключить файлы модулей, +# так как они будут воссоздаваться и могут вызывать сбои. Раскомментируйте, если используете +# автоматический импорт. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# плагин Mongo Explorer +.idea/**/mongoSettings.xml + +# Формат проекта на основе файлов +*.iws + +# IntelliJ +out/ + +# плагин mpeltonen/sbt-idea +.idea_modules/ + +# Плагин JIRA +atlassian-ide-plugin.xml + +# Плагин Cursive Clojure +.idea/replstate.xml + +# Плагин SonarLint +.idea/sonarlint/ + +# Плагин Crashlytics (для Android Studio и IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Клиент REST на основе редактора +.idea/httpRequests + +# Файл сериализованного кэша Android Studio 3.1+ +.idea/caches/build_file_checksums.ser + +### Исправление для GoLand ### +# Причина комментария: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# Плагин Sonarlint +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# Плагин SonarQube +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Плагин Markdown Navigator +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Ошибка при создании файла кэша +# См. https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# Плагин CodeStream +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + +# Плагин Azure Toolkit для IntelliJ +# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij +.idea/**/azureSettings.xml + +# Игнорируем папку uploads +uploads \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/aides-repo-api.iml b/.idea/aides-repo-api.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/aides-repo-api.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..c90c71a --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..545fbf9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +# Первый этап: сборка приложения +FROM golang:latest AS builder + +WORKDIR /rest-app + +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +RUN go build -o alt-storage + +# Второй этап: создание финального образа +FROM registry.altlinux.org/alt/alt:sisyphus + +COPY --from=builder /rest-app/alt-storage /bin/main +ENTRYPOINT ["/bin/main"] \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b88f40a --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module rest_api + +go 1.23.3 + +require ( + github.com/ajg/form v1.5.1 // indirect + github.com/go-chi/chi/v5 v5.1.0 // indirect + github.com/go-chi/render v1.0.3 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0b7bdf6 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= +github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= +github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= +github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= diff --git a/main.go b/main.go new file mode 100644 index 0000000..a132d6a --- /dev/null +++ b/main.go @@ -0,0 +1,23 @@ +package main + +import ( + "log" + "net/http" + "rest_api/models" + "rest_api/router" +) + +func main() { + config := models.Config{ + Token: "Alt", + UploadDir: "./uploads", // Папка для сохранения файлов + MaxSizeUpload: 100 * 1024 * 1024, // Max размер файла + TaskDir: "extra", + } + + router := router.NewRouter(config).SetupRoutes() + + log.Println("Сервер запущен на :8080") + http.ListenAndServe(":8080", router) + +} diff --git a/models/models.go b/models/models.go new file mode 100644 index 0000000..d2f19db --- /dev/null +++ b/models/models.go @@ -0,0 +1,21 @@ +package models + +type Config struct { + Token string + Repo string + UploadDir string + TaskDir string + SymLink string + MaxSizeUpload int64 +} + +type FileUpload struct { + TaskID string + + FileName string +} + +type ErrResponse struct { + Message string `json:"message"` + Code int `json:"code"` +} diff --git a/router/router.go b/router/router.go new file mode 100644 index 0000000..d86e52a --- /dev/null +++ b/router/router.go @@ -0,0 +1,133 @@ +package router + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "path" + "rest_api/models" // Импорт пакета с моделями, где определены структуры конфигурации + "strconv" + + "github.com/go-chi/render" + + "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 { + Config models.Config +} + +// Создает Роутер +func NewRouter(cfg models.Config) *Router { + return &Router{Config: cfg} +} + +// Метод настройки маршрутов для Роутера +func (r *Router) SetupRoutes() *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) + 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) +}