diff --git a/file-transfer/Makefile b/file-transfer/Makefile new file mode 100644 index 0000000..1cacac8 --- /dev/null +++ b/file-transfer/Makefile @@ -0,0 +1,43 @@ +BINARY_NAME=main +OS := $(shell uname | tr '[:upper:]' '[:lower:]') +ARCH := $(shell uname -m | sed 's/x86_64/amd64/') + + +.DEFAULT_GOAL := help + +.PHONY: help +help: ## Show the help message. + @echo "Usage: make " + @echo "" + @echo "Targets:" + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}' + +.PHONY: build +build: ## Build the application. + GOARCH=amd64 GOOS=darwin go build -o ${BINARY_NAME}-darwin main.go + GOARCH=amd64 GOOS=linux go build -o ${BINARY_NAME}-linux main.go + GOARCH=amd64 GOOS=windows go build -o ${BINARY_NAME}-windows main.go + +.PHONY: run +run: build ## Run the application. + ./${BINARY_NAME}-${OS} + +.PHONY: test +test: ## Run tests. + go test -v ./... -coverprofile=coverage.out + +.PHONY: dep +dep: ## Install dependencies. + go mod download + +.PHONY: clean +clean: ## Clean project by deleting files in .gitignore. + go clean + rm ${BINARY_NAME}-darwin + rm ${BINARY_NAME}-linux + rm ${BINARY_NAME}-windows + git clean -Xdf + +.PHONY: clean +clean: ## Clean project by deleting files in .gitignore. + diff --git a/file-transfer/README.md b/file-transfer/README.md index b4b77ee..167098a 100644 --- a/file-transfer/README.md +++ b/file-transfer/README.md @@ -11,7 +11,7 @@ It communicates with other services to store retrieve specific files. - [Docker](https://docs.docker.com/engine/install/) - [Docker Compose](https://docs.docker.com/compose/install/) -## Usage +## Configuration This microservice is dependent on MongoDB database so before starting the web server you need to provide connection configuration to MongoDB database. @@ -19,29 +19,72 @@ This microservice is dependent on MongoDB database so before starting the web se You can run MongoDB locally with `docker compose` by using `docker compose run mongodb` command in project _file-transfer_ directory. Then you need to create `.env` file with MongoDB configuration. -Example configuration: +Example configuration for MongoDB with user `root` and password `example`: ```bash -MONGODB_URI=mongodb://localhost:27017/ -MONGODB_DB_USER=root -MONGODB_DB_PASSWORD=example +MONGODB_URI=mongodb://root:example@localhost:27017/ ``` ### Connecting to existing instance Provide connection details in `.env` file. -### Building and downloading packages +### Connecting to Storage + +#### Azure Storage + +You need to specify `AZURE_STORAGE_ACCOUNT_NAME` and `AZURE_STORAGE_ACCOUNT_KEY` in `.env` file. +Example configuration for Azure Storage: ```bash -go mod download -go mod build -o main +AZURE_STORAGE_ACCOUNT_NAME=example +AZURE_STORAGE_ACCOUNT_KEY=example ``` -### Running the project +#### Local Storage + +Alternatively you can use local storage by providing `LOCAL_STORAGE_PATH` and `STORAGE_TYPE` in `.env` file. +Example configuration for local storage: ```bash -./main +LOCAL_STORAGE_PATH=/tmp +STORAGE_TYPE=local ``` -It should start web server on specified port and connect to MongoDB. +## Usage + +### Running + +To run the microservice execute `make run` command in project _file-transfer_ directory. + +### Testing + +To run tests execute `make test` command in project _file-transfer_ directory. + +### Example usage + +To upload file you need to send POST request to `/files` endpoint with file in body. +Example request using `curl`: + +```bash +curl -X POST "http://localhost:8080/file" \ + -H "Content-Type: application/json" \ + -d '{ + "file_name": "test.txt", + "user_id": "123" + }' +``` + +In response you will receive file id which you can use to download file. + +To download file you need to send GET request to `/files/{file_id}` endpoint. +Example request using `curl`: + +```bash +curl -X GET "http://localhost:8080/file/123" \ + -H "Content-Type: application/json" +``` + +### Swagger + +To access Swagger documentation go to `http://localhost:8080/swagger/index.html`. diff --git a/file-transfer/app/app.go b/file-transfer/app/app.go index 375f2a4..0bcc4a1 100644 --- a/file-transfer/app/app.go +++ b/file-transfer/app/app.go @@ -10,7 +10,8 @@ import ( "net/http" "github.com/gorilla/mux" - "go.mongodb.org/mongo-driver/mongo" + "github.com/joho/godotenv" + "go.mongodb.org/mongo-driver/v2/mongo" ) type App struct { @@ -19,21 +20,41 @@ type App struct { Logger *log.Logger MongoClient *mongo.Client MongoCollection *mongo.Collection + BlobStorage db.BlobStorage } -func (a *App) Initialize() { +func (a *App) Initialize(ctx *context.Context) { a.Router = mux.NewRouter().StrictSlash(true) a.Logger = log.New(os.Stdout, "server: ", log.Flags()) + a.MongoCollection, a.MongoClient = db.InitMongo(ctx) + + _ = godotenv.Load("./.env") + + storageType := os.Getenv("STORAGE_TYPE") + + if storageType == "local" { + path := os.Getenv("LOCAL_STORAGE_PATH") + a.BlobStorage, _ = db.InitLocalBlobStorage(path) + } else { + a.BlobStorage, _ = db.InitAzureBlobStorage("files") + } + logMiddleware := NewLogMiddleware(a.Logger) + corsMiddleware := NewCORSMiddleware( + []string{"http://localhost:8080"}, // Allowed origins + []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, // Allowed methods + []string{"Content-Type", "Authorization"}, // Allowed headers + true, + ) a.Router.Use(logMiddleware.Func()) + a.Router.Use(corsMiddleware.Func()) a.initRoutes() } func (a *App) Run(ctx *context.Context, addr string) { a.Server = &http.Server{Addr: addr, Handler: a.Router} - a.MongoCollection, a.MongoClient = db.InitMongo(ctx) a.Logger.Println("Server is ready to handle requests at :8080") if err := a.Server.ListenAndServe(); err != nil && err != http.ErrServerClosed { diff --git a/file-transfer/app/app_test.go b/file-transfer/app/app_test.go new file mode 100644 index 0000000..6b9089f --- /dev/null +++ b/file-transfer/app/app_test.go @@ -0,0 +1,99 @@ +package app + +import ( + "context" + "fmt" + "log" + "net/http" + "net/http/httptest" + "os" + "os/exec" + "testing" + + "file-transfer/db" + + "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" + "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/mongo" + "go.mongodb.org/mongo-driver/v2/mongo/options" +) + +func RunTestApp(t *testing.T) *App { + log.Println("Setting up database...") + cmd := exec.Command( + "docker", "run", + "--rm", "-d", "-p", "27017:27017", + "--name", "testDB", + "-e", "MONGO_INITDB_ROOT_USERNAME=root", "-e", "MONGO_INITDB_ROOT_PASSWORD=example", + "mongo", + ) + _, err := cmd.Output() + if err != nil { + fmt.Println("could not run command: ", err) + } + + ctx := context.Background() + a := &App{} + + a.Router = mux.NewRouter().StrictSlash(true) + a.Logger = log.New(os.Stdout, "server: ", log.Flags()) + + clientOptions := options.Client().ApplyURI("mongodb://root:example@localhost:27017") + a.MongoClient, err = mongo.Connect(clientOptions) + if err != nil { + log.Fatal(err) + } + + err = a.MongoClient.Ping(ctx, nil) + if err != nil { + log.Fatal(err) + } + + log.Println("Connected to MongoDB") + a.MongoCollection = a.MongoClient.Database("files").Collection("Files") + + a.BlobStorage, _ = db.InitLocalBlobStorage("files") + + a.initRoutes() + + a.Server = &http.Server{Addr: ":8080", Handler: a.Router} + + return a +} + +func KillDatabase() { + log.Println("Killing database...") + cmd := exec.Command("docker", "kill", "testDB") + _, err := cmd.Output() + if err != nil { + fmt.Println("could not run command: ", err) + } else { + log.Println("Database killed") + } +} + +func CleanDatabase(t *testing.T, collection *mongo.Collection) { + log.Println("Cleaning database...") + ctx := context.TODO() + _, err := collection.DeleteMany(ctx, bson.M{}) + if err != nil { + t.Error(err) + } +} + +func TestHealthCheck(t *testing.T) { + a := RunTestApp(t) + defer a.Close(context.Background()) + defer KillDatabase() + + t.Run("it should return 200", func(t *testing.T) { + server := httptest.NewServer(a.Router) + resp, err := http.Get(server.URL + "/health") + if err != nil { + t.Error(err) + } + + assert.Equal(t, 200, resp.StatusCode) + }) +} diff --git a/file-transfer/app/cors_middleware.go b/file-transfer/app/cors_middleware.go new file mode 100644 index 0000000..7871e72 --- /dev/null +++ b/file-transfer/app/cors_middleware.go @@ -0,0 +1,85 @@ +package app + +import ( + "net/http" + + "github.com/gorilla/mux" +) + +type CORSMiddleware struct { + allowedOrigins []string + allowedMethods []string + allowedHeaders []string + allowCredentials bool +} + +func NewCORSMiddleware(allowedOrigins, allowedMethods, allowedHeaders []string, allowCredentials bool) *CORSMiddleware { + return &CORSMiddleware{ + allowedOrigins: allowedOrigins, + allowedMethods: allowedMethods, + allowedHeaders: allowedHeaders, + allowCredentials: allowCredentials, + } +} + +func (m *CORSMiddleware) Func() mux.MiddlewareFunc { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", m.getAllowedOrigin(r)) + w.Header().Set("Access-Control-Allow-Methods", m.getAllowedMethods()) + w.Header().Set("Access-Control-Allow-Headers", m.getAllowedHeaders()) + + if m.allowCredentials { + w.Header().Set("Access-Control-Allow-Credentials", "true") + } + + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusOK) + return + } + + next.ServeHTTP(w, r) + }) + } +} + +func (m *CORSMiddleware) getAllowedOrigin(r *http.Request) string { + origin := r.Header.Get("Origin") + if origin == "" { + return "*" + } + + for _, allowedOrigin := range m.allowedOrigins { + if allowedOrigin == "*" || allowedOrigin == origin { + return origin + } + } + + return "" +} + +func (m *CORSMiddleware) getAllowedMethods() string { + if len(m.allowedMethods) == 0 { + return "GET, POST, PUT, DELETE, OPTIONS" + } + return joinStrings(m.allowedMethods) +} + +func (m *CORSMiddleware) getAllowedHeaders() string { + if len(m.allowedHeaders) == 0 { + return "Content-Type, Authorization" + } + return joinStrings(m.allowedHeaders) +} + +func joinStrings(slice []string) string { + result := "" + for i, str := range slice { + if i > 0 { + result += ", " + } + result += str + } + return result +} + diff --git a/file-transfer/app/handle_file.go b/file-transfer/app/handle_file.go index 7e90465..4a39468 100644 --- a/file-transfer/app/handle_file.go +++ b/file-transfer/app/handle_file.go @@ -7,8 +7,9 @@ import ( "file-transfer/db" "file-transfer/models" + "github.com/gorilla/mux" - "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/v2/bson" ) // createFile godoc @@ -19,10 +20,10 @@ import ( // @Accept json // @Produce json // @Param file body models.File true "File object to create" -// @Success 200 {object} models.File "Created file object" +// @Success 201 {object} models.File "Created file object" // @Failure 400 {string} string "Invalid request payload" // @Failure 500 {string} string "Internal server error" -// @Router /files [post] +// @Router /file [post] func (a *App) createFile(w http.ResponseWriter, r *http.Request) { ctx := context.TODO() f := models.File{} @@ -34,12 +35,13 @@ func (a *App) createFile(w http.ResponseWriter, r *http.Request) { } defer r.Body.Close() - if err := db.CreateFile(&ctx, a.MongoCollection, f); err != nil { + file, err := db.CreateFile(&ctx, a.MongoCollection, f) + if err != nil { respondWithError(w, http.StatusInternalServerError, err.Error()) return } - respondWithJSON(w, http.StatusOK, f) + respondWithJSON(w, http.StatusCreated, file) } // getFile godoc @@ -52,11 +54,11 @@ func (a *App) createFile(w http.ResponseWriter, r *http.Request) { // @Success 200 {object} models.File "Retrieved file object" // @Failure 400 {string} string "Invalid file ID" // @Failure 500 {string} string "Internal server error" -// @Router /files/{file_id} [get] +// @Router /file/{file_id} [get] func (a *App) getFile(w http.ResponseWriter, r *http.Request) { ctx := context.TODO() vars := mux.Vars(r) - id, err := primitive.ObjectIDFromHex(vars["file_id"]) + id, err := bson.ObjectIDFromHex(vars["file_id"]) if err != nil { respondWithError(w, http.StatusBadRequest, "Invalid file ID") return @@ -65,6 +67,10 @@ func (a *App) getFile(w http.ResponseWriter, r *http.Request) { f := models.File{FileID: id} f, err = db.GetFile(&ctx, a.MongoCollection, f) if err != nil { + if err.Error() == "mongo: no documents in result" { + respondWithError(w, http.StatusNotFound, "File not found") + return + } respondWithError(w, http.StatusInternalServerError, err.Error()) return } @@ -89,7 +95,39 @@ func (a *App) getAllFiles(w http.ResponseWriter, r *http.Request) { return } - respondWithJSON(w, http.StatusCreated, files) + if len(files) == 0 { + respondWithError(w, http.StatusNoContent, string(json.RawMessage("[]"))) + return + } + respondWithJSON(w, http.StatusOK, files) +} + +// getFilesByUser godoc +// +// @Summary Retrieve files by user +// @Description Retrieve information about all files uploaded by a specific user +// @Tags files +// @Produce json +// @Param user_id path string true "User ID" +// @Success 200 {array} models.File "Files uploaded by the user" +// @Failure 500 {string} string "Internal server error" +// @Router /file/user/{user_id} [get] +func (a *App) getFilesByUser(w http.ResponseWriter, r *http.Request) { + ctx := context.TODO() + vars := mux.Vars(r) + userID := vars["user_id"] + + files, err := db.GetFilesByUserID(&ctx, a.MongoCollection, userID) + if err != nil { + respondWithError(w, http.StatusInternalServerError, err.Error()) + return + } + + if len(files) == 0 { + respondWithError(w, http.StatusNoContent, string(json.RawMessage("[]"))) + return + } + respondWithJSON(w, http.StatusOK, files) } // updateFile godoc @@ -104,17 +142,17 @@ func (a *App) getAllFiles(w http.ResponseWriter, r *http.Request) { // @Success 200 {object} models.File "Updated file object" // @Failure 400 {string} string "Invalid request payload or file ID" // @Failure 500 {string} string "Internal server error" -// @Router /files/{file_id} [put] +// @Router /file/{file_id} [put] func (a *App) updateFile(w http.ResponseWriter, r *http.Request) { ctx := context.TODO() vars := mux.Vars(r) - id, err := primitive.ObjectIDFromHex(vars["file_id"]) + id, err := bson.ObjectIDFromHex(vars["file_id"]) if err != nil { respondWithError(w, http.StatusBadRequest, "Invalid file ID") return } - f := models.File{FileID: id} + f := models.File{} decoder := json.NewDecoder(r.Body) if err := decoder.Decode(&f); err != nil { respondWithError(w, http.StatusBadRequest, "Invalid request payload:"+err.Error()) @@ -122,7 +160,13 @@ func (a *App) updateFile(w http.ResponseWriter, r *http.Request) { } defer r.Body.Close() - if err := db.UpdateFile(&ctx, a.MongoCollection, f); err != nil { + f.FileID = id + f, err = db.UpdateFile(&ctx, a.MongoCollection, f) + if err != nil { + if err.Error() == "mongo: no documents in result" { + respondWithError(w, http.StatusNotFound, "File not found") + return + } respondWithError(w, http.StatusInternalServerError, err.Error()) return } @@ -140,12 +184,16 @@ func (a *App) updateFile(w http.ResponseWriter, r *http.Request) { // @Success 200 {object} map[string]string "Result: success" // @Failure 400 {string} string "Invalid file ID" // @Failure 500 {string} string "Internal server error" -// @Router /files/{file_id} [delete] +// @Router /file/{file_id} [delete] func (a *App) deleteFile(w http.ResponseWriter, r *http.Request) { ctx := context.TODO() vars := mux.Vars(r) - id, err := primitive.ObjectIDFromHex(vars["file_id"]) + id, err := bson.ObjectIDFromHex(vars["file_id"]) if err != nil { + if err.Error() == "mongo: no documents in result" { + respondWithError(w, http.StatusNotFound, "File not found") + return + } respondWithError(w, http.StatusBadRequest, "Invalid file ID") return } diff --git a/file-transfer/app/handle_file_test.go b/file-transfer/app/handle_file_test.go new file mode 100644 index 0000000..e409d7d --- /dev/null +++ b/file-transfer/app/handle_file_test.go @@ -0,0 +1,220 @@ +package app + +import ( + "bytes" + "context" + "encoding/json" + "file-transfer/models" + "fmt" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFileIntegrationTests(t *testing.T) { + a := RunTestApp(t) + defer a.Close(context.Background()) + defer KillDatabase() + + t.Run("it should create and return file", func(t *testing.T) { + defer CleanDatabase(t, a.MongoCollection) + server := httptest.NewServer(a.Server.Handler) + expected := models.File{ + FileName: "test.txt", + UserID: "123", + Path: "path/test.txt", + } + + body, err := json.Marshal(expected) + assert.NoError(t, err) + + reader := bytes.NewReader(body) + resp, err := http.Post(server.URL+"/file", "application/json", reader) + assert.NoError(t, err) + + assert.Equal(t, http.StatusCreated, resp.StatusCode) + + var actual models.File + err = json.NewDecoder(resp.Body).Decode(&actual) + assert.NoError(t, err) + + assert.Equal(t, expected.FileName, actual.FileName) + assert.Equal(t, expected.UserID, actual.UserID) + assert.Equal(t, expected.Path, actual.Path) + }) + + t.Run("it should return all files", func(t *testing.T) { + defer CleanDatabase(t, a.MongoCollection) + server := httptest.NewServer(a.Server.Handler) + file := models.File{ + FileName: "test.txt", + UserID: "123", + Path: "path/test.txt", + } + + // Create files + for i := 0; i < 3; i++ { + file.FileName = fmt.Sprintf("test%d.txt", i) + file.Path = fmt.Sprintf("path/test%d.txt", i) + body, err := json.Marshal(file) + assert.NoError(t, err) + + reader := bytes.NewReader(body) + resp, err := http.Post(server.URL+"/file", "application/json", reader) + assert.NoError(t, err) + + assert.Equal(t, http.StatusCreated, resp.StatusCode) + } + + // Get all files + resp, err := http.Get(server.URL + "/files") + assert.NoError(t, err) + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var actual []models.File + data, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + err = json.Unmarshal(data, &actual) + assert.NoError(t, err) + + assert.Len(t, actual, 3) + }) + + t.Run("it should modify file", func(t *testing.T) { + defer CleanDatabase(t, a.MongoCollection) + server := httptest.NewServer(a.Server.Handler) + initial := models.File{ + FileName: "test.txt", + UserID: "123", + Path: "path/test.txt", + } + expected := models.File{ + FileName: "test.txt", + UserID: "123", + Path: "path/test.txt", + BlobURL: "http://example.com", + } + + // Create file + body, err := json.Marshal(initial) + assert.NoError(t, err) + + reader := bytes.NewReader(body) + resp, err := http.Post(server.URL+"/file", "application/json", reader) + assert.NoError(t, err) + + var actual models.File + err = json.NewDecoder(resp.Body).Decode(&actual) + assert.NoError(t, err) + + // Update file + body, err = json.Marshal(expected) + assert.NoError(t, err) + req, err := http.NewRequest("PUT", server.URL+"/file/"+actual.FileID.Hex(), bytes.NewReader(body)) + assert.NoError(t, err) + + req.Header.Set("Content-Type", "application/json") + resp, err = http.DefaultClient.Do(req) + assert.NoError(t, err) + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + body, err = io.ReadAll(resp.Body) + assert.NoError(t, err) + + err = json.Unmarshal(body, &actual) + assert.NoError(t, err) + + assert.Equal(t, expected.FileName, actual.FileName) + assert.Equal(t, expected.UserID, actual.UserID) + assert.Equal(t, expected.Path, actual.Path) + assert.Equal(t, expected.BlobURL, actual.BlobURL) + }) + + t.Run("it should delete file", func(t *testing.T) { + defer CleanDatabase(t, a.MongoCollection) + server := httptest.NewServer(a.Server.Handler) + file := models.File{ + FileName: "test.txt", + UserID: "123", + Path: "path/test.txt", + } + + // Create file + body, err := json.Marshal(file) + assert.NoError(t, err) + + reader := bytes.NewReader(body) + resp, err := http.Post(server.URL+"/file", "application/json", reader) + assert.NoError(t, err) + + var actual models.File + err = json.NewDecoder(resp.Body).Decode(&actual) + assert.NoError(t, err) + + // Delete file + req, err := http.NewRequest(http.MethodDelete, server.URL+"/file/"+actual.FileID.Hex(), nil) + assert.NoError(t, err) + + resp, err = http.DefaultClient.Do(req) + assert.NoError(t, err) + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + // Get file + resp, err = http.Get(server.URL + "/file/" + actual.FileID.Hex()) + assert.NoError(t, err) + + assert.Equal(t, http.StatusNotFound, resp.StatusCode) + }) + + t.Run("it should return all user files", func(t *testing.T) { + defer CleanDatabase(t, a.MongoCollection) + server := httptest.NewServer(a.Server.Handler) + file := models.File{ + FileName: "test.txt", + UserID: "123", + Path: "path/test.txt", + } + file2 := models.File{ + FileName: "test2.txt", + UserID: "456", + Path: "path/test2.txt", + } + + // Create files + reqBody, err := json.Marshal(file) + assert.NoError(t, err) + + reader := bytes.NewReader(reqBody) + resp, err := http.Post(server.URL+"/file", "application/json", reader) + assert.NoError(t, err) + + assert.Equal(t, http.StatusCreated, resp.StatusCode) + + reqBody, err = json.Marshal(file2) + assert.NoError(t, err) + + reader = bytes.NewReader(reqBody) + resp, err = http.Post(server.URL+"/file", "application/json", reader) + + // Get all files + resp, err = http.Get(server.URL + "/file/user/123") + assert.NoError(t, err) + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + actual := []models.File{} + data, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + + err = json.Unmarshal(data, &actual) + assert.NoError(t, err) + + assert.Len(t, actual, 1) + }) +} diff --git a/file-transfer/app/handle_upload.go b/file-transfer/app/handle_upload.go new file mode 100644 index 0000000..71da211 --- /dev/null +++ b/file-transfer/app/handle_upload.go @@ -0,0 +1,99 @@ +package app + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "path/filepath" + + "file-transfer/models" +) + +// uploadFile godoc +// +// @Summary Upload a file +// @Description Upload file with path and content +// @Tags files +// @Accept multipart/form-data +// @Produce json +// @Param metadata formData string true "JSON metadata with path" +// @Param file formData file true "File content" +// @Success 200 {object} models.FileResponse +// @Failure 400 {string} string "Invalid request" +// @Failure 500 {string} string "Internal server error" +// @Router /upload [post] +func (a *App) uploadFile(w http.ResponseWriter, r *http.Request) { + ctx := context.TODO() + + req, err := models.ParseUploadRequest(r) + if err != nil { + respondWithError(w, http.StatusBadRequest, "Failed to parse request: "+err.Error()) + return + } + defer req.File.Close() + + // Read file content + file, err := io.ReadAll(req.File) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Failed to read file: "+err.Error()) + return + } + + fileData := models.FileData{ + UserID: req.UserID, + Path: req.Path, + Data: file, + } + + // Upload to blob storage + blobURL, err := a.BlobStorage.UploadFile(ctx, fileData) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Failed to upload: "+err.Error()) + return + } + + response := models.FileResponse{ + UserID: req.UserID, + Path: req.Path, + URL: blobURL, + } + + respondWithJSON(w, http.StatusOK, response) +} + +// downloadFile godoc +// +// @Summary Download a file +// @Description Download a file with path +// @Tags files +// @Accept json +// @Produce octet-stream +// @Param file body models.FileDownloadRequest true "File metadata" +// @Success 200 {file} file "File content" +// @Failure 400 {string} string "Invalid request" +// @Failure 500 {string} string "Internal server error" +// @Router /download [post] +func (a *App) downloadFile(w http.ResponseWriter, r *http.Request) { + ctx := context.TODO() + f := models.FileDownloadRequest{} + + decoder := json.NewDecoder(r.Body) + if err := decoder.Decode(&f); err != nil { + respondWithError(w, http.StatusBadRequest, "Invalid request payload: "+err.Error()) + return + } + + // Download from blob storage + file, err := a.BlobStorage.DownloadFile(ctx, f.UserID, f.Path) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Failed to download: "+err.Error()) + return + } + + // Set headers for file download + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filepath.Base(file.Path))) + w.Header().Set("Content-Type", "application/octet-stream") + w.Write(file.Data) +} diff --git a/file-transfer/app/handle_upload_test.go b/file-transfer/app/handle_upload_test.go new file mode 100644 index 0000000..c4fbb90 --- /dev/null +++ b/file-transfer/app/handle_upload_test.go @@ -0,0 +1,70 @@ +package app + +import ( + "bytes" + "context" + "encoding/json" + "io" + "mime/multipart" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "file-transfer/models" + + "github.com/stretchr/testify/assert" +) + +func TestLocalStorage(t *testing.T) { + a := RunTestApp(t) + defer a.Close(context.Background()) + defer KillDatabase() + + t.Run("it should upload and download file", func(t *testing.T) { + server := httptest.NewServer(a.Server.Handler) + expected := models.FileResponse{ + UserID: "123", + Path: "test.txt", + URL: "http://localhost:8080/files/123/test.txt", + Size: 13, + } + + fileData := "Hello, World!" + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + part, err := writer.CreateFormFile("file", "test.txt") + assert.NoError(t, err) + + _, err = io.Copy(part, strings.NewReader(fileData)) + assert.NoError(t, err) + + err = writer.WriteField("metadata", `{"user_id":"123","path":"test.txt"}`) + assert.NoError(t, err) + + writer.Close() + + resp, err := http.Post(server.URL+"/upload", writer.FormDataContentType(), body) + assert.NoError(t, err) + + var actual models.FileResponse + err = json.NewDecoder(resp.Body).Decode(&actual) + assert.NoError(t, err) + + assert.Equal(t, expected.UserID, actual.UserID) + assert.Equal(t, expected.Path, actual.Path) + assert.Equal(t, expected.URL, actual.URL) + + resp, err = http.Post(server.URL+"/download", "application/json", strings.NewReader(`{"user_id":"123","path":"test.txt"}`)) + assert.NoError(t, err) + + respFile, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "attachment; filename=test.txt", resp.Header.Get("Content-Disposition")) + assert.Equal(t, "application/octet-stream", resp.Header.Get("Content-Type")) + assert.Equal(t, fileData, string(respFile)) + }) +} diff --git a/file-transfer/app/routes.go b/file-transfer/app/routes.go index 202302a..fd267c5 100644 --- a/file-transfer/app/routes.go +++ b/file-transfer/app/routes.go @@ -17,9 +17,14 @@ func (a *App) initRoutes() { httpSwagger.DomID("swagger-ui"), )).Methods(http.MethodGet) log.Println("Swagger available at: http://localhost:8080/swagger/index.html") + a.Router.HandleFunc("/file", a.createFile).Methods(http.MethodPost) a.Router.HandleFunc("/files", a.getAllFiles).Methods(http.MethodGet) a.Router.HandleFunc("/file/{file_id}", a.getFile).Methods(http.MethodGet) a.Router.HandleFunc("/file/{file_id}", a.updateFile).Methods(http.MethodPut) a.Router.HandleFunc("/file/{file_id}", a.deleteFile).Methods(http.MethodDelete) + a.Router.HandleFunc("/file/user/{user_id}", a.getFilesByUser).Methods(http.MethodGet) + + a.Router.HandleFunc("/upload", a.uploadFile).Methods(http.MethodPost) + a.Router.HandleFunc("/download", a.downloadFile).Methods(http.MethodPost) } diff --git a/file-transfer/db/blob_storage.go b/file-transfer/db/blob_storage.go new file mode 100644 index 0000000..0bfa5cd --- /dev/null +++ b/file-transfer/db/blob_storage.go @@ -0,0 +1,52 @@ +package db + +import ( + "context" + "fmt" + "net/url" + "os" + + "file-transfer/models" + + "github.com/Azure/azure-storage-blob-go/azblob" + "github.com/joho/godotenv" +) + +type BlobStorage interface { + UploadFile(ctx context.Context, f models.FileData) (string, error) + DownloadFile(ctx context.Context, userID string, path string) (*models.FileData, error) +} + +type LocalBlobStorage struct { + rootPath string +} + +func InitLocalBlobStorage(rootPath string) (*LocalBlobStorage, error) { + return &LocalBlobStorage{ + rootPath: rootPath, + }, nil +} + +type AzureBlobStorage struct { + containerURL azblob.ContainerURL +} + +func InitAzureBlobStorage(containerName string) (*AzureBlobStorage, error) { + _ = godotenv.Load("./.env") + + accountName := os.Getenv("AZURE_STORAGE_ACCOUNT_NAME") + accountKey := os.Getenv("AZURE_STORAGE_ACCOUNT_KEY") + + credential, err := azblob.NewSharedKeyCredential(accountName, accountKey) + if err != nil { + return nil, fmt.Errorf("invalid credentials: %v", err) + } + + pipeline := azblob.NewPipeline(credential, azblob.PipelineOptions{}) + URL, _ := url.Parse(fmt.Sprintf("https://%s.blob.core.windows.net/%s", accountName, containerName)) + containerURL := azblob.NewContainerURL(*URL, pipeline) + + return &AzureBlobStorage{ + containerURL: containerURL, + }, nil +} diff --git a/file-transfer/db/handle_file.go b/file-transfer/db/handle_file.go index 517223c..0bfd627 100644 --- a/file-transfer/db/handle_file.go +++ b/file-transfer/db/handle_file.go @@ -5,22 +5,23 @@ import ( "file-transfer/models" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/mongo" ) -func CreateFile(ctx *context.Context, collection *mongo.Collection, f models.File) error { - _, err := collection.InsertOne(*ctx, f) +func CreateFile(ctx *context.Context, collection *mongo.Collection, f models.File) (models.File, error) { + res, err := collection.InsertOne(*ctx, f) if err != nil { - return err + return f, err } - return nil + f.FileID = res.InsertedID.(bson.ObjectID) + return f, nil } func GetAllFiles(ctx *context.Context, collection *mongo.Collection) ([]models.File, error) { var files []models.File - cursor, err := collection.Find(*ctx, nil) + cursor, err := collection.Find(*ctx, bson.D{}) if err != nil { return files, err } @@ -34,7 +35,10 @@ func GetAllFiles(ctx *context.Context, collection *mongo.Collection) ([]models.F } func GetFile(ctx *context.Context, collection *mongo.Collection, f models.File) (models.File, error) { - err := collection.FindOne(*ctx, f).Decode(f) + filter := bson.M{"_id": f.FileID} + + res := collection.FindOne(*ctx, filter) + err := res.Decode(&f) if err != nil { return f, err } @@ -42,7 +46,24 @@ func GetFile(ctx *context.Context, collection *mongo.Collection, f models.File) return f, nil } -func UpdateFile(ctx *context.Context, collection *mongo.Collection, f models.File) error { +func GetFilesByUserID(ctx *context.Context, collection *mongo.Collection, userID string) ([]models.File, error) { + var files []models.File + filter := bson.M{"userID": userID} + + cursor, err := collection.Find(*ctx, filter) + if err != nil { + return files, err + } + + err = cursor.All(*ctx, &files) + if err != nil { + return files, err + } + + return files, nil +} + +func UpdateFile(ctx *context.Context, collection *mongo.Collection, f models.File) (models.File, error) { filter := bson.M{"_id": f.FileID} update := bson.M{ @@ -50,17 +71,19 @@ func UpdateFile(ctx *context.Context, collection *mongo.Collection, f models.Fil "fileName": f.FileName, "userID": f.UserID, "tags": f.Tags, - "data": f.Data, + "path": f.Path, + "blobURL": f.BlobURL, "hasAccess": f.HasAccess, }, } - err := collection.FindOneAndUpdate(*ctx, filter, update).Decode(f) + res := collection.FindOneAndUpdate(*ctx, filter, update) + err := res.Decode(&f) if err != nil { - return err + return f, err } - return nil + return f, nil } func DeleteFile(ctx *context.Context, collection *mongo.Collection, f models.File) error { diff --git a/file-transfer/db/handle_upload.go b/file-transfer/db/handle_upload.go new file mode 100644 index 0000000..da4fff8 --- /dev/null +++ b/file-transfer/db/handle_upload.go @@ -0,0 +1,85 @@ +package db + +import ( + "bytes" + "context" + "fmt" + "io" + "os" + "path/filepath" + + "file-transfer/models" + + "github.com/Azure/azure-storage-blob-go/azblob" +) + +func (bs *LocalBlobStorage) UploadFile(ctx context.Context, f models.FileData) (string, error) { + path := fmt.Sprintf("%s/%s/%s", bs.rootPath, f.UserID, f.Path) + os.MkdirAll(filepath.Dir(path), os.ModePerm) + file, err := os.Create(path) + if err != nil { + return "", err + } + defer file.Close() + + _, err = file.Write(f.Data) + if err != nil { + return "", err + } + + return fmt.Sprintf("http://localhost:8080/%s", path), nil +} + +func (bs *LocalBlobStorage) DownloadFile(ctx context.Context, userID string, path string) (*models.FileData, error) { + file, err := os.Open(fmt.Sprintf("%s/%s/%s", bs.rootPath, userID, path)) + if err != nil { + return nil, err + } + defer file.Close() + + reader := io.Reader(file) + data, err := io.ReadAll(reader) + if err != nil { + return nil, err + } + + f := &models.FileData{ + Path: path, + Data: data, + } + return f, nil +} + +func (bs *AzureBlobStorage) UploadFile(ctx context.Context, f models.FileData) (string, error) { + reader := bytes.NewReader(f.Data) + + blobURL := bs.containerURL.NewBlockBlobURL(fmt.Sprintf("%s/%s", f.UserID, f.Path)) + _, err := azblob.UploadStreamToBlockBlob(ctx, reader, blobURL, azblob.UploadStreamToBlockBlobOptions{}) + if err != nil { + return "", err + } + + return blobURL.String(), nil +} + +func (bs *AzureBlobStorage) DownloadFile(ctx context.Context, userID string, path string) (*models.FileData, error) { + blobURL := bs.containerURL.NewBlockBlobURL(fmt.Sprintf("%s/%s", userID, path)) + resp, err := blobURL.Download(ctx, 0, 0, azblob.BlobAccessConditions{}, false, azblob.ClientProvidedKeyOptions{}) + if err != nil { + return nil, err + } + + reader := resp.Body(azblob.RetryReaderOptions{}) + defer reader.Close() + + data, err := io.ReadAll(reader) + if err != nil { + return nil, err + } + + f := &models.FileData{ + Path: path, + Data: data, + } + return f, nil +} diff --git a/file-transfer/db/mongo.go b/file-transfer/db/mongo.go index bee77dd..d5e0418 100644 --- a/file-transfer/db/mongo.go +++ b/file-transfer/db/mongo.go @@ -7,15 +7,15 @@ import ( "os" "github.com/joho/godotenv" - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" + "go.mongodb.org/mongo-driver/v2/mongo" + "go.mongodb.org/mongo-driver/v2/mongo/options" ) func InitMongo(ctx *context.Context) (*mongo.Collection, *mongo.Client) { _ = godotenv.Load("./.env") clientOptions := options.Client().ApplyURI(os.Getenv("MONGODB_URI")) - client, err := mongo.Connect(*ctx, clientOptions) + client, err := mongo.Connect(clientOptions) if err != nil { panic(fmt.Sprintf("Mongo DB Connect issue %s", err)) } diff --git a/file-transfer/docs/docs.go b/file-transfer/docs/docs.go index cba68ec..cae68b5 100644 --- a/file-transfer/docs/docs.go +++ b/file-transfer/docs/docs.go @@ -19,24 +19,41 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { - "/files": { - "get": { - "description": "Retrieve information about all existing files", - "produces": [ + "/download": { + "post": { + "description": "Download a file with path", + "consumes": [ "application/json" ], + "produces": [ + "application/octet-stream" + ], "tags": [ "files" ], - "summary": "Retrieve all files", + "summary": "Download a file", + "parameters": [ + { + "description": "File metadata", + "name": "file", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.FileDownloadRequest" + } + } + ], "responses": { "200": { - "description": "Every existing file", + "description": "File content", "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/models.File" - } + "type": "file" + } + }, + "400": { + "description": "Invalid request", + "schema": { + "type": "string" } }, "500": { @@ -46,7 +63,9 @@ const docTemplate = `{ } } } - }, + } + }, + "/file": { "post": { "description": "Create a new file record in the database", "consumes": [ @@ -71,7 +90,7 @@ const docTemplate = `{ } ], "responses": { - "200": { + "201": { "description": "Created file object", "schema": { "$ref": "#/definitions/models.File" @@ -92,7 +111,45 @@ const docTemplate = `{ } } }, - "/files/{file_id}": { + "/file/user/{user_id}": { + "get": { + "description": "Retrieve information about all files uploaded by a specific user", + "produces": [ + "application/json" + ], + "tags": [ + "files" + ], + "summary": "Retrieve files by user", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "user_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Files uploaded by the user", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.File" + } + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "string" + } + } + } + } + }, + "/file/{file_id}": { "get": { "description": "Get information about a file by its ID", "produces": [ @@ -225,17 +282,94 @@ const docTemplate = `{ } } } + }, + "/files": { + "get": { + "description": "Retrieve information about all existing files", + "produces": [ + "application/json" + ], + "tags": [ + "files" + ], + "summary": "Retrieve all files", + "responses": { + "200": { + "description": "Every existing file", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.File" + } + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "string" + } + } + } + } + }, + "/upload": { + "post": { + "description": "Upload file with path and content", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "files" + ], + "summary": "Upload a file", + "parameters": [ + { + "type": "string", + "description": "JSON metadata with path", + "name": "metadata", + "in": "formData", + "required": true + }, + { + "type": "file", + "description": "File content", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.FileResponse" + } + }, + "400": { + "description": "Invalid request", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "string" + } + } + } + } } }, "definitions": { "models.File": { "type": "object", "properties": { - "data": { - "type": "array", - "items": { - "type": "integer" - } + "blob_url": { + "type": "string" }, "file_name": { "type": "string" @@ -250,6 +384,9 @@ const docTemplate = `{ "id": { "type": "string" }, + "path": { + "type": "string" + }, "tags": { "type": "array", "items": { @@ -260,6 +397,34 @@ const docTemplate = `{ "type": "string" } } + }, + "models.FileDownloadRequest": { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "user_id": { + "type": "string" + } + } + }, + "models.FileResponse": { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "url": { + "type": "string" + }, + "user_id": { + "type": "string" + } + } } } }` diff --git a/file-transfer/docs/swagger.json b/file-transfer/docs/swagger.json index f74e00d..ba26f56 100644 --- a/file-transfer/docs/swagger.json +++ b/file-transfer/docs/swagger.json @@ -12,24 +12,41 @@ }, "basePath": "/", "paths": { - "/files": { - "get": { - "description": "Retrieve information about all existing files", - "produces": [ + "/download": { + "post": { + "description": "Download a file with path", + "consumes": [ "application/json" ], + "produces": [ + "application/octet-stream" + ], "tags": [ "files" ], - "summary": "Retrieve all files", + "summary": "Download a file", + "parameters": [ + { + "description": "File metadata", + "name": "file", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.FileDownloadRequest" + } + } + ], "responses": { "200": { - "description": "Every existing file", + "description": "File content", "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/models.File" - } + "type": "file" + } + }, + "400": { + "description": "Invalid request", + "schema": { + "type": "string" } }, "500": { @@ -39,7 +56,9 @@ } } } - }, + } + }, + "/file": { "post": { "description": "Create a new file record in the database", "consumes": [ @@ -64,7 +83,7 @@ } ], "responses": { - "200": { + "201": { "description": "Created file object", "schema": { "$ref": "#/definitions/models.File" @@ -85,7 +104,45 @@ } } }, - "/files/{file_id}": { + "/file/user/{user_id}": { + "get": { + "description": "Retrieve information about all files uploaded by a specific user", + "produces": [ + "application/json" + ], + "tags": [ + "files" + ], + "summary": "Retrieve files by user", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "user_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Files uploaded by the user", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.File" + } + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "string" + } + } + } + } + }, + "/file/{file_id}": { "get": { "description": "Get information about a file by its ID", "produces": [ @@ -218,17 +275,94 @@ } } } + }, + "/files": { + "get": { + "description": "Retrieve information about all existing files", + "produces": [ + "application/json" + ], + "tags": [ + "files" + ], + "summary": "Retrieve all files", + "responses": { + "200": { + "description": "Every existing file", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.File" + } + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "string" + } + } + } + } + }, + "/upload": { + "post": { + "description": "Upload file with path and content", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "files" + ], + "summary": "Upload a file", + "parameters": [ + { + "type": "string", + "description": "JSON metadata with path", + "name": "metadata", + "in": "formData", + "required": true + }, + { + "type": "file", + "description": "File content", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.FileResponse" + } + }, + "400": { + "description": "Invalid request", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "string" + } + } + } + } } }, "definitions": { "models.File": { "type": "object", "properties": { - "data": { - "type": "array", - "items": { - "type": "integer" - } + "blob_url": { + "type": "string" }, "file_name": { "type": "string" @@ -243,6 +377,9 @@ "id": { "type": "string" }, + "path": { + "type": "string" + }, "tags": { "type": "array", "items": { @@ -253,6 +390,34 @@ "type": "string" } } + }, + "models.FileDownloadRequest": { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "user_id": { + "type": "string" + } + } + }, + "models.FileResponse": { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "url": { + "type": "string" + }, + "user_id": { + "type": "string" + } + } } } } \ No newline at end of file diff --git a/file-transfer/docs/swagger.yaml b/file-transfer/docs/swagger.yaml index 6df4995..6f9b27f 100644 --- a/file-transfer/docs/swagger.yaml +++ b/file-transfer/docs/swagger.yaml @@ -2,10 +2,8 @@ basePath: / definitions: models.File: properties: - data: - items: - type: integer - type: array + blob_url: + type: string file_name: type: string has_access: @@ -15,6 +13,8 @@ definitions: type: array id: type: string + path: + type: string tags: items: type: string @@ -22,6 +22,24 @@ definitions: user_id: type: string type: object + models.FileDownloadRequest: + properties: + path: + type: string + user_id: + type: string + type: object + models.FileResponse: + properties: + path: + type: string + size: + type: integer + url: + type: string + user_id: + type: string + type: object info: contact: {} description: Webserver providing saving and retrieval of files from MongoDB @@ -31,25 +49,37 @@ info: title: File transfer API version: "0.2" paths: - /files: - get: - description: Retrieve information about all existing files - produces: + /download: + post: + consumes: - application/json + description: Download a file with path + parameters: + - description: File metadata + in: body + name: file + required: true + schema: + $ref: '#/definitions/models.FileDownloadRequest' + produces: + - application/octet-stream responses: "200": - description: Every existing file + description: File content schema: - items: - $ref: '#/definitions/models.File' - type: array + type: file + "400": + description: Invalid request + schema: + type: string "500": description: Internal server error schema: type: string - summary: Retrieve all files + summary: Download a file tags: - files + /file: post: consumes: - application/json @@ -64,7 +94,7 @@ paths: produces: - application/json responses: - "200": + "201": description: Created file object schema: $ref: '#/definitions/models.File' @@ -79,7 +109,7 @@ paths: summary: Create a new file tags: - files - /files/{file_id}: + /file/{file_id}: delete: description: Remove a file from the database by its ID parameters: @@ -168,4 +198,82 @@ paths: summary: Update an existing file tags: - files + /file/user/{user_id}: + get: + description: Retrieve information about all files uploaded by a specific user + parameters: + - description: User ID + in: path + name: user_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: Files uploaded by the user + schema: + items: + $ref: '#/definitions/models.File' + type: array + "500": + description: Internal server error + schema: + type: string + summary: Retrieve files by user + tags: + - files + /files: + get: + description: Retrieve information about all existing files + produces: + - application/json + responses: + "200": + description: Every existing file + schema: + items: + $ref: '#/definitions/models.File' + type: array + "500": + description: Internal server error + schema: + type: string + summary: Retrieve all files + tags: + - files + /upload: + post: + consumes: + - multipart/form-data + description: Upload file with path and content + parameters: + - description: JSON metadata with path + in: formData + name: metadata + required: true + type: string + - description: File content + in: formData + name: file + required: true + type: file + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.FileResponse' + "400": + description: Invalid request + schema: + type: string + "500": + description: Internal server error + schema: + type: string + summary: Upload a file + tags: + - files swagger: "2.0" diff --git a/file-transfer/go.mod b/file-transfer/go.mod index 5a526ec..d6b44ba 100644 --- a/file-transfer/go.mod +++ b/file-transfer/go.mod @@ -3,34 +3,44 @@ module file-transfer go 1.23 require ( + github.com/Azure/azure-storage-blob-go v0.15.0 + github.com/go-chi/chi/v5 v5.2.0 github.com/gorilla/mux v1.8.1 github.com/joho/godotenv v1.5.1 + github.com/stretchr/testify v1.10.0 github.com/swaggo/http-swagger v1.3.4 github.com/swaggo/swag v1.16.4 go.mongodb.org/mongo-driver v1.17.1 + go.mongodb.org/mongo-driver/v2 v2.0.0 ) require ( + github.com/Azure/azure-pipeline-go v0.2.3 // indirect github.com/KyleBanks/depth v1.2.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/spec v0.20.9 // indirect github.com/go-openapi/swag v0.22.3 // indirect github.com/golang/snappy v0.0.4 // indirect + github.com/google/uuid v1.2.0 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/klauspost/compress v1.13.6 // indirect + github.com/klauspost/compress v1.16.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-ieproxy v0.0.1 // indirect github.com/montanaflynn/stats v0.7.1 // indirect - github.com/stretchr/testify v1.8.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect - golang.org/x/crypto v0.26.0 // indirect + golang.org/x/crypto v0.29.0 // indirect golang.org/x/net v0.25.0 // indirect - golang.org/x/sync v0.8.0 // indirect - golang.org/x/text v0.17.0 // indirect + golang.org/x/sync v0.9.0 // indirect + golang.org/x/sys v0.27.0 // indirect + golang.org/x/text v0.20.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/file-transfer/go.sum b/file-transfer/go.sum index 6113db2..02881d7 100644 --- a/file-transfer/go.sum +++ b/file-transfer/go.sum @@ -1,9 +1,28 @@ +github.com/Azure/azure-pipeline-go v0.2.3 h1:7U9HBg1JFK3jHl5qmo4CTZKFTVgMwdFHMVtCdfBE21U= +github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k= +github.com/Azure/azure-storage-blob-go v0.15.0 h1:rXtgp8tN1p29GvpGgfJetavIG0V7OgcSXPpwp3tx6qk= +github.com/Azure/azure-storage-blob-go v0.15.0/go.mod h1:vbjsVbX0dlxnRc4FFMPsS9BsJWPcne7GB7onqlPvz58= +github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest/adal v0.9.13 h1:Mp5hbtOePIzM8pJVRa3YLrWWmZtoxRXqUEzCfJt3+/Q= +github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= +github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= +github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= +github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= +github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk= +github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0= +github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= @@ -21,14 +40,16 @@ github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= -github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= +github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -41,21 +62,27 @@ github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-ieproxy v0.0.1 h1:qiyop7gCflfhwCzGyeT0gro3sF9AIg9HU98JORTkqfI= +github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E= github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe h1:K8pHPVoTgxFJt1lXuIzzOX7zZhZFldJQK/CgKx9BFIc= github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w= github.com/swaggo/http-swagger v1.3.4 h1:q7t/XLx0n15H1Q9/tk3Y9L4n210XzJF5WtnDX64a5ww= @@ -73,38 +100,50 @@ github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfS github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.mongodb.org/mongo-driver v1.17.1 h1:Wic5cJIwJgSpBhe3lx3+/RybR5PiYRMpVFgO7cOHyIM= go.mongodb.org/mongo-driver v1.17.1/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4= +go.mongodb.org/mongo-driver/v2 v2.0.0 h1:Jfd7XpdZa9yk3eY774bO7SWVb30noLSirL9nKTpavhI= +go.mongodb.org/mongo-driver/v2 v2.0.0/go.mod h1:nSjmNq4JUstE8IRZKTktLgMHM4F1fccL6HGX1yh+8RA= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= +golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191112182307-2180aed22343/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191112214154-59a1497f0cea/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/file-transfer/main.go b/file-transfer/main.go index fcd77c5..1fbc926 100644 --- a/file-transfer/main.go +++ b/file-transfer/main.go @@ -23,13 +23,16 @@ import ( func main() { port := flag.String("port", "8080", "Port to listen on.") flag.Parse() - a := app.App{} - a.Initialize() done := make(chan bool) quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + a := app.App{} + go func() { <-quit @@ -42,9 +45,7 @@ func main() { close(done) }() - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - + a.Initialize(&ctx) a.Run(&ctx, ":"+*port) <-done } diff --git a/file-transfer/main_test.go b/file-transfer/main_test.go index 325eda2..b5d3c9b 100644 --- a/file-transfer/main_test.go +++ b/file-transfer/main_test.go @@ -2,8 +2,6 @@ package main import "testing" -func TestSanity(t *testing.T) { - if false { - t.Fatal("expected true; got false") - } +func TestAll(t *testing.T) { + t.Log("Setting up the router and connecting to the database, nothing to test here") } diff --git a/file-transfer/models/file.go b/file-transfer/models/file.go index dd1d7f8..8d7fb87 100644 --- a/file-transfer/models/file.go +++ b/file-transfer/models/file.go @@ -1,14 +1,15 @@ package models import ( - "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/v2/bson" ) type File struct { - FileID primitive.ObjectID `json:"id,omitempty" bson:"_id,omitempty"` - FileName string `json:"file_name,omitempty" bson:"fileName,omitempty"` - UserID string `json:"user_id,omitempty" bson:"userID,omitempty"` - Tags []string `json:"tags,omitempty" bson:"tags,omitempty"` - Data []byte `json:"data,omitempty" bson:"data,omitempty"` - HasAccess []string `json:"has_access,omitempty" bson:"hasAccess,omitempty"` // List of user IDs + FileID bson.ObjectID `json:"id,omitempty" bson:"_id,omitempty"` + FileName string `json:"file_name,omitempty" bson:"fileName,omitempty"` + UserID string `json:"user_id,omitempty" bson:"userID,omitempty"` + Tags []string `json:"tags,omitempty" bson:"tags,omitempty"` + Path string `json:"path,omitempty" bson:"path,omitempty"` + BlobURL string `json:"blob_url,omitempty" bson:"blobURL,omitempty"` + HasAccess []string `json:"has_access,omitempty" bson:"hasAccess,omitempty"` // List of user IDs } diff --git a/file-transfer/models/file_data.go b/file-transfer/models/file_data.go new file mode 100644 index 0000000..8f5e34f --- /dev/null +++ b/file-transfer/models/file_data.go @@ -0,0 +1,56 @@ +package models + +import ( + "encoding/json" + "fmt" + "mime/multipart" + "net/http" +) + +type FileData struct { + UserID string `json:"user_id"` + Path string `json:"path"` + Data []byte `json:"data"` +} + +type FileUploadRequest struct { + UserID string `json:"user_id"` + Path string `json:"path"` + File multipart.File +} + +type FileDownloadRequest struct { + UserID string `json:"user_id"` + Path string `json:"path"` +} + +type FileResponse struct { + UserID string `json:"user_id"` + Path string `json:"path"` + URL string `json:"url"` + Size int64 `json:"size"` +} + +func ParseUploadRequest(r *http.Request) (*FileUploadRequest, error) { + if err := r.ParseMultipartForm(32 << 20); err != nil { + return nil, err + } + + metadataStr := r.FormValue("metadata") + if metadataStr == "" { + return nil, fmt.Errorf("missing metadata") + } + + var req FileUploadRequest + if err := json.Unmarshal([]byte(metadataStr), &req); err != nil { + return nil, err + } + + file, _, err := r.FormFile("file") + if err != nil { + return nil, err + } + + req.File = file + return &req, nil +} diff --git a/file-transfer/models/main_test.go b/file-transfer/models/main_test.go new file mode 100644 index 0000000..a1a3238 --- /dev/null +++ b/file-transfer/models/main_test.go @@ -0,0 +1,7 @@ +package models + +import "testing" + +func TestAll(t *testing.T) { + t.Log("Just plain data structures, nothing to test here") +}