Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
77f5ddb
added azure blob storage
BrunoSienkiewicz Jan 13, 2025
246ddee
added azure storage package
BrunoSienkiewicz Jan 13, 2025
bc10e24
added file download method and file data model
BrunoSienkiewicz Jan 13, 2025
104e0c0
added local blob storage for testing
BrunoSienkiewicz Jan 15, 2025
c345281
added tests
BrunoSienkiewicz Jan 15, 2025
c488cf2
added file upload to routes
BrunoSienkiewicz Jan 15, 2025
279cee4
split file upload
BrunoSienkiewicz Jan 15, 2025
59939b7
added basic health test
BrunoSienkiewicz Jan 15, 2025
c65f12c
updated file reading
BrunoSienkiewicz Jan 15, 2025
e79c0b3
added simple health check test
BrunoSienkiewicz Jan 19, 2025
c5cd331
updated mongo driver version
BrunoSienkiewicz Jan 19, 2025
6bc37c3
working health check and create file tests
BrunoSienkiewicz Jan 19, 2025
eb643b0
added remaining tests
BrunoSienkiewicz Jan 19, 2025
1f2c5af
added documentation for upload methods
BrunoSienkiewicz Jan 19, 2025
738a39d
added upload tests
BrunoSienkiewicz Jan 19, 2025
de85374
added status code assert
BrunoSienkiewicz Jan 22, 2025
021585f
updated upload tests
BrunoSienkiewicz Jan 22, 2025
824a3d0
added get files by user
BrunoSienkiewicz Jan 22, 2025
d6a0d02
updated endpoints
BrunoSienkiewicz Jan 22, 2025
2ed5f49
added user files test
BrunoSienkiewicz Jan 22, 2025
e2f0081
fixed upload method and status codes
BrunoSienkiewicz Jan 23, 2025
cf3b396
working get method
BrunoSienkiewicz Jan 23, 2025
e1e6d75
added handling files not found
BrunoSienkiewicz Jan 23, 2025
1ce7878
added makefile and fixed paths
Jan 24, 2025
e652af9
working upload tests
Jan 24, 2025
3b1a4f6
added cors
Jan 24, 2025
101820e
tests are passing
Jan 24, 2025
5041062
updated readme with configuration and examples
Jan 24, 2025
c931162
fixed model type
Jan 24, 2025
0d53e1f
updated swagger docs
Jan 24, 2025
b1566b3
Merge remote-tracking branch 'origin/main' into 62-add-file-transfer-…
Jan 24, 2025
841a0c0
fixed typo
Jan 24, 2025
63e4e5a
consistent file endpoints
Jan 24, 2025
7807d76
updated test for new endpoint
Jan 24, 2025
9ff28a3
updated docs
Jan 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions file-transfer/Makefile
Original file line number Diff line number Diff line change
@@ -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 <target>"
@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.

65 changes: 54 additions & 11 deletions file-transfer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,37 +11,80 @@ 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.

### Running MongoDB locally

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`.
27 changes: 24 additions & 3 deletions file-transfer/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
99 changes: 99 additions & 0 deletions file-transfer/app/app_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
85 changes: 85 additions & 0 deletions file-transfer/app/cors_middleware.go
Original file line number Diff line number Diff line change
@@ -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
}

Loading