From 77f5ddb02748ff9299dd0d033b10ce1a6ad84639 Mon Sep 17 00:00:00 2001 From: Bruno Sienkiewicz Date: Mon, 13 Jan 2025 13:27:59 +0100 Subject: [PATCH 01/34] added azure blob storage --- file-transfer/app/app.go | 3 +++ file-transfer/app/routes.go | 1 + file-transfer/db/blob_storage.go | 46 ++++++++++++++++++++++++++++++++ file-transfer/db/handle_file.go | 3 ++- file-transfer/models/file.go | 3 ++- 5 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 file-transfer/db/blob_storage.go diff --git a/file-transfer/app/app.go b/file-transfer/app/app.go index 375f2a4..ca0fa58 100644 --- a/file-transfer/app/app.go +++ b/file-transfer/app/app.go @@ -19,6 +19,7 @@ type App struct { Logger *log.Logger MongoClient *mongo.Client MongoCollection *mongo.Collection + BlobStorage *db.BlobStorage } func (a *App) Initialize() { @@ -33,7 +34,9 @@ func (a *App) Initialize() { 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.BlobStorage, _ = db.InitBlobStorage("files") 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/routes.go b/file-transfer/app/routes.go index 202302a..ecb82b1 100644 --- a/file-transfer/app/routes.go +++ b/file-transfer/app/routes.go @@ -17,6 +17,7 @@ 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) diff --git a/file-transfer/db/blob_storage.go b/file-transfer/db/blob_storage.go new file mode 100644 index 0000000..60866b6 --- /dev/null +++ b/file-transfer/db/blob_storage.go @@ -0,0 +1,46 @@ +package db + +import ( + "context" + "fmt" + "io" + "net/url" + "os" + + "github.com/Azure/azure-storage-blob-go/azblob" + "github.com/joho/godotenv" +) + +type BlobStorage struct { + containerURL azblob.ContainerURL +} + +func InitBlobStorage(containerName string) (*BlobStorage, 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 &BlobStorage{ + containerURL: containerURL, + }, nil +} + +func (bs *BlobStorage) UploadFile(ctx context.Context, filename string, data io.Reader) (string, error) { + blobURL := bs.containerURL.NewBlockBlobURL(filename) + _, err := azblob.UploadStreamToBlockBlob(ctx, data, blobURL, azblob.UploadStreamToBlockBlobOptions{}) + if err != nil { + return "", err + } + + return blobURL.String(), nil +} diff --git a/file-transfer/db/handle_file.go b/file-transfer/db/handle_file.go index 517223c..a9b6394 100644 --- a/file-transfer/db/handle_file.go +++ b/file-transfer/db/handle_file.go @@ -50,7 +50,8 @@ 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, }, } diff --git a/file-transfer/models/file.go b/file-transfer/models/file.go index dd1d7f8..8fd09ff 100644 --- a/file-transfer/models/file.go +++ b/file-transfer/models/file.go @@ -9,6 +9,7 @@ type File struct { 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"` + 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 } From 246ddee27d103ac46809a8327791d323c3c422cd Mon Sep 17 00:00:00 2001 From: Bruno Sienkiewicz Date: Mon, 13 Jan 2025 13:29:04 +0100 Subject: [PATCH 02/34] added azure storage package --- file-transfer/go.mod | 5 +++++ file-transfer/go.sum | 26 ++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/file-transfer/go.mod b/file-transfer/go.mod index 5a526ec..3e876e4 100644 --- a/file-transfer/go.mod +++ b/file-transfer/go.mod @@ -11,15 +11,19 @@ require ( ) require ( + github.com/Azure/azure-pipeline-go v0.2.3 // indirect + github.com/Azure/azure-storage-blob-go v0.15.0 // indirect github.com/KyleBanks/depth v1.2.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/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/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect @@ -30,6 +34,7 @@ require ( golang.org/x/crypto v0.26.0 // indirect golang.org/x/net v0.25.0 // indirect golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.23.0 // indirect golang.org/x/text v0.17.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..5567686 100644 --- a/file-transfer/go.sum +++ b/file-transfer/go.sum @@ -1,9 +1,20 @@ +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/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +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/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/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +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/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= 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,6 +32,8 @@ 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= @@ -41,9 +54,12 @@ 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/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= @@ -74,14 +90,19 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t go.mongodb.org/mongo-driver v1.17.1 h1:Wic5cJIwJgSpBhe3lx3+/RybR5PiYRMpVFgO7cOHyIM= go.mongodb.org/mongo-driver v1.17.1/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4= 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/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= @@ -91,14 +112,19 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.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.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.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= From bc10e24c655dfad903dddd61c5a94aed713ed0dc Mon Sep 17 00:00:00 2001 From: Bruno Sienkiewicz Date: Mon, 13 Jan 2025 14:21:51 +0100 Subject: [PATCH 03/34] added file download method and file data model --- file-transfer/db/blob_storage.go | 23 +++++++++++++++++++---- file-transfer/models/file_data.go | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 file-transfer/models/file_data.go diff --git a/file-transfer/db/blob_storage.go b/file-transfer/db/blob_storage.go index 60866b6..4109b86 100644 --- a/file-transfer/db/blob_storage.go +++ b/file-transfer/db/blob_storage.go @@ -3,10 +3,11 @@ package db import ( "context" "fmt" - "io" "net/url" "os" + "file-transfer/models" + "github.com/Azure/azure-storage-blob-go/azblob" "github.com/joho/godotenv" ) @@ -35,12 +36,26 @@ func InitBlobStorage(containerName string) (*BlobStorage, error) { }, nil } -func (bs *BlobStorage) UploadFile(ctx context.Context, filename string, data io.Reader) (string, error) { - blobURL := bs.containerURL.NewBlockBlobURL(filename) - _, err := azblob.UploadStreamToBlockBlob(ctx, data, blobURL, azblob.UploadStreamToBlockBlobOptions{}) +func (bs *BlobStorage) UploadFile(ctx context.Context, f models.FileData) (string, error) { + blobURL := bs.containerURL.NewBlockBlobURL(f.Path) + _, err := azblob.UploadStreamToBlockBlob(ctx, f.Data, blobURL, azblob.UploadStreamToBlockBlobOptions{}) if err != nil { return "", err } return blobURL.String(), nil } + +func (bs *BlobStorage) DownloadFile(ctx context.Context, path string) (*models.FileData, error) { + blobURL := bs.containerURL.NewBlockBlobURL(path) + resp, err := blobURL.Download(ctx, 0, 0, azblob.BlobAccessConditions{}, false, azblob.ClientProvidedKeyOptions{}) + if err != nil { + return nil, err + } + + f := &models.FileData{ + Path: path, + Data: resp.Body(azblob.RetryReaderOptions{}), + } + return f, nil +} diff --git a/file-transfer/models/file_data.go b/file-transfer/models/file_data.go new file mode 100644 index 0000000..f76228d --- /dev/null +++ b/file-transfer/models/file_data.go @@ -0,0 +1,19 @@ +package models + +import ( + "io" +) + +type FileData struct { + Path string `json:"path"` + Data io.Reader `json:"data"` +} + +type FileDataResponse struct { + Path string `json:"path"` + URL string `json:"url"` +} + +type FileDataRequest struct { + Path string `json:"path"` +} From 104e0c01a63869310d7cacf025489f59e6fb035a Mon Sep 17 00:00:00 2001 From: Bruno Sienkiewicz Date: Wed, 15 Jan 2025 12:11:48 +0100 Subject: [PATCH 04/34] added local blob storage for testing --- file-transfer/app/app.go | 14 ++++++-- file-transfer/db/blob_storage.go | 55 +++++++++++++++++++++++++++++--- 2 files changed, 62 insertions(+), 7 deletions(-) diff --git a/file-transfer/app/app.go b/file-transfer/app/app.go index ca0fa58..6c83a7e 100644 --- a/file-transfer/app/app.go +++ b/file-transfer/app/app.go @@ -10,6 +10,7 @@ import ( "net/http" "github.com/gorilla/mux" + "github.com/joho/godotenv" "go.mongodb.org/mongo-driver/mongo" ) @@ -19,7 +20,7 @@ type App struct { Logger *log.Logger MongoClient *mongo.Client MongoCollection *mongo.Collection - BlobStorage *db.BlobStorage + BlobStorage db.BlobStorage } func (a *App) Initialize() { @@ -36,7 +37,16 @@ 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.BlobStorage, _ = db.InitBlobStorage("files") + + _ = godotenv.Load("./.env") + + storageType := os.Getenv("STORAGE_TYPE") + + if storageType == "local" { + a.BlobStorage, _ = db.InitLocalBlobStorage("files") + } else { + a.BlobStorage, _ = db.InitAzureBlobStorage("files") + } 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/db/blob_storage.go b/file-transfer/db/blob_storage.go index 4109b86..efafa66 100644 --- a/file-transfer/db/blob_storage.go +++ b/file-transfer/db/blob_storage.go @@ -12,11 +12,56 @@ import ( "github.com/joho/godotenv" ) -type BlobStorage struct { +type BlobStorage interface { + UploadFile(ctx context.Context, f models.FileData) (string, error) + DownloadFile(ctx context.Context, path string) (*models.FileData, error) +} + +type LocalBlobStorage struct { + rootPath string +} + +func InitLocalBlobStorage(rootPath string) (*LocalBlobStorage, error) { + return &LocalBlobStorage{ + rootPath: rootPath, + }, nil +} + +func (bs *LocalBlobStorage) UploadFile(ctx context.Context, f models.FileData) (string, error) { + path := fmt.Sprintf("%s/%s", bs.rootPath, f.Path) + file, err := os.Create(path) + if err != nil { + return "", err + } + defer file.Close() + + _, err = file.Write(f.Data) + if err != nil { + return "", err + } + + return path, nil +} + +func (bs *LocalBlobStorage) DownloadFile(ctx context.Context, path string) (*models.FileData, error) { + file, err := os.Open(fmt.Sprintf("%s/%s", bs.rootPath, path)) + if err != nil { + return nil, err + } + defer file.Close() + + f := &models.FileData{ + Path: path, + Data: file, + } + return f, nil +} + +type AzureBlobStorage struct { containerURL azblob.ContainerURL } -func InitBlobStorage(containerName string) (*BlobStorage, error) { +func InitAzureBlobStorage(containerName string) (*AzureBlobStorage, error) { _ = godotenv.Load("./.env") accountName := os.Getenv("AZURE_STORAGE_ACCOUNT_NAME") @@ -31,12 +76,12 @@ func InitBlobStorage(containerName string) (*BlobStorage, error) { URL, _ := url.Parse(fmt.Sprintf("https://%s.blob.core.windows.net/%s", accountName, containerName)) containerURL := azblob.NewContainerURL(*URL, pipeline) - return &BlobStorage{ + return &AzureBlobStorage{ containerURL: containerURL, }, nil } -func (bs *BlobStorage) UploadFile(ctx context.Context, f models.FileData) (string, error) { +func (bs *AzureBlobStorage) UploadFile(ctx context.Context, f models.FileData) (string, error) { blobURL := bs.containerURL.NewBlockBlobURL(f.Path) _, err := azblob.UploadStreamToBlockBlob(ctx, f.Data, blobURL, azblob.UploadStreamToBlockBlobOptions{}) if err != nil { @@ -46,7 +91,7 @@ func (bs *BlobStorage) UploadFile(ctx context.Context, f models.FileData) (strin return blobURL.String(), nil } -func (bs *BlobStorage) DownloadFile(ctx context.Context, path string) (*models.FileData, error) { +func (bs *AzureBlobStorage) DownloadFile(ctx context.Context, path string) (*models.FileData, error) { blobURL := bs.containerURL.NewBlockBlobURL(path) resp, err := blobURL.Download(ctx, 0, 0, azblob.BlobAccessConditions{}, false, azblob.ClientProvidedKeyOptions{}) if err != nil { From c345281c96bd061e9e879480d3f4b223c79f5777 Mon Sep 17 00:00:00 2001 From: Bruno Sienkiewicz Date: Wed, 15 Jan 2025 15:44:44 +0100 Subject: [PATCH 05/34] added tests --- file-transfer/app/handle_file_test.go | 0 file-transfer/app/handle_upload_test.go | 0 file-transfer/main_test.go | 6 ++---- file-transfer/models/main_test.go | 7 +++++++ 4 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 file-transfer/app/handle_file_test.go create mode 100644 file-transfer/app/handle_upload_test.go create mode 100644 file-transfer/models/main_test.go diff --git a/file-transfer/app/handle_file_test.go b/file-transfer/app/handle_file_test.go new file mode 100644 index 0000000..e69de29 diff --git a/file-transfer/app/handle_upload_test.go b/file-transfer/app/handle_upload_test.go new file mode 100644 index 0000000..e69de29 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/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") +} From c488cf27451f2b0b59e7e068ba83d79ba4097ffd Mon Sep 17 00:00:00 2001 From: Bruno Sienkiewicz Date: Wed, 15 Jan 2025 15:46:23 +0100 Subject: [PATCH 06/34] added file upload to routes --- file-transfer/app/handle_file.go | 1 + file-transfer/app/handle_upload.go | 77 ++++++++++++++++++++++++++++++ file-transfer/app/routes.go | 3 ++ file-transfer/models/file_data.go | 8 +--- 4 files changed, 83 insertions(+), 6 deletions(-) create mode 100644 file-transfer/app/handle_upload.go diff --git a/file-transfer/app/handle_file.go b/file-transfer/app/handle_file.go index 7e90465..3222717 100644 --- a/file-transfer/app/handle_file.go +++ b/file-transfer/app/handle_file.go @@ -7,6 +7,7 @@ import ( "file-transfer/db" "file-transfer/models" + "github.com/gorilla/mux" "go.mongodb.org/mongo-driver/bson/primitive" ) diff --git a/file-transfer/app/handle_upload.go b/file-transfer/app/handle_upload.go new file mode 100644 index 0000000..2746706 --- /dev/null +++ b/file-transfer/app/handle_upload.go @@ -0,0 +1,77 @@ +package app + +import ( + "context" + "encoding/json" + "io" + "net/http" + + "file-transfer/models" +) + +func (a *App) uploadFile(w http.ResponseWriter, r *http.Request) { + ctx := context.TODO() + f := models.FileDataRequest{} + + decoder := json.NewDecoder(r.Body) + if err := decoder.Decode(&f); err != nil { + respondWithError(w, http.StatusBadRequest, "Invalid request payload:"+err.Error()) + return + } + + // Parse multipart form with 32MB max memory + if err := r.ParseMultipartForm(32 << 20); err != nil { + respondWithError(w, http.StatusBadRequest, "File too large or invalid form") + return + } + + file, _, err := r.FormFile("file") + if err != nil { + respondWithError(w, http.StatusBadRequest, "Invalid file") + return + } + defer file.Close() + + buffer, err := io.ReadAll(file) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Error reading file") + return + } + + fileData := models.FileData{ + Path: f.Path, + Data: buffer, + } + + URL, err := a.BlobStorage.UploadFile(ctx, fileData) + if err != nil { + respondWithError(w, http.StatusInternalServerError, err.Error()) + return + } + + respondWithJSON(w, http.StatusOK, models.FileDataResponse{ + Path: f.Path, + URL: URL, + }) +} + +func (a *App) downloadFile(w http.ResponseWriter, r *http.Request) { + ctx := context.TODO() + f := models.FileDataRequest{} + + decoder := json.NewDecoder(r.Body) + if err := decoder.Decode(&f); err != nil { + respondWithError(w, http.StatusBadRequest, "Invalid request payload:"+err.Error()) + return + } + + file, err := a.BlobStorage.DownloadFile(ctx, f.Path) + if err != nil { + respondWithError(w, http.StatusInternalServerError, err.Error()) + return + } + + w.Header().Set("Content-Disposition", "attachment; filename="+f.Path) + w.Header().Set("Content-Type", "application/octet-stream") + w.Write(file.Data) +} diff --git a/file-transfer/app/routes.go b/file-transfer/app/routes.go index ecb82b1..4690519 100644 --- a/file-transfer/app/routes.go +++ b/file-transfer/app/routes.go @@ -23,4 +23,7 @@ func (a *App) initRoutes() { 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/upload", a.uploadFile).Methods(http.MethodPost) + a.Router.HandleFunc("file/download", a.downloadFile).Methods(http.MethodGet) } diff --git a/file-transfer/models/file_data.go b/file-transfer/models/file_data.go index f76228d..b18ebad 100644 --- a/file-transfer/models/file_data.go +++ b/file-transfer/models/file_data.go @@ -1,12 +1,8 @@ package models -import ( - "io" -) - type FileData struct { - Path string `json:"path"` - Data io.Reader `json:"data"` + Path string `json:"path"` + Data []byte `json:"data"` } type FileDataResponse struct { From 279cee47e6867d1ad0a6e7b57a1739831859ebae Mon Sep 17 00:00:00 2001 From: Bruno Sienkiewicz Date: Wed, 15 Jan 2025 15:46:54 +0100 Subject: [PATCH 07/34] split file upload --- file-transfer/db/blob_storage.go | 54 -------------------------- file-transfer/db/handle_upload.go | 63 +++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 54 deletions(-) create mode 100644 file-transfer/db/handle_upload.go diff --git a/file-transfer/db/blob_storage.go b/file-transfer/db/blob_storage.go index efafa66..82e55e2 100644 --- a/file-transfer/db/blob_storage.go +++ b/file-transfer/db/blob_storage.go @@ -27,36 +27,6 @@ func InitLocalBlobStorage(rootPath string) (*LocalBlobStorage, error) { }, nil } -func (bs *LocalBlobStorage) UploadFile(ctx context.Context, f models.FileData) (string, error) { - path := fmt.Sprintf("%s/%s", bs.rootPath, f.Path) - file, err := os.Create(path) - if err != nil { - return "", err - } - defer file.Close() - - _, err = file.Write(f.Data) - if err != nil { - return "", err - } - - return path, nil -} - -func (bs *LocalBlobStorage) DownloadFile(ctx context.Context, path string) (*models.FileData, error) { - file, err := os.Open(fmt.Sprintf("%s/%s", bs.rootPath, path)) - if err != nil { - return nil, err - } - defer file.Close() - - f := &models.FileData{ - Path: path, - Data: file, - } - return f, nil -} - type AzureBlobStorage struct { containerURL azblob.ContainerURL } @@ -80,27 +50,3 @@ func InitAzureBlobStorage(containerName string) (*AzureBlobStorage, error) { containerURL: containerURL, }, nil } - -func (bs *AzureBlobStorage) UploadFile(ctx context.Context, f models.FileData) (string, error) { - blobURL := bs.containerURL.NewBlockBlobURL(f.Path) - _, err := azblob.UploadStreamToBlockBlob(ctx, f.Data, blobURL, azblob.UploadStreamToBlockBlobOptions{}) - if err != nil { - return "", err - } - - return blobURL.String(), nil -} - -func (bs *AzureBlobStorage) DownloadFile(ctx context.Context, path string) (*models.FileData, error) { - blobURL := bs.containerURL.NewBlockBlobURL(path) - resp, err := blobURL.Download(ctx, 0, 0, azblob.BlobAccessConditions{}, false, azblob.ClientProvidedKeyOptions{}) - if err != nil { - return nil, err - } - - f := &models.FileData{ - Path: path, - Data: resp.Body(azblob.RetryReaderOptions{}), - } - return f, nil -} diff --git a/file-transfer/db/handle_upload.go b/file-transfer/db/handle_upload.go new file mode 100644 index 0000000..ea2c189 --- /dev/null +++ b/file-transfer/db/handle_upload.go @@ -0,0 +1,63 @@ +package db + +import ( + "context" + "fmt" + "os" + + "file-transfer/models" +) + +func (bs *LocalBlobStorage) UploadFile(ctx context.Context, f models.FileData) (string, error) { + path := fmt.Sprintf("%s/%s", bs.rootPath, f.Path) + file, err := os.Create(path) + if err != nil { + return "", err + } + defer file.Close() + + _, err = file.Write() + if err != nil { + return "", err + } + + return path, nil +} + +func (bs *LocalBlobStorage) DownloadFile(ctx context.Context, path string) (*models.FileData, error) { + file, err := os.Open(fmt.Sprintf("%s/%s", bs.rootPath, path)) + if err != nil { + return nil, err + } + defer file.Close() + + f := &models.FileData{ + Path: path, + Data: file, + } + return f, nil +} + +func (bs *AzureBlobStorage) UploadFile(ctx context.Context, f models.FileData) (string, error) { + blobURL := bs.containerURL.NewBlockBlobURL(f.Path) + _, err := azblob.UploadStreamToBlockBlob(ctx, f.Data, blobURL, azblob.UploadStreamToBlockBlobOptions{}) + if err != nil { + return "", err + } + + return blobURL.String(), nil +} + +func (bs *AzureBlobStorage) DownloadFile(ctx context.Context, path string) (*models.FileData, error) { + blobURL := bs.containerURL.NewBlockBlobURL(path) + resp, err := blobURL.Download(ctx, 0, 0, azblob.BlobAccessConditions{}, false, azblob.ClientProvidedKeyOptions{}) + if err != nil { + return nil, err + } + + f := &models.FileData{ + Path: path, + Data: resp.Body(azblob.RetryReaderOptions{}), + } + return f, nil +} From 59939b7b71c81266bf1efe0d8d3a2a00bdce5959 Mon Sep 17 00:00:00 2001 From: Bruno Sienkiewicz Date: Wed, 15 Jan 2025 16:21:17 +0100 Subject: [PATCH 08/34] added basic health test --- file-transfer/app/app.go | 20 ++++---- file-transfer/app/handle_file_test.go | 27 +++++++++++ file-transfer/app/main_test.go | 70 +++++++++++++++++++++++++++ file-transfer/go.mod | 2 +- file-transfer/go.sum | 2 + file-transfer/main.go | 11 +++-- 6 files changed, 116 insertions(+), 16 deletions(-) create mode 100644 file-transfer/app/main_test.go diff --git a/file-transfer/app/app.go b/file-transfer/app/app.go index 6c83a7e..611c8f2 100644 --- a/file-transfer/app/app.go +++ b/file-transfer/app/app.go @@ -23,19 +23,10 @@ type App struct { 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()) - logMiddleware := NewLogMiddleware(a.Logger) - a.Router.Use(logMiddleware.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) _ = godotenv.Load("./.env") @@ -48,6 +39,15 @@ func (a *App) Run(ctx *context.Context, addr string) { a.BlobStorage, _ = db.InitAzureBlobStorage("files") } + logMiddleware := NewLogMiddleware(a.Logger) + a.Router.Use(logMiddleware.Func()) + + a.initRoutes() +} + +func (a *App) Run(ctx *context.Context, addr string) { + a.Server = &http.Server{Addr: addr, Handler: a.Router} + a.Logger.Println("Server is ready to handle requests at :8080") if err := a.Server.ListenAndServe(); err != nil && err != http.ErrServerClosed { a.Logger.Fatalf("Could not listen on :8080: %v\n", err) diff --git a/file-transfer/app/handle_file_test.go b/file-transfer/app/handle_file_test.go index e69de29..836aee3 100644 --- a/file-transfer/app/handle_file_test.go +++ b/file-transfer/app/handle_file_test.go @@ -0,0 +1,27 @@ +package app + +import ( + "file-transfer/db" + "file-transfer/models" + "testing" + + "github.com/gorilla/mux" + "github.com/joho/godotenv" + "github.com/stretchr/testify/mock" + "go.mongodb.org/mongo-driver/mongo" +) + +func TestFileIntegrationTests(t *testing.T) { + ts := RunTestApp(t) + defer ts.Close() + + t.Run("it should return 200 when health is ok", func(t *testing.T) { + resp, err := http.Get("localhost:8080/health") + + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + assert.Equal(t, 200, resp.StatusCode) + }) +} diff --git a/file-transfer/app/main_test.go b/file-transfer/app/main_test.go new file mode 100644 index 0000000..9e44899 --- /dev/null +++ b/file-transfer/app/main_test.go @@ -0,0 +1,70 @@ +package app + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "github.com/go-chi/chi/v5" + "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/mongo" + "go.mongodb.org/mongo-driver/v2/mongo/options" + "log" + "net/http" + "net/http/httptest" + "os" + "os/exec" + "testing" + "time" +) + +func runTestApp(t *testing.T) *App { + a := &App{} + + a.Router = mux.NewRouter().StrictSlash(true) + a.Logger = log.New(os.Stdout, "server: ", log.Flags()) + + a.MongoCollection, a.MongoClient = db.InitMongo(ctx) + + a.BlobStorage, _ = db.InitLocalBlobStorage("files") + + a.initRoutes() +} + +func SetupDatabase() { + log.Println("Setting up database...") + cmd := exec.Command("docker", "run", "--rm", "-d", "-p", "27017:27017", "--name", "testDB", "mongo") + _, err := cmd.Output() + if err != nil { + fmt.Println("could not run command: ", err) + } + db.Client, err = mongo.Connect(options.Client().ApplyURI("mongodb://localhost:27017")) + if err != nil { + log.Fatalf("Failed to connect to MongoDB: %v", err) + } else { + log.Println("Database set up successfully") + } +} + +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() { + coll := db.GetCollection("files") + _, _ = coll.DeleteMany(context.Background(), bson.D{}) +} + +func TestMain(m *testing.M) { + SetupDatabase() + code := m.Run() + KillDatabase() + os.Exit(code) +} diff --git a/file-transfer/go.mod b/file-transfer/go.mod index 3e876e4..9c39cff 100644 --- a/file-transfer/go.mod +++ b/file-transfer/go.mod @@ -25,7 +25,7 @@ require ( 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/stretchr/testify v1.10.0 // 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 diff --git a/file-transfer/go.sum b/file-transfer/go.sum index 5567686..c402234 100644 --- a/file-transfer/go.sum +++ b/file-transfer/go.sum @@ -72,6 +72,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO 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= diff --git a/file-transfer/main.go b/file-transfer/main.go index fcd77c5..3ba6d03 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(), 10*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 } From c65f12c93c0c0ce7aca8b4b254622a956bb93bf5 Mon Sep 17 00:00:00 2001 From: Bruno Sienkiewicz Date: Wed, 15 Jan 2025 16:34:17 +0100 Subject: [PATCH 09/34] updated file reading --- file-transfer/app/handle_upload_test.go | 1 + file-transfer/db/handle_upload.go | 28 +++++++++++++++++++++---- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/file-transfer/app/handle_upload_test.go b/file-transfer/app/handle_upload_test.go index e69de29..4879f7a 100644 --- a/file-transfer/app/handle_upload_test.go +++ b/file-transfer/app/handle_upload_test.go @@ -0,0 +1 @@ +package app diff --git a/file-transfer/db/handle_upload.go b/file-transfer/db/handle_upload.go index ea2c189..fcbdbd0 100644 --- a/file-transfer/db/handle_upload.go +++ b/file-transfer/db/handle_upload.go @@ -1,11 +1,15 @@ package db import ( + "bytes" "context" "fmt" + "io" "os" "file-transfer/models" + + "github.com/Azure/azure-storage-blob-go/azblob" ) func (bs *LocalBlobStorage) UploadFile(ctx context.Context, f models.FileData) (string, error) { @@ -16,7 +20,7 @@ func (bs *LocalBlobStorage) UploadFile(ctx context.Context, f models.FileData) ( } defer file.Close() - _, err = file.Write() + _, err = file.Write(f.Data) if err != nil { return "", err } @@ -31,16 +35,24 @@ func (bs *LocalBlobStorage) DownloadFile(ctx context.Context, path string) (*mod } defer file.Close() + reader := io.Reader(file) + data, err := io.ReadAll(reader) + if err != nil { + return nil, err + } + f := &models.FileData{ Path: path, - Data: file, + 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(f.Path) - _, err := azblob.UploadStreamToBlockBlob(ctx, f.Data, blobURL, azblob.UploadStreamToBlockBlobOptions{}) + _, err := azblob.UploadStreamToBlockBlob(ctx, reader, blobURL, azblob.UploadStreamToBlockBlobOptions{}) if err != nil { return "", err } @@ -55,9 +67,17 @@ func (bs *AzureBlobStorage) DownloadFile(ctx context.Context, path string) (*mod 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: resp.Body(azblob.RetryReaderOptions{}), + Data: data, } return f, nil } From e79c0b3a99f7cec5e2333fb0dccb5bd6ddf92e2b Mon Sep 17 00:00:00 2001 From: Bruno Sienkiewicz Date: Sun, 19 Jan 2025 14:02:43 +0100 Subject: [PATCH 10/34] added simple health check test --- file-transfer/app/main_test.go | 47 ++++++++++++++++++++-------------- file-transfer/go.mod | 19 +++++++++----- file-transfer/go.sum | 35 ++++++++++++++++--------- 3 files changed, 63 insertions(+), 38 deletions(-) diff --git a/file-transfer/app/main_test.go b/file-transfer/app/main_test.go index 9e44899..684c4b0 100644 --- a/file-transfer/app/main_test.go +++ b/file-transfer/app/main_test.go @@ -1,34 +1,35 @@ package app import ( - "bytes" "context" - "encoding/json" "fmt" - "github.com/go-chi/chi/v5" - "go.mongodb.org/mongo-driver/v2/bson" - "go.mongodb.org/mongo-driver/v2/mongo" - "go.mongodb.org/mongo-driver/v2/mongo/options" "log" "net/http" - "net/http/httptest" "os" "os/exec" "testing" - "time" + + "file-transfer/db" + + "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" + "go.mongodb.org/mongo-driver/v2/bson" ) -func runTestApp(t *testing.T) *App { +func RunTestApp(t *testing.T) *App { + ctx := context.Background() a := &App{} a.Router = mux.NewRouter().StrictSlash(true) a.Logger = log.New(os.Stdout, "server: ", log.Flags()) - a.MongoCollection, a.MongoClient = db.InitMongo(ctx) + a.MongoCollection, a.MongoClient = db.InitMongo(&ctx) a.BlobStorage, _ = db.InitLocalBlobStorage("files") a.initRoutes() + + return a } func SetupDatabase() { @@ -38,12 +39,6 @@ func SetupDatabase() { if err != nil { fmt.Println("could not run command: ", err) } - db.Client, err = mongo.Connect(options.Client().ApplyURI("mongodb://localhost:27017")) - if err != nil { - log.Fatalf("Failed to connect to MongoDB: %v", err) - } else { - log.Println("Database set up successfully") - } } func KillDatabase() { @@ -57,9 +52,23 @@ func KillDatabase() { } } -func CleanDatabase() { - coll := db.GetCollection("files") - _, _ = coll.DeleteMany(context.Background(), bson.D{}) +func CleanDatabase(a *App) { + _, _ = a.MongoCollection.DeleteMany(context.Background(), bson.D{}) +} + +func TestHealthCheck(t *testing.T) { + a := RunTestApp(t) + defer a.Close(context.Background()) + + t.Run("it should return 200 OK", func(t *testing.T) { + resp, err := http.Get("http://localhost:8080/health") + + if err != nil { + t.Fatalf("Could not send GET request: %v", err) + } + + assert.Equal(t, 200, resp.StatusCode) + }) } func TestMain(m *testing.M) { diff --git a/file-transfer/go.mod b/file-transfer/go.mod index 9c39cff..d6b44ba 100644 --- a/file-transfer/go.mod +++ b/file-transfer/go.mod @@ -3,17 +3,21 @@ 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/Azure/azure-storage-blob-go v0.15.0 // 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 @@ -21,21 +25,22 @@ require ( 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.10.0 // 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/sys v0.23.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 c402234..02881d7 100644 --- a/file-transfer/go.sum +++ b/file-transfer/go.sum @@ -2,11 +2,16 @@ github.com/Azure/azure-pipeline-go v0.2.3 h1:7U9HBg1JFK3jHl5qmo4CTZKFTVgMwdFHMVt 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= @@ -14,7 +19,10 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 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= @@ -40,8 +48,8 @@ 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= @@ -59,19 +67,20 @@ github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/ 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= @@ -91,12 +100,14 @@ 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= @@ -111,8 +122,8 @@ 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= @@ -121,8 +132,8 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w 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.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= -golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +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= @@ -131,8 +142,8 @@ 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= From c5cd331eb785140ffa96c442601bff64ac70cbaa Mon Sep 17 00:00:00 2001 From: Bruno Sienkiewicz Date: Sun, 19 Jan 2025 14:44:27 +0100 Subject: [PATCH 11/34] updated mongo driver version --- file-transfer/app/app.go | 2 +- .../app/{main_test.go => app_test.go} | 42 ++++++------- file-transfer/app/handle_file_test.go | 62 +++++++++++++++---- file-transfer/db/handle_file.go | 4 +- file-transfer/db/mongo.go | 6 +- 5 files changed, 76 insertions(+), 40 deletions(-) rename file-transfer/app/{main_test.go => app_test.go} (73%) diff --git a/file-transfer/app/app.go b/file-transfer/app/app.go index 611c8f2..0d8c59f 100644 --- a/file-transfer/app/app.go +++ b/file-transfer/app/app.go @@ -11,7 +11,7 @@ import ( "github.com/gorilla/mux" "github.com/joho/godotenv" - "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/v2/mongo" ) type App struct { diff --git a/file-transfer/app/main_test.go b/file-transfer/app/app_test.go similarity index 73% rename from file-transfer/app/main_test.go rename to file-transfer/app/app_test.go index 684c4b0..f70f1e3 100644 --- a/file-transfer/app/main_test.go +++ b/file-transfer/app/app_test.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "net/http" + "net/http/httptest" "os" "os/exec" "testing" @@ -14,9 +15,17 @@ import ( "github.com/gorilla/mux" "github.com/stretchr/testify/assert" "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/mongo" ) func RunTestApp(t *testing.T) *App { + log.Println("Setting up database...") + cmd := exec.Command("docker", "run", "--rm", "-d", "-p", "27017:27017", "--name", "testDB", "mongo") + _, err := cmd.Output() + if err != nil { + fmt.Println("could not run command: ", err) + } + ctx := context.Background() a := &App{} @@ -32,15 +41,6 @@ func RunTestApp(t *testing.T) *App { return a } -func SetupDatabase() { - log.Println("Setting up database...") - cmd := exec.Command("docker", "run", "--rm", "-d", "-p", "27017:27017", "--name", "testDB", "mongo") - _, err := cmd.Output() - if err != nil { - fmt.Println("could not run command: ", err) - } -} - func KillDatabase() { log.Println("Killing database...") cmd := exec.Command("docker", "kill", "testDB") @@ -52,28 +52,26 @@ func KillDatabase() { } } -func CleanDatabase(a *App) { - _, _ = a.MongoCollection.DeleteMany(context.Background(), bson.D{}) +func CleanDatabase(t *testing.T, collection *mongo.Collection) { + 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 OK", func(t *testing.T) { - resp, err := http.Get("http://localhost:8080/health") - + t.Run("it should return 200", func(t *testing.T) { + server := httptest.NewServer(a.Server.Handler) + resp, err := http.Get(server.URL + "/health") if err != nil { - t.Fatalf("Could not send GET request: %v", err) + t.Error(err) } assert.Equal(t, 200, resp.StatusCode) }) } - -func TestMain(m *testing.M) { - SetupDatabase() - code := m.Run() - KillDatabase() - os.Exit(code) -} diff --git a/file-transfer/app/handle_file_test.go b/file-transfer/app/handle_file_test.go index 836aee3..3417fb7 100644 --- a/file-transfer/app/handle_file_test.go +++ b/file-transfer/app/handle_file_test.go @@ -1,27 +1,65 @@ package app import ( - "file-transfer/db" + "bytes" + "context" + "encoding/json" "file-transfer/models" + "net/http" + "net/http/httptest" "testing" - "github.com/gorilla/mux" - "github.com/joho/godotenv" - "github.com/stretchr/testify/mock" - "go.mongodb.org/mongo-driver/mongo" + "github.com/stretchr/testify/assert" ) func TestFileIntegrationTests(t *testing.T) { - ts := RunTestApp(t) - defer ts.Close() + a := RunTestApp(t) + defer a.Close(context.Background()) + defer KillDatabase() - t.Run("it should return 200 when health is ok", func(t *testing.T) { - resp, err := http.Get("localhost:8080/health") + t.Run("it should create and return file", func(t *testing.T) { + server := httptest.NewServer(a.Server.Handler) + expected := models.File{ + FileName: "test.txt", + UserID: "123", + Path: "path/test.txt", + } + + // Create file + 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) + + var actual models.File + err = json.NewDecoder(resp.Body).Decode(&actual) + assert.NoError(t, err) - if err != nil { - t.Fatalf("Expected no error, got %v", 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 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", } - assert.Equal(t, 200, resp.StatusCode) + // Create file + a.MongoCollection.InsertOne(context.TODO(), expected) + + // Get file + resp, err := http.Get(server.URL + "/file/" + expected.FileID.Hex()) + assert.NoError(t, err) + + var actual models.File + err = json.NewDecoder(resp.Body).Decode(&actual) + assert.NoError(t, err) }) } diff --git a/file-transfer/db/handle_file.go b/file-transfer/db/handle_file.go index a9b6394..e3914bc 100644 --- a/file-transfer/db/handle_file.go +++ b/file-transfer/db/handle_file.go @@ -5,8 +5,8 @@ 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 { 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)) } From 6bc37c34eeaf021865a137c16d54e823bb91c376 Mon Sep 17 00:00:00 2001 From: Bruno Sienkiewicz Date: Sun, 19 Jan 2025 15:04:40 +0100 Subject: [PATCH 12/34] working health check and create file tests --- file-transfer/app/app_test.go | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/file-transfer/app/app_test.go b/file-transfer/app/app_test.go index f70f1e3..72a9d21 100644 --- a/file-transfer/app/app_test.go +++ b/file-transfer/app/app_test.go @@ -16,11 +16,18 @@ import ( "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", "mongo") + 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) @@ -32,12 +39,26 @@ func RunTestApp(t *testing.T) *App { a.Router = mux.NewRouter().StrictSlash(true) a.Logger = log.New(os.Stdout, "server: ", log.Flags()) - a.MongoCollection, a.MongoClient = db.InitMongo(&ctx) + 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 } @@ -66,7 +87,7 @@ func TestHealthCheck(t *testing.T) { defer KillDatabase() t.Run("it should return 200", func(t *testing.T) { - server := httptest.NewServer(a.Server.Handler) + server := httptest.NewServer(a.Router) resp, err := http.Get(server.URL + "/health") if err != nil { t.Error(err) From eb643b0d4219ccbf07b8416dd1db0e3a9491a976 Mon Sep 17 00:00:00 2001 From: Bruno Sienkiewicz Date: Sun, 19 Jan 2025 15:18:05 +0100 Subject: [PATCH 13/34] added remaining tests --- file-transfer/app/app_test.go | 1 + file-transfer/app/handle_file_test.go | 61 ++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/file-transfer/app/app_test.go b/file-transfer/app/app_test.go index 72a9d21..6b9089f 100644 --- a/file-transfer/app/app_test.go +++ b/file-transfer/app/app_test.go @@ -74,6 +74,7 @@ func KillDatabase() { } func CleanDatabase(t *testing.T, collection *mongo.Collection) { + log.Println("Cleaning database...") ctx := context.TODO() _, err := collection.DeleteMany(ctx, bson.M{}) if err != nil { diff --git a/file-transfer/app/handle_file_test.go b/file-transfer/app/handle_file_test.go index 3417fb7..27b53f6 100644 --- a/file-transfer/app/handle_file_test.go +++ b/file-transfer/app/handle_file_test.go @@ -25,7 +25,6 @@ func TestFileIntegrationTests(t *testing.T) { Path: "path/test.txt", } - // Create file body, err := json.Marshal(expected) assert.NoError(t, err) @@ -62,4 +61,64 @@ func TestFileIntegrationTests(t *testing.T) { err = json.NewDecoder(resp.Body).Decode(&actual) assert.NoError(t, err) }) + + t.Run("it should return all files", 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", + } + + // Create file + a.MongoCollection.InsertOne(context.TODO(), expected) + a.MongoCollection.InsertOne(context.TODO(), expected) + + // Get all files + resp, err := http.Get(server.URL + "/files") + assert.NoError(t, err) + + var actual []models.File + err = json.NewDecoder(resp.Body).Decode(&actual) + assert.NoError(t, err) + }) + + 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: "456", + Path: "path/test.txt", + BlobURL: "http://example.com", + } + + // Create file + a.MongoCollection.InsertOne(context.TODO(), initial) + + // Update file + body, err := json.Marshal(expected) + assert.NoError(t, err) + resp, err := http.Post(server.URL+"/file/"+initial.FileID.Hex(), "application/json", bytes.NewReader(body)) + assert.NoError(t, err) + + // Get file + resp, err = http.Get(server.URL + "/file/" + initial.FileID.Hex()) + assert.NoError(t, err) + + 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) + assert.Equal(t, expected.BlobURL, actual.BlobURL) + }) } From 1f2c5afec3c06671a5690f7ed0259a921dc65bb7 Mon Sep 17 00:00:00 2001 From: Bruno Sienkiewicz Date: Sun, 19 Jan 2025 15:21:06 +0100 Subject: [PATCH 14/34] added documentation for upload methods --- file-transfer/app/handle_upload.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/file-transfer/app/handle_upload.go b/file-transfer/app/handle_upload.go index 2746706..8f346b8 100644 --- a/file-transfer/app/handle_upload.go +++ b/file-transfer/app/handle_upload.go @@ -9,6 +9,19 @@ import ( "file-transfer/models" ) +// uploadFile godoc +// +// @Summary Upload a file +// @Description Upload a file to the server +// @Tags files +// @Accept json +// @Produce json +// @Param file formData file true "File to upload" +// @Param path body string true "Path to store the file" +// @Success 200 {object} models.FileDataResponse "Uploaded file object" +// @Failure 400 {string} string "Invalid request payload" +// @Failure 500 {string} string "Internal server error" +// @Router /files/upload [post] func (a *App) uploadFile(w http.ResponseWriter, r *http.Request) { ctx := context.TODO() f := models.FileDataRequest{} @@ -55,6 +68,18 @@ func (a *App) uploadFile(w http.ResponseWriter, r *http.Request) { }) } +// downloadFile godoc +// +// @Summary Download a file +// @Description Download a file from the server +// @Tags files +// @Accept json +// @Produce json +// @Param path body string true "Path to the file" +// @Success 200 {object} models.FileDataResponse "Downloaded file object" +// @Failure 400 {string} string "Invalid request payload" +// @Failure 500 {string} string "Internal server error" +// @Router /files/download [post] func (a *App) downloadFile(w http.ResponseWriter, r *http.Request) { ctx := context.TODO() f := models.FileDataRequest{} From 738a39d0e6b7e358120b4f4b9d8d2359934897bf Mon Sep 17 00:00:00 2001 From: Bruno Sienkiewicz Date: Sun, 19 Jan 2025 18:51:52 +0100 Subject: [PATCH 15/34] added upload tests --- file-transfer/app/handle_file_test.go | 16 +++- file-transfer/app/handle_upload_test.go | 97 +++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 4 deletions(-) diff --git a/file-transfer/app/handle_file_test.go b/file-transfer/app/handle_file_test.go index 27b53f6..058a681 100644 --- a/file-transfer/app/handle_file_test.go +++ b/file-transfer/app/handle_file_test.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "file-transfer/models" + "io" "net/http" "net/http/httptest" "testing" @@ -80,7 +81,9 @@ func TestFileIntegrationTests(t *testing.T) { assert.NoError(t, err) var actual []models.File - err = json.NewDecoder(resp.Body).Decode(&actual) + data, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + err = json.Unmarshal(data, &actual) assert.NoError(t, err) }) @@ -100,12 +103,17 @@ func TestFileIntegrationTests(t *testing.T) { } // Create file - a.MongoCollection.InsertOne(context.TODO(), initial) + 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) // Update file - body, err := json.Marshal(expected) + body, err = json.Marshal(expected) assert.NoError(t, err) - resp, err := http.Post(server.URL+"/file/"+initial.FileID.Hex(), "application/json", bytes.NewReader(body)) + resp, err = http.Post(server.URL+"/file/"+initial.FileID.Hex(), "application/json", bytes.NewReader(body)) assert.NoError(t, err) // Get file diff --git a/file-transfer/app/handle_upload_test.go b/file-transfer/app/handle_upload_test.go index 4879f7a..0f3e00d 100644 --- a/file-transfer/app/handle_upload_test.go +++ b/file-transfer/app/handle_upload_test.go @@ -1 +1,98 @@ 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 TestUploadLocalStorage(t *testing.T) { + a := RunTestApp(t) + defer a.Close(context.Background()) + defer KillDatabase() + + t.Run("it should upload and return file", func(t *testing.T) { + server := httptest.NewServer(a.Server.Handler) + expected := models.FileDataRequest{ + Path: "test.txt", + } + + 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("path", expected.Path) + assert.NoError(t, err) + + writer.Close() + + resp, err := http.Post(server.URL+"/files/upload", writer.FormDataContentType(), body) + assert.NoError(t, err) + + var actual models.FileDataResponse + err = json.NewDecoder(resp.Body).Decode(&actual) + assert.NoError(t, err) + + assert.Equal(t, expected.Path, actual.Path) + }) +} + +func TestDownloadLocalStorage(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.FileDataRequest{ + Path: "test.txt", + } + + 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("path", expected.Path) + assert.NoError(t, err) + + writer.Close() + + resp, err := http.Post(server.URL+"/files/upload", writer.FormDataContentType(), body) + assert.NoError(t, err) + + var actual models.FileDataResponse + err = json.NewDecoder(resp.Body).Decode(&actual) + + resp, err = http.Post(server.URL+"/files/download", "application/json", strings.NewReader(`{"path":"test.txt"}`)) + assert.NoError(t, err) + + var actualDownload models.FileDataResponse + err = json.NewDecoder(resp.Body).Decode(&actualDownload) + assert.NoError(t, err) + + assert.Equal(t, expected.Path, actualDownload.Path) + assert.Equal(t, actual.URL, actualDownload.URL) + }) +} From de85374c93217407cb384385211f7e61108baddf Mon Sep 17 00:00:00 2001 From: Bruno Sienkiewicz Date: Wed, 22 Jan 2025 12:33:16 +0100 Subject: [PATCH 16/34] added status code assert --- file-transfer/app/handle_file_test.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/file-transfer/app/handle_file_test.go b/file-transfer/app/handle_file_test.go index 058a681..0a79138 100644 --- a/file-transfer/app/handle_file_test.go +++ b/file-transfer/app/handle_file_test.go @@ -33,6 +33,8 @@ func TestFileIntegrationTests(t *testing.T) { resp, err := http.Post(server.URL+"/file", "application/json", reader) assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + var actual models.File err = json.NewDecoder(resp.Body).Decode(&actual) assert.NoError(t, err) @@ -58,6 +60,8 @@ func TestFileIntegrationTests(t *testing.T) { resp, err := http.Get(server.URL + "/file/" + expected.FileID.Hex()) assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + var actual models.File err = json.NewDecoder(resp.Body).Decode(&actual) assert.NoError(t, err) @@ -80,6 +84,8 @@ func TestFileIntegrationTests(t *testing.T) { 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) @@ -120,6 +126,8 @@ func TestFileIntegrationTests(t *testing.T) { resp, err = http.Get(server.URL + "/file/" + initial.FileID.Hex()) assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + var actual models.File err = json.NewDecoder(resp.Body).Decode(&actual) assert.NoError(t, err) From 021585f0ce8b4f005b98cf63b3dfe8886d0c9b63 Mon Sep 17 00:00:00 2001 From: Bruno Sienkiewicz Date: Wed, 22 Jan 2025 13:21:12 +0100 Subject: [PATCH 17/34] updated upload tests --- file-transfer/app/handle_file_test.go | 48 +++++------ file-transfer/app/handle_upload.go | 101 ++++++++++++------------ file-transfer/app/handle_upload_test.go | 65 ++++----------- file-transfer/db/blob_storage.go | 2 +- file-transfer/db/handle_upload.go | 12 +-- file-transfer/models/file_data.go | 55 +++++++++++-- 6 files changed, 141 insertions(+), 142 deletions(-) diff --git a/file-transfer/app/handle_file_test.go b/file-transfer/app/handle_file_test.go index 0a79138..4b1b3fb 100644 --- a/file-transfer/app/handle_file_test.go +++ b/file-transfer/app/handle_file_test.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "file-transfer/models" + "fmt" "io" "net/http" "net/http/httptest" @@ -44,42 +45,28 @@ func TestFileIntegrationTests(t *testing.T) { assert.Equal(t, expected.Path, actual.Path) }) - t.Run("it should return file", func(t *testing.T) { + t.Run("it should return all files", func(t *testing.T) { defer CleanDatabase(t, a.MongoCollection) server := httptest.NewServer(a.Server.Handler) - expected := models.File{ + file := models.File{ FileName: "test.txt", UserID: "123", Path: "path/test.txt", } - // Create file - a.MongoCollection.InsertOne(context.TODO(), expected) - - // Get file - resp, err := http.Get(server.URL + "/file/" + expected.FileID.Hex()) - assert.NoError(t, err) - - assert.Equal(t, http.StatusOK, resp.StatusCode) + // Create files + for i := 0; i < 3; i++ { + file.FileName = fmt.Sprintf("test%d.txt", i) + body, err := json.Marshal(file) + assert.NoError(t, err) - var actual models.File - err = json.NewDecoder(resp.Body).Decode(&actual) - assert.NoError(t, err) - }) + reader := bytes.NewReader(body) + resp, err := http.Post(server.URL+"/file", "application/json", reader) + assert.NoError(t, err) - t.Run("it should return all files", 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", + assert.Equal(t, http.StatusOK, resp.StatusCode) } - // Create file - a.MongoCollection.InsertOne(context.TODO(), expected) - a.MongoCollection.InsertOne(context.TODO(), expected) - // Get all files resp, err := http.Get(server.URL + "/files") assert.NoError(t, err) @@ -91,6 +78,8 @@ func TestFileIntegrationTests(t *testing.T) { 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) { @@ -116,19 +105,22 @@ func TestFileIntegrationTests(t *testing.T) { 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) - resp, err = http.Post(server.URL+"/file/"+initial.FileID.Hex(), "application/json", bytes.NewReader(body)) + resp, err = http.Post(server.URL+"/file/"+actual.FileID.Hex(), "application/json", bytes.NewReader(body)) assert.NoError(t, err) // Get file - resp, err = http.Get(server.URL + "/file/" + initial.FileID.Hex()) + resp, err = http.Get(server.URL + "/file/" + actual.FileID.Hex()) assert.NoError(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) - var actual models.File err = json.NewDecoder(resp.Body).Decode(&actual) assert.NoError(t, err) diff --git a/file-transfer/app/handle_upload.go b/file-transfer/app/handle_upload.go index 8f346b8..13474d6 100644 --- a/file-transfer/app/handle_upload.go +++ b/file-transfer/app/handle_upload.go @@ -3,100 +3,97 @@ package app import ( "context" "encoding/json" + "fmt" "io" "net/http" + "path/filepath" "file-transfer/models" ) // uploadFile godoc // -// @Summary Upload a file -// @Description Upload a file to the server -// @Tags files -// @Accept json -// @Produce json -// @Param file formData file true "File to upload" -// @Param path body string true "Path to store the file" -// @Success 200 {object} models.FileDataResponse "Uploaded file object" -// @Failure 400 {string} string "Invalid request payload" -// @Failure 500 {string} string "Internal server error" -// @Router /files/upload [post] +// @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.FileDataResponse +// @Failure 400 {string} string "Invalid request" +// @Failure 500 {string} string "Internal server error" +// @Router /files/upload [post] func (a *App) uploadFile(w http.ResponseWriter, r *http.Request) { ctx := context.TODO() - f := models.FileDataRequest{} - decoder := json.NewDecoder(r.Body) - if err := decoder.Decode(&f); err != nil { - respondWithError(w, http.StatusBadRequest, "Invalid request payload:"+err.Error()) - return - } - - // Parse multipart form with 32MB max memory - if err := r.ParseMultipartForm(32 << 20); err != nil { - respondWithError(w, http.StatusBadRequest, "File too large or invalid form") - return - } - - file, _, err := r.FormFile("file") + req, err := models.ParseUploadRequest(r) if err != nil { - respondWithError(w, http.StatusBadRequest, "Invalid file") + respondWithError(w, http.StatusBadRequest, "Failed to parse request: "+err.Error()) return } - defer file.Close() + defer req.File.Close() - buffer, err := io.ReadAll(file) + // Read file content + file, err := io.ReadAll(req.File) if err != nil { - respondWithError(w, http.StatusInternalServerError, "Error reading file") + respondWithError(w, http.StatusInternalServerError, "Failed to read file: "+err.Error()) return } fileData := models.FileData{ - Path: f.Path, - Data: buffer, + UserID: req.UserID, + Path: req.Path, + Data: file, } - URL, err := a.BlobStorage.UploadFile(ctx, fileData) + // Upload to blob storage + blobURL, err := a.BlobStorage.UploadFile(ctx, fileData) if err != nil { - respondWithError(w, http.StatusInternalServerError, err.Error()) + respondWithError(w, http.StatusInternalServerError, "Failed to upload: "+err.Error()) return } - respondWithJSON(w, http.StatusOK, models.FileDataResponse{ - Path: f.Path, - URL: URL, - }) + 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 from the server -// @Tags files -// @Accept json -// @Produce json -// @Param path body string true "Path to the file" -// @Success 200 {object} models.FileDataResponse "Downloaded file object" -// @Failure 400 {string} string "Invalid request payload" -// @Failure 500 {string} string "Internal server error" -// @Router /files/download [post] +// @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 /files/download [post] func (a *App) downloadFile(w http.ResponseWriter, r *http.Request) { ctx := context.TODO() - f := models.FileDataRequest{} + f := models.FileDownloadRequest{} decoder := json.NewDecoder(r.Body) if err := decoder.Decode(&f); err != nil { - respondWithError(w, http.StatusBadRequest, "Invalid request payload:"+err.Error()) + respondWithError(w, http.StatusBadRequest, "Invalid request payload: "+err.Error()) return } - file, err := a.BlobStorage.DownloadFile(ctx, f.Path) + // Download from blob storage + file, err := a.BlobStorage.DownloadFile(ctx, f.UserID, f.Path) if err != nil { - respondWithError(w, http.StatusInternalServerError, err.Error()) + respondWithError(w, http.StatusInternalServerError, "Failed to download: "+err.Error()) return } - w.Header().Set("Content-Disposition", "attachment; filename="+f.Path) + // 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 index 0f3e00d..f6ba823 100644 --- a/file-transfer/app/handle_upload_test.go +++ b/file-transfer/app/handle_upload_test.go @@ -16,15 +16,18 @@ import ( "github.com/stretchr/testify/assert" ) -func TestUploadLocalStorage(t *testing.T) { +func TestLocalStorage(t *testing.T) { a := RunTestApp(t) defer a.Close(context.Background()) defer KillDatabase() - t.Run("it should upload and return file", func(t *testing.T) { + t.Run("it should upload and download file", func(t *testing.T) { server := httptest.NewServer(a.Server.Handler) - expected := models.FileDataRequest{ - Path: "test.txt", + expected := models.FileResponse{ + UserID: "123", + Path: "test.txt", + URL: "http://localhost:8080/files/test.txt", + Size: 13, } fileData := "Hello, World!" @@ -37,62 +40,28 @@ func TestUploadLocalStorage(t *testing.T) { _, err = io.Copy(part, strings.NewReader(fileData)) assert.NoError(t, err) - err = writer.WriteField("path", expected.Path) + err = writer.WriteField("metadata", `{"user_id":"123","path":"test.txt"}`) assert.NoError(t, err) writer.Close() - resp, err := http.Post(server.URL+"/files/upload", writer.FormDataContentType(), body) + resp, err := http.Post(server.URL+"/file/upload", writer.FormDataContentType(), body) assert.NoError(t, err) - var actual models.FileDataResponse + 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) - }) -} - -func TestDownloadLocalStorage(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.FileDataRequest{ - Path: "test.txt", - } - - 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("path", expected.Path) - assert.NoError(t, err) - - writer.Close() - - resp, err := http.Post(server.URL+"/files/upload", writer.FormDataContentType(), body) - assert.NoError(t, err) - - var actual models.FileDataResponse - err = json.NewDecoder(resp.Body).Decode(&actual) - - resp, err = http.Post(server.URL+"/files/download", "application/json", strings.NewReader(`{"path":"test.txt"}`)) - assert.NoError(t, err) + assert.Equal(t, expected.URL, actual.URL) - var actualDownload models.FileDataResponse - err = json.NewDecoder(resp.Body).Decode(&actualDownload) + resp, err = http.Post(server.URL+"/file/download", "application/json", strings.NewReader(`{"user_id":"123","path":"test.txt"}`)) assert.NoError(t, err) - assert.Equal(t, expected.Path, actualDownload.Path) - assert.Equal(t, actual.URL, actualDownload.URL) + 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, resp.Body) }) } diff --git a/file-transfer/db/blob_storage.go b/file-transfer/db/blob_storage.go index 82e55e2..0bfa5cd 100644 --- a/file-transfer/db/blob_storage.go +++ b/file-transfer/db/blob_storage.go @@ -14,7 +14,7 @@ import ( type BlobStorage interface { UploadFile(ctx context.Context, f models.FileData) (string, error) - DownloadFile(ctx context.Context, path string) (*models.FileData, error) + DownloadFile(ctx context.Context, userID string, path string) (*models.FileData, error) } type LocalBlobStorage struct { diff --git a/file-transfer/db/handle_upload.go b/file-transfer/db/handle_upload.go index fcbdbd0..6b9bb4e 100644 --- a/file-transfer/db/handle_upload.go +++ b/file-transfer/db/handle_upload.go @@ -13,7 +13,7 @@ import ( ) func (bs *LocalBlobStorage) UploadFile(ctx context.Context, f models.FileData) (string, error) { - path := fmt.Sprintf("%s/%s", bs.rootPath, f.Path) + path := fmt.Sprintf("%s/%s/%s", bs.rootPath, f.UserID, f.Path) file, err := os.Create(path) if err != nil { return "", err @@ -28,8 +28,8 @@ func (bs *LocalBlobStorage) UploadFile(ctx context.Context, f models.FileData) ( return path, nil } -func (bs *LocalBlobStorage) DownloadFile(ctx context.Context, path string) (*models.FileData, error) { - file, err := os.Open(fmt.Sprintf("%s/%s", bs.rootPath, path)) +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 } @@ -51,7 +51,7 @@ func (bs *LocalBlobStorage) DownloadFile(ctx context.Context, path string) (*mod func (bs *AzureBlobStorage) UploadFile(ctx context.Context, f models.FileData) (string, error) { reader := bytes.NewReader(f.Data) - blobURL := bs.containerURL.NewBlockBlobURL(f.Path) + 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 @@ -60,8 +60,8 @@ func (bs *AzureBlobStorage) UploadFile(ctx context.Context, f models.FileData) ( return blobURL.String(), nil } -func (bs *AzureBlobStorage) DownloadFile(ctx context.Context, path string) (*models.FileData, error) { - blobURL := bs.containerURL.NewBlockBlobURL(path) +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 diff --git a/file-transfer/models/file_data.go b/file-transfer/models/file_data.go index b18ebad..8f5e34f 100644 --- a/file-transfer/models/file_data.go +++ b/file-transfer/models/file_data.go @@ -1,15 +1,56 @@ package models +import ( + "encoding/json" + "fmt" + "mime/multipart" + "net/http" +) + type FileData struct { - Path string `json:"path"` - Data []byte `json:"data"` + 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 FileDataResponse struct { - Path string `json:"path"` - URL string `json:"url"` +type FileResponse struct { + UserID string `json:"user_id"` + Path string `json:"path"` + URL string `json:"url"` + Size int64 `json:"size"` } -type FileDataRequest struct { - Path string `json:"path"` +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 } From 824a3d0a44c8b546598a6b9fb61b61618a151dec Mon Sep 17 00:00:00 2001 From: Bruno Sienkiewicz Date: Wed, 22 Jan 2025 14:41:03 +0100 Subject: [PATCH 18/34] added get files by user --- file-transfer/app/handle_file.go | 39 ++++++++++++++++++++++++++------ file-transfer/app/routes.go | 1 + file-transfer/db/handle_file.go | 32 +++++++++++++++++++++----- file-transfer/main.go | 2 +- file-transfer/models/file.go | 16 ++++++------- 5 files changed, 68 insertions(+), 22 deletions(-) diff --git a/file-transfer/app/handle_file.go b/file-transfer/app/handle_file.go index 3222717..a922a14 100644 --- a/file-transfer/app/handle_file.go +++ b/file-transfer/app/handle_file.go @@ -9,7 +9,7 @@ import ( "file-transfer/models" "github.com/gorilla/mux" - "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/v2/bson" ) // createFile godoc @@ -35,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 @@ -57,7 +58,7 @@ func (a *App) createFile(w http.ResponseWriter, r *http.Request) { 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 @@ -90,7 +91,31 @@ func (a *App) getAllFiles(w http.ResponseWriter, r *http.Request) { return } - respondWithJSON(w, http.StatusCreated, files) + 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 /files/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 + } + + respondWithJSON(w, http.StatusOK, files) } // updateFile godoc @@ -109,7 +134,7 @@ func (a *App) getAllFiles(w http.ResponseWriter, r *http.Request) { 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 @@ -145,7 +170,7 @@ func (a *App) updateFile(w http.ResponseWriter, r *http.Request) { 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 { respondWithError(w, http.StatusBadRequest, "Invalid file ID") return diff --git a/file-transfer/app/routes.go b/file-transfer/app/routes.go index 4690519..51ef9d4 100644 --- a/file-transfer/app/routes.go +++ b/file-transfer/app/routes.go @@ -23,6 +23,7 @@ func (a *App) initRoutes() { 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("/files/user/{user_id}", a.getFilesByUser).Methods(http.MethodGet) a.Router.HandleFunc("file/upload", a.uploadFile).Methods(http.MethodPost) a.Router.HandleFunc("file/download", a.downloadFile).Methods(http.MethodGet) diff --git a/file-transfer/db/handle_file.go b/file-transfer/db/handle_file.go index e3914bc..42fd6be 100644 --- a/file-transfer/db/handle_file.go +++ b/file-transfer/db/handle_file.go @@ -9,18 +9,19 @@ import ( "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,9 @@ 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} + + err := collection.FindOne(*ctx, filter).Decode(f) if err != nil { return f, err } @@ -42,6 +45,23 @@ func GetFile(ctx *context.Context, collection *mongo.Collection, f models.File) return f, nil } +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) error { filter := bson.M{"_id": f.FileID} diff --git a/file-transfer/main.go b/file-transfer/main.go index 3ba6d03..1fbc926 100644 --- a/file-transfer/main.go +++ b/file-transfer/main.go @@ -28,7 +28,7 @@ func main() { quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() a := app.App{} diff --git a/file-transfer/models/file.go b/file-transfer/models/file.go index 8fd09ff..8d7fb87 100644 --- a/file-transfer/models/file.go +++ b/file-transfer/models/file.go @@ -1,15 +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"` - 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 + 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 } From d6a0d02366887275191d876dfb6fc2d8b10e5d82 Mon Sep 17 00:00:00 2001 From: Bruno Sienkiewicz Date: Wed, 22 Jan 2025 15:12:43 +0100 Subject: [PATCH 19/34] updated endpoints --- file-transfer/app/handle_upload.go | 4 ++-- file-transfer/app/routes.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/file-transfer/app/handle_upload.go b/file-transfer/app/handle_upload.go index 13474d6..b0e8fca 100644 --- a/file-transfer/app/handle_upload.go +++ b/file-transfer/app/handle_upload.go @@ -23,7 +23,7 @@ import ( // @Success 200 {object} models.FileDataResponse // @Failure 400 {string} string "Invalid request" // @Failure 500 {string} string "Internal server error" -// @Router /files/upload [post] +// @Router /upload [post] func (a *App) uploadFile(w http.ResponseWriter, r *http.Request) { ctx := context.TODO() @@ -74,7 +74,7 @@ func (a *App) uploadFile(w http.ResponseWriter, r *http.Request) { // @Success 200 {file} file "File content" // @Failure 400 {string} string "Invalid request" // @Failure 500 {string} string "Internal server error" -// @Router /files/download [post] +// @Router /download [get] func (a *App) downloadFile(w http.ResponseWriter, r *http.Request) { ctx := context.TODO() f := models.FileDownloadRequest{} diff --git a/file-transfer/app/routes.go b/file-transfer/app/routes.go index 51ef9d4..c343932 100644 --- a/file-transfer/app/routes.go +++ b/file-transfer/app/routes.go @@ -25,6 +25,6 @@ func (a *App) initRoutes() { a.Router.HandleFunc("/file/{file_id}", a.deleteFile).Methods(http.MethodDelete) a.Router.HandleFunc("/files/user/{user_id}", a.getFilesByUser).Methods(http.MethodGet) - a.Router.HandleFunc("file/upload", a.uploadFile).Methods(http.MethodPost) - a.Router.HandleFunc("file/download", a.downloadFile).Methods(http.MethodGet) + a.Router.HandleFunc("/upload", a.uploadFile).Methods(http.MethodPost) + a.Router.HandleFunc("/download", a.downloadFile).Methods(http.MethodGet) } From 2ed5f49cec4e18fdc750694e32f9b0013256a96f Mon Sep 17 00:00:00 2001 From: Bruno Sienkiewicz Date: Wed, 22 Jan 2025 15:17:25 +0100 Subject: [PATCH 20/34] added user files test --- file-transfer/app/handle_file_test.go | 83 +++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/file-transfer/app/handle_file_test.go b/file-transfer/app/handle_file_test.go index 4b1b3fb..7fe7a4d 100644 --- a/file-transfer/app/handle_file_test.go +++ b/file-transfer/app/handle_file_test.go @@ -129,4 +129,87 @@ func TestFileIntegrationTests(t *testing.T) { 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.StatusOK, 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 + "/files/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) + }) } From e2f0081b9f43de2c2c2c69d09891f49cd7a25026 Mon Sep 17 00:00:00 2001 From: Bruno Sienkiewicz Date: Fri, 24 Jan 2025 00:02:59 +0100 Subject: [PATCH 21/34] fixed upload method and status codes --- file-transfer/app/app.go | 3 ++- file-transfer/app/handle_file.go | 2 +- file-transfer/app/handle_file_test.go | 8 +++++--- file-transfer/db/handle_upload.go | 2 ++ 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/file-transfer/app/app.go b/file-transfer/app/app.go index 0d8c59f..0221312 100644 --- a/file-transfer/app/app.go +++ b/file-transfer/app/app.go @@ -34,7 +34,8 @@ func (a *App) Initialize(ctx *context.Context) { storageType := os.Getenv("STORAGE_TYPE") if storageType == "local" { - a.BlobStorage, _ = db.InitLocalBlobStorage("files") + path := os.Getenv("LOCAL_STORAGE_PATH") + a.BlobStorage, _ = db.InitLocalBlobStorage(path) } else { a.BlobStorage, _ = db.InitAzureBlobStorage("files") } diff --git a/file-transfer/app/handle_file.go b/file-transfer/app/handle_file.go index a922a14..fc61c9f 100644 --- a/file-transfer/app/handle_file.go +++ b/file-transfer/app/handle_file.go @@ -20,7 +20,7 @@ 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] diff --git a/file-transfer/app/handle_file_test.go b/file-transfer/app/handle_file_test.go index 7fe7a4d..ef4f36f 100644 --- a/file-transfer/app/handle_file_test.go +++ b/file-transfer/app/handle_file_test.go @@ -20,6 +20,7 @@ func TestFileIntegrationTests(t *testing.T) { 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", @@ -34,7 +35,7 @@ func TestFileIntegrationTests(t *testing.T) { resp, err := http.Post(server.URL+"/file", "application/json", reader) assert.NoError(t, err) - assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, http.StatusCreated, resp.StatusCode) var actual models.File err = json.NewDecoder(resp.Body).Decode(&actual) @@ -57,6 +58,7 @@ func TestFileIntegrationTests(t *testing.T) { // 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) @@ -64,7 +66,7 @@ func TestFileIntegrationTests(t *testing.T) { resp, err := http.Post(server.URL+"/file", "application/json", reader) assert.NoError(t, err) - assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, http.StatusCreated, resp.StatusCode) } // Get all files @@ -189,7 +191,7 @@ func TestFileIntegrationTests(t *testing.T) { resp, err := http.Post(server.URL+"/file", "application/json", reader) assert.NoError(t, err) - assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, http.StatusCreated, resp.StatusCode) reqBody, err = json.Marshal(file2) assert.NoError(t, err) diff --git a/file-transfer/db/handle_upload.go b/file-transfer/db/handle_upload.go index 6b9bb4e..4b741b3 100644 --- a/file-transfer/db/handle_upload.go +++ b/file-transfer/db/handle_upload.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "os" + "path/filepath" "file-transfer/models" @@ -14,6 +15,7 @@ import ( 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 From cf3b396f58d898bb391fba4b9bfc279abe023f6d Mon Sep 17 00:00:00 2001 From: Bruno Sienkiewicz Date: Fri, 24 Jan 2025 00:20:57 +0100 Subject: [PATCH 22/34] working get method --- file-transfer/app/handle_file_test.go | 3 ++- file-transfer/db/handle_file.go | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/file-transfer/app/handle_file_test.go b/file-transfer/app/handle_file_test.go index ef4f36f..98c1b0c 100644 --- a/file-transfer/app/handle_file_test.go +++ b/file-transfer/app/handle_file_test.go @@ -114,8 +114,9 @@ func TestFileIntegrationTests(t *testing.T) { // Update file body, err = json.Marshal(expected) assert.NoError(t, err) - resp, err = http.Post(server.URL+"/file/"+actual.FileID.Hex(), "application/json", bytes.NewReader(body)) + req, err := http.NewRequest("PUT", server.URL+"/files"+actual.FileID.Hex(), bytes.NewReader(body)) assert.NoError(t, err) + http.DefaultClient.Do(req) // Get file resp, err = http.Get(server.URL + "/file/" + actual.FileID.Hex()) diff --git a/file-transfer/db/handle_file.go b/file-transfer/db/handle_file.go index 42fd6be..bcfb599 100644 --- a/file-transfer/db/handle_file.go +++ b/file-transfer/db/handle_file.go @@ -37,7 +37,8 @@ func GetAllFiles(ctx *context.Context, collection *mongo.Collection) ([]models.F func GetFile(ctx *context.Context, collection *mongo.Collection, f models.File) (models.File, error) { filter := bson.M{"_id": f.FileID} - err := collection.FindOne(*ctx, filter).Decode(f) + res := collection.FindOne(*ctx, filter) + err := res.Decode(&f) if err != nil { return f, err } From e1e6d751d426a7162c5cc43ca6d2a5b757a6ff95 Mon Sep 17 00:00:00 2001 From: Bruno Sienkiewicz Date: Fri, 24 Jan 2025 00:31:36 +0100 Subject: [PATCH 23/34] added handling files not found --- file-transfer/app/handle_file.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/file-transfer/app/handle_file.go b/file-transfer/app/handle_file.go index fc61c9f..8fdbf9e 100644 --- a/file-transfer/app/handle_file.go +++ b/file-transfer/app/handle_file.go @@ -67,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 } @@ -91,6 +95,10 @@ func (a *App) getAllFiles(w http.ResponseWriter, r *http.Request) { return } + if len(files) == 0 { + respondWithError(w, http.StatusNotFound, "No files found") + return + } respondWithJSON(w, http.StatusOK, files) } @@ -149,6 +157,10 @@ func (a *App) updateFile(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() if err := db.UpdateFile(&ctx, a.MongoCollection, f); 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 } @@ -172,6 +184,10 @@ func (a *App) deleteFile(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) 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 } From 1ce7878894f87247f7d60803e4d4d508ec8b4cd9 Mon Sep 17 00:00:00 2001 From: Bruno Sienkiewicz Date: Fri, 24 Jan 2025 12:37:40 +0100 Subject: [PATCH 24/34] added makefile and fixed paths --- file-transfer/Makefile | 43 +++++++++++++++++++++++++ file-transfer/app/handle_upload_test.go | 4 +-- 2 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 file-transfer/Makefile 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/app/handle_upload_test.go b/file-transfer/app/handle_upload_test.go index f6ba823..d6757c9 100644 --- a/file-transfer/app/handle_upload_test.go +++ b/file-transfer/app/handle_upload_test.go @@ -45,7 +45,7 @@ func TestLocalStorage(t *testing.T) { writer.Close() - resp, err := http.Post(server.URL+"/file/upload", writer.FormDataContentType(), body) + resp, err := http.Post(server.URL+"/upload", writer.FormDataContentType(), body) assert.NoError(t, err) var actual models.FileResponse @@ -56,7 +56,7 @@ func TestLocalStorage(t *testing.T) { assert.Equal(t, expected.Path, actual.Path) assert.Equal(t, expected.URL, actual.URL) - resp, err = http.Post(server.URL+"/file/download", "application/json", strings.NewReader(`{"user_id":"123","path":"test.txt"}`)) + resp, err = http.Post(server.URL+"/download", "application/json", strings.NewReader(`{"user_id":"123","path":"test.txt"}`)) assert.NoError(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) From e652af9da7e76c512f8833cf239f13c431199538 Mon Sep 17 00:00:00 2001 From: Bruno Sienkiewicz Date: Fri, 24 Jan 2025 13:31:50 +0100 Subject: [PATCH 25/34] working upload tests --- file-transfer/app/handle_upload.go | 2 +- file-transfer/app/handle_upload_test.go | 7 +++++-- file-transfer/app/routes.go | 2 +- file-transfer/db/handle_upload.go | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/file-transfer/app/handle_upload.go b/file-transfer/app/handle_upload.go index b0e8fca..32dc904 100644 --- a/file-transfer/app/handle_upload.go +++ b/file-transfer/app/handle_upload.go @@ -74,7 +74,7 @@ func (a *App) uploadFile(w http.ResponseWriter, r *http.Request) { // @Success 200 {file} file "File content" // @Failure 400 {string} string "Invalid request" // @Failure 500 {string} string "Internal server error" -// @Router /download [get] +// @Router /download [post] func (a *App) downloadFile(w http.ResponseWriter, r *http.Request) { ctx := context.TODO() f := models.FileDownloadRequest{} diff --git a/file-transfer/app/handle_upload_test.go b/file-transfer/app/handle_upload_test.go index d6757c9..c4fbb90 100644 --- a/file-transfer/app/handle_upload_test.go +++ b/file-transfer/app/handle_upload_test.go @@ -26,7 +26,7 @@ func TestLocalStorage(t *testing.T) { expected := models.FileResponse{ UserID: "123", Path: "test.txt", - URL: "http://localhost:8080/files/test.txt", + URL: "http://localhost:8080/files/123/test.txt", Size: 13, } @@ -59,9 +59,12 @@ func TestLocalStorage(t *testing.T) { 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, resp.Body) + assert.Equal(t, fileData, string(respFile)) }) } diff --git a/file-transfer/app/routes.go b/file-transfer/app/routes.go index c343932..a20f6c7 100644 --- a/file-transfer/app/routes.go +++ b/file-transfer/app/routes.go @@ -26,5 +26,5 @@ func (a *App) initRoutes() { a.Router.HandleFunc("/files/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.MethodGet) + a.Router.HandleFunc("/download", a.downloadFile).Methods(http.MethodPost) } diff --git a/file-transfer/db/handle_upload.go b/file-transfer/db/handle_upload.go index 4b741b3..824761d 100644 --- a/file-transfer/db/handle_upload.go +++ b/file-transfer/db/handle_upload.go @@ -14,7 +14,7 @@ import ( ) func (bs *LocalBlobStorage) UploadFile(ctx context.Context, f models.FileData) (string, error) { - path := fmt.Sprintf("%s/%s/%s", bs.rootPath, f.UserID, f.Path) + path := fmt.Sprintf("%s/%s/%s/%s", "http://localhost:8080", bs.rootPath, f.UserID, f.Path) os.MkdirAll(filepath.Dir(path), os.ModePerm) file, err := os.Create(path) if err != nil { From 3b1a4f64e781de52488adccf3bfc7bb9274ef5a4 Mon Sep 17 00:00:00 2001 From: Bruno Sienkiewicz Date: Fri, 24 Jan 2025 13:32:04 +0100 Subject: [PATCH 26/34] added cors --- file-transfer/app/app.go | 7 +++ file-transfer/app/cors_middleware.go | 85 +++++++++++++++++++++++++++ file-transfer/app/handle_file_test.go | 2 +- 3 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 file-transfer/app/cors_middleware.go diff --git a/file-transfer/app/app.go b/file-transfer/app/app.go index 0221312..189ffe2 100644 --- a/file-transfer/app/app.go +++ b/file-transfer/app/app.go @@ -41,7 +41,14 @@ func (a *App) Initialize(ctx *context.Context) { } 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() } 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_test.go b/file-transfer/app/handle_file_test.go index 98c1b0c..bb8c298 100644 --- a/file-transfer/app/handle_file_test.go +++ b/file-transfer/app/handle_file_test.go @@ -114,7 +114,7 @@ func TestFileIntegrationTests(t *testing.T) { // Update file body, err = json.Marshal(expected) assert.NoError(t, err) - req, err := http.NewRequest("PUT", server.URL+"/files"+actual.FileID.Hex(), bytes.NewReader(body)) + req, err := http.NewRequest("PUT", server.URL+"/file/"+actual.FileID.Hex(), bytes.NewReader(body)) assert.NoError(t, err) http.DefaultClient.Do(req) From 101820e6bf7f7f1ae23dd9168284ec5856a414e3 Mon Sep 17 00:00:00 2001 From: Bruno Sienkiewicz Date: Fri, 24 Jan 2025 14:40:12 +0100 Subject: [PATCH 27/34] tests are passing --- file-transfer/app/app.go | 2 +- file-transfer/app/handle_file.go | 12 +++++++++--- file-transfer/app/handle_file_test.go | 18 ++++++++++-------- file-transfer/db/handle_file.go | 9 +++++---- file-transfer/db/handle_upload.go | 4 ++-- 5 files changed, 27 insertions(+), 18 deletions(-) diff --git a/file-transfer/app/app.go b/file-transfer/app/app.go index 189ffe2..0bcc4a1 100644 --- a/file-transfer/app/app.go +++ b/file-transfer/app/app.go @@ -41,7 +41,7 @@ func (a *App) Initialize(ctx *context.Context) { } logMiddleware := NewLogMiddleware(a.Logger) - corsMiddleware := NewCorsMiddleware( + corsMiddleware := NewCORSMiddleware( []string{"http://localhost:8080"}, // Allowed origins []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, // Allowed methods []string{"Content-Type", "Authorization"}, // Allowed headers diff --git a/file-transfer/app/handle_file.go b/file-transfer/app/handle_file.go index 8fdbf9e..b615e8b 100644 --- a/file-transfer/app/handle_file.go +++ b/file-transfer/app/handle_file.go @@ -96,7 +96,7 @@ func (a *App) getAllFiles(w http.ResponseWriter, r *http.Request) { } if len(files) == 0 { - respondWithError(w, http.StatusNotFound, "No files found") + respondWithError(w, http.StatusNoContent, string(json.RawMessage("[]"))) return } respondWithJSON(w, http.StatusOK, files) @@ -123,6 +123,10 @@ func (a *App) getFilesByUser(w http.ResponseWriter, r *http.Request) { return } + if len(files) == 0 { + respondWithError(w, http.StatusNoContent, string(json.RawMessage("[]"))) + return + } respondWithJSON(w, http.StatusOK, files) } @@ -148,7 +152,7 @@ func (a *App) updateFile(w http.ResponseWriter, r *http.Request) { 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()) @@ -156,7 +160,9 @@ 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 diff --git a/file-transfer/app/handle_file_test.go b/file-transfer/app/handle_file_test.go index bb8c298..d231471 100644 --- a/file-transfer/app/handle_file_test.go +++ b/file-transfer/app/handle_file_test.go @@ -94,7 +94,7 @@ func TestFileIntegrationTests(t *testing.T) { } expected := models.File{ FileName: "test.txt", - UserID: "456", + UserID: "123", Path: "path/test.txt", BlobURL: "http://example.com", } @@ -116,16 +116,18 @@ func TestFileIntegrationTests(t *testing.T) { assert.NoError(t, err) req, err := http.NewRequest("PUT", server.URL+"/file/"+actual.FileID.Hex(), bytes.NewReader(body)) assert.NoError(t, err) - http.DefaultClient.Do(req) - // Get file - resp, err = http.Get(server.URL + "/file/" + actual.FileID.Hex()) - 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) + assert.Equal(t, http.StatusOK, resp.StatusCode) - err = json.NewDecoder(resp.Body).Decode(&actual) - assert.NoError(t, err) + 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) diff --git a/file-transfer/db/handle_file.go b/file-transfer/db/handle_file.go index bcfb599..0bfd627 100644 --- a/file-transfer/db/handle_file.go +++ b/file-transfer/db/handle_file.go @@ -63,7 +63,7 @@ func GetFilesByUserID(ctx *context.Context, collection *mongo.Collection, userID return files, nil } -func UpdateFile(ctx *context.Context, collection *mongo.Collection, f models.File) error { +func UpdateFile(ctx *context.Context, collection *mongo.Collection, f models.File) (models.File, error) { filter := bson.M{"_id": f.FileID} update := bson.M{ @@ -77,12 +77,13 @@ func UpdateFile(ctx *context.Context, collection *mongo.Collection, f models.Fil }, } - 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 index 824761d..da4fff8 100644 --- a/file-transfer/db/handle_upload.go +++ b/file-transfer/db/handle_upload.go @@ -14,7 +14,7 @@ import ( ) func (bs *LocalBlobStorage) UploadFile(ctx context.Context, f models.FileData) (string, error) { - path := fmt.Sprintf("%s/%s/%s/%s", "http://localhost:8080", bs.rootPath, f.UserID, f.Path) + 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 { @@ -27,7 +27,7 @@ func (bs *LocalBlobStorage) UploadFile(ctx context.Context, f models.FileData) ( return "", err } - return path, nil + return fmt.Sprintf("http://localhost:8080/%s", path), nil } func (bs *LocalBlobStorage) DownloadFile(ctx context.Context, userID string, path string) (*models.FileData, error) { From 5041062a8f58e0a7553787073ffb44daa4e292a8 Mon Sep 17 00:00:00 2001 From: Bruno Sienkiewicz Date: Fri, 24 Jan 2025 15:11:55 +0100 Subject: [PATCH 28/34] updated readme with configuration and examples --- file-transfer/README.md | 65 ++++++++++++++++++++++++++++++++++------- 1 file changed, 54 insertions(+), 11 deletions(-) diff --git a/file-transfer/README.md b/file-transfer/README.md index b4b77ee..38a70ad 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. + +### Exapmle 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`. From c9311625dbeaf55ca7e8be29f2d306e556f7e715 Mon Sep 17 00:00:00 2001 From: Bruno Sienkiewicz Date: Fri, 24 Jan 2025 15:33:02 +0100 Subject: [PATCH 29/34] fixed model type --- file-transfer/app/handle_upload.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/file-transfer/app/handle_upload.go b/file-transfer/app/handle_upload.go index 32dc904..d24c31b 100644 --- a/file-transfer/app/handle_upload.go +++ b/file-transfer/app/handle_upload.go @@ -20,7 +20,7 @@ import ( // @Produce json // @Param metadata formData string true "JSON metadata with path" // @Param file formData file true "File content" -// @Success 200 {object} models.FileDataResponse +// @Success 200 {object} models.FileResponse // @Failure 400 {string} string "Invalid request" // @Failure 500 {string} string "Internal server error" // @Router /upload [post] From 0d53e1f41bf0df1ff55d049673acd8f3b7138716 Mon Sep 17 00:00:00 2001 From: Bruno Sienkiewicz Date: Fri, 24 Jan 2025 15:35:51 +0100 Subject: [PATCH 30/34] updated swagger docs --- file-transfer/app/handle_file.go | 16 ++++++------ file-transfer/app/handle_upload.go | 42 +++++++++++++++--------------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/file-transfer/app/handle_file.go b/file-transfer/app/handle_file.go index b615e8b..296214f 100644 --- a/file-transfer/app/handle_file.go +++ b/file-transfer/app/handle_file.go @@ -104,14 +104,14 @@ func (a *App) getAllFiles(w http.ResponseWriter, r *http.Request) { // 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 /files/user/{user_id} [get] +// @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 /files/user/{user_id} [get] func (a *App) getFilesByUser(w http.ResponseWriter, r *http.Request) { ctx := context.TODO() vars := mux.Vars(r) diff --git a/file-transfer/app/handle_upload.go b/file-transfer/app/handle_upload.go index d24c31b..71da211 100644 --- a/file-transfer/app/handle_upload.go +++ b/file-transfer/app/handle_upload.go @@ -13,17 +13,17 @@ import ( // 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] +// @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() @@ -65,16 +65,16 @@ func (a *App) uploadFile(w http.ResponseWriter, r *http.Request) { // 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] +// @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{} From 841a0c01ee51eb952d7f7deffe4984742933ea6b Mon Sep 17 00:00:00 2001 From: Bruno Sienkiewicz Date: Fri, 24 Jan 2025 16:20:08 +0100 Subject: [PATCH 31/34] fixed typo --- file-transfer/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/file-transfer/README.md b/file-transfer/README.md index 38a70ad..167098a 100644 --- a/file-transfer/README.md +++ b/file-transfer/README.md @@ -61,7 +61,7 @@ To run the microservice execute `make run` command in project _file-transfer_ di To run tests execute `make test` command in project _file-transfer_ directory. -### Exapmle usage +### Example usage To upload file you need to send POST request to `/files` endpoint with file in body. Example request using `curl`: From 63e4e5a107e7e854fe235247eab8b6a8e93a23a1 Mon Sep 17 00:00:00 2001 From: Bruno Sienkiewicz Date: Fri, 24 Jan 2025 16:21:49 +0100 Subject: [PATCH 32/34] consistent file endpoints --- file-transfer/app/handle_file.go | 10 +++++----- file-transfer/app/routes.go | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/file-transfer/app/handle_file.go b/file-transfer/app/handle_file.go index 296214f..4a39468 100644 --- a/file-transfer/app/handle_file.go +++ b/file-transfer/app/handle_file.go @@ -23,7 +23,7 @@ import ( // @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{} @@ -54,7 +54,7 @@ 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) @@ -111,7 +111,7 @@ func (a *App) getAllFiles(w http.ResponseWriter, r *http.Request) { // @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 /files/user/{user_id} [get] +// @Router /file/user/{user_id} [get] func (a *App) getFilesByUser(w http.ResponseWriter, r *http.Request) { ctx := context.TODO() vars := mux.Vars(r) @@ -142,7 +142,7 @@ func (a *App) getFilesByUser(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) @@ -184,7 +184,7 @@ 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) diff --git a/file-transfer/app/routes.go b/file-transfer/app/routes.go index a20f6c7..fd267c5 100644 --- a/file-transfer/app/routes.go +++ b/file-transfer/app/routes.go @@ -23,7 +23,7 @@ func (a *App) initRoutes() { 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("/files/user/{user_id}", a.getFilesByUser).Methods(http.MethodGet) + 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) From 7807d76bbdf83be524581496aa90e3ef901cce35 Mon Sep 17 00:00:00 2001 From: Bruno Sienkiewicz Date: Fri, 24 Jan 2025 16:24:53 +0100 Subject: [PATCH 33/34] updated test for new endpoint --- file-transfer/app/handle_file_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/file-transfer/app/handle_file_test.go b/file-transfer/app/handle_file_test.go index d231471..e409d7d 100644 --- a/file-transfer/app/handle_file_test.go +++ b/file-transfer/app/handle_file_test.go @@ -203,7 +203,7 @@ func TestFileIntegrationTests(t *testing.T) { resp, err = http.Post(server.URL+"/file", "application/json", reader) // Get all files - resp, err = http.Get(server.URL + "/files/user/123") + resp, err = http.Get(server.URL + "/file/user/123") assert.NoError(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) From 9ff28a3d42292284e4ad5c49930ca8321d6e58ce Mon Sep 17 00:00:00 2001 From: Bruno Sienkiewicz Date: Fri, 24 Jan 2025 16:31:29 +0100 Subject: [PATCH 34/34] updated docs --- file-transfer/docs/docs.go | 201 +++++++++++++++++++++++++++++--- file-transfer/docs/swagger.json | 201 +++++++++++++++++++++++++++++--- file-transfer/docs/swagger.yaml | 138 +++++++++++++++++++--- 3 files changed, 489 insertions(+), 51 deletions(-) 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"