diff --git a/api/.idea/.gitignore b/api/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/api/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/api/.idea/api.iml b/api/.idea/api.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/api/.idea/api.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/api/.idea/modules.xml b/api/.idea/modules.xml new file mode 100644 index 0000000..d50cf45 --- /dev/null +++ b/api/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/api/.idea/vcs.xml b/api/.idea/vcs.xml new file mode 100644 index 0000000..6c0b863 --- /dev/null +++ b/api/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/api/go.mod b/api/go.mod index fe0a60a..a838522 100644 --- a/api/go.mod +++ b/api/go.mod @@ -5,4 +5,5 @@ go 1.17 require ( github.com/go-chi/chi v1.5.4 // indirect github.com/go-chi/render v1.0.1 // indirect + github.com/google/uuid v1.3.0 // indirect ) diff --git a/api/go.sum b/api/go.sum index 544024a..7606d92 100644 --- a/api/go.sum +++ b/api/go.sum @@ -2,3 +2,5 @@ github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs= github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg= github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8= github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/api/internal/app/api/common/responses/responses.go b/api/internal/app/api/common/responses/responses.go index 0a082b1..cf622bf 100644 --- a/api/internal/app/api/common/responses/responses.go +++ b/api/internal/app/api/common/responses/responses.go @@ -27,3 +27,7 @@ func ErrInvalidRequest(err error) render.Renderer { ErrorText: err.Error(), } } + +func ErrNotFound() render.Renderer { + return &ErrResponse{HTTPStatusCode: 404, StatusText: "Resource not found."} +} diff --git a/api/internal/app/api/download/download.go b/api/internal/app/api/download/download.go deleted file mode 100644 index 5b3a4f0..0000000 --- a/api/internal/app/api/download/download.go +++ /dev/null @@ -1,42 +0,0 @@ -package download - -import ( - "downloader/internal/app/api/common/responses" - "errors" - "github.com/go-chi/chi" - "github.com/go-chi/render" - "net/http" -) - -type api struct{} - -func New() *api { - return &api{} -} - -func (api *api) SetupDownloadApi(router *chi.Mux) { - router.Route("/downloads", func(r chi.Router) { - r.Post("/", api.requestDownload) - }) -} - -type requestDownloadRequest struct { - Provider string `json:"provider"` - Link string `json:"link"` -} - -func (dr *requestDownloadRequest) Bind(r *http.Request) error { - if dr.Link == "" || dr.Provider == "" { - return errors.New("missing required download request field") - } - - return nil -} - -func (api *api) requestDownload(w http.ResponseWriter, r *http.Request) { - data := &requestDownloadRequest{} - if err := render.Bind(r, data); err != nil { - _ = render.Render(w, r, responses.ErrInvalidRequest(err)) - return - } -} diff --git a/api/internal/app/api/download/download_context.go b/api/internal/app/api/download/download_context.go new file mode 100644 index 0000000..7737f52 --- /dev/null +++ b/api/internal/app/api/download/download_context.go @@ -0,0 +1,22 @@ +package download + +import ( + "context" + "downloader/internal/app/api/common/responses" + "github.com/go-chi/chi" + "github.com/go-chi/render" + "net/http" +) + +func Context(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if downloadId := chi.URLParam(r, "downloadId"); downloadId != "" { + ctx := context.WithValue(r.Context(), "downloadId", downloadId) + next.ServeHTTP(w, r.WithContext(ctx)) + } else { + _ = render.Render(w, r, responses.ErrNotFound()) + return + } + + }) +} diff --git a/api/internal/app/api/download/request_download.go b/api/internal/app/api/download/request_download.go new file mode 100644 index 0000000..a5b5123 --- /dev/null +++ b/api/internal/app/api/download/request_download.go @@ -0,0 +1,66 @@ +package download + +import ( + "downloader/internal/app/api/common/responses" + "downloader/internal/core/entities" + "errors" + "github.com/go-chi/render" + "net/http" +) + +type requestDownloadRequest struct { + Provider string `json:"provider"` + Link string `json:"link"` +} + +type requestDownloadResponse struct { + *entities.Download +} + +func (_ requestDownloadResponse) Render(_ http.ResponseWriter, _ *http.Request) error { + return nil +} + +func (dr *requestDownloadRequest) Bind(r *http.Request) error { + if dr.Link == "" || dr.Provider == "" { + return errors.New("missing required download request field") + } + + return nil +} + +func (a *api) requestDownload(w http.ResponseWriter, r *http.Request) { + data := &requestDownloadRequest{} + if err := render.Bind(r, data); err != nil { + _ = render.Render(w, r, responses.ErrInvalidRequest(err)) + return + } + + download, err := a.drService.Schedule(data.Provider, data.Link) + if err != nil { + _ = render.Render(w, r, responses.ErrInvalidRequest(err)) + return + } + + render.Status(r, http.StatusAccepted) + _ = render.Render(w, r, newRequestDownloadResponse(download)) +} + +func (a *api) getDownloadById(w http.ResponseWriter, r *http.Request) { + downloadId := r.Context().Value("downloadId").(string) + + download, err := a.drService.Get(downloadId) + if err != nil { + _ = render.Render(w, r, responses.ErrNotFound()) + return + } + + if err := render.Render(w, r, newRequestDownloadResponse(download)); err != nil { + _ = render.Render(w, r, responses.ErrInvalidRequest(err)) + return + } +} + +func newRequestDownloadResponse(download *entities.Download) *requestDownloadResponse { + return &requestDownloadResponse{Download: download} +} diff --git a/api/internal/app/api/download/routes.go b/api/internal/app/api/download/routes.go new file mode 100644 index 0000000..f68ba63 --- /dev/null +++ b/api/internal/app/api/download/routes.go @@ -0,0 +1,24 @@ +package download + +import ( + "downloader/internal/core/ports/download_request" + "github.com/go-chi/chi" +) + +type api struct { + drService download_request.Service +} + +func New(service download_request.Service) *api { + return &api{drService: service} +} + +func (a *api) SetupDownloadApi(router *chi.Mux) { + router.Route("/downloads", func(r chi.Router) { + r.Post("/", a.requestDownload) + r.Route("/{downloadId}", func(r chi.Router) { + r.Use(Context) + r.Get("/", a.getDownloadById) + }) + }) +} diff --git a/api/internal/app/router/router.go b/api/internal/app/router/router.go index 84c3115..4094aa6 100644 --- a/api/internal/app/router/router.go +++ b/api/internal/app/router/router.go @@ -2,6 +2,8 @@ package router import ( "downloader/internal/app/api/download" + "downloader/internal/core/ports/download_request" + "downloader/pkg/common/uuid" "github.com/go-chi/chi" "github.com/go-chi/chi/middleware" "net/http" @@ -23,7 +25,7 @@ func NewRouter() *router { } func (router *router) Run() { - http.ListenAndServe(":3333", router.internalRouter) + _ = http.ListenAndServe(":3333", router.internalRouter) } func (router *router) RegisterApi() *chi.Mux { @@ -41,9 +43,17 @@ func (router *router) setupMiddleware() *router { } func (router *router) setupRoutes() *router { - downloadApi := download.New() - - downloadApi.SetupDownloadApi(router.internalRouter) + setupDownloadRoute(router) return router } + +func setupDownloadRoute(router *router) { + drRepository := download_request.NewInMemoryRepository() + drBackgroundService := download_request.NewLocalBackgroundService(drRepository) + gen := uuid.New() + + drService := download_request.NewLocalService(drRepository, gen, drBackgroundService) + downloadApi := download.New(drService) + downloadApi.SetupDownloadApi(router.internalRouter) +} diff --git a/api/internal/core/entities/Download.go b/api/internal/core/entities/Download.go deleted file mode 100644 index 103437c..0000000 --- a/api/internal/core/entities/Download.go +++ /dev/null @@ -1,8 +0,0 @@ -package entities - -type Download struct { - ID string `json:"id"` - Status string `json:"status"` - Link string `json:"link"` - Data string `json:"data"` -} diff --git a/api/internal/core/entities/download.go b/api/internal/core/entities/download.go new file mode 100644 index 0000000..d65980a --- /dev/null +++ b/api/internal/core/entities/download.go @@ -0,0 +1,28 @@ +package entities + +import ( + "downloader/pkg/common/uuid" + "errors" +) + +type Download struct { + ID string `json:"id"` + Status string `json:"status"` + Link string `json:"link"` + Data string `json:"data"` +} + +func NewDownload(link string, data string) func(uuidGen uuid.Gen) (*Download, error) { + return func(uuidGen uuid.Gen) (*Download, error) { + if link == "" || data == "" { + return nil, errors.New("A field was not valid") + } + + return &Download{ + ID: uuidGen.Create(), + Status: "scheduled", + Link: link, + Data: data, + }, nil + } +} diff --git a/api/internal/core/ports/download_request/download_request.go b/api/internal/core/ports/download_request/download_request.go index cba72de..c0cd802 100644 --- a/api/internal/core/ports/download_request/download_request.go +++ b/api/internal/core/ports/download_request/download_request.go @@ -3,11 +3,16 @@ package download_request import "downloader/internal/core/entities" type Service interface { - Schedule(provider string, link string) (entities.Download, error) - Get(id string) (entities.Download, error) + Schedule(provider string, link string) (*entities.Download, error) + Get(id string) (*entities.Download, error) } type Repository interface { - Create(download entities.Download) (entities.Download, error) - GetById(id string) (entities.Download, error) + Create(download *entities.Download) (*entities.Download, error) + GetById(id string) (*entities.Download, error) + Update(download *entities.Download) error +} + +type BackgroundService interface { + Run(download *entities.Download) error } diff --git a/api/internal/core/ports/download_request/in_memory.go b/api/internal/core/ports/download_request/in_memory.go new file mode 100644 index 0000000..c440376 --- /dev/null +++ b/api/internal/core/ports/download_request/in_memory.go @@ -0,0 +1,110 @@ +package download_request + +import ( + "downloader/internal/core/entities" + "downloader/pkg/common/uuid" + "errors" + "fmt" + "time" +) + +// Repository +type inMemoryRepository struct { + collection map[string]*entities.Download +} + +func NewInMemoryRepository() Repository { + return &inMemoryRepository{collection: make(map[string]*entities.Download)} +} + +func (i *inMemoryRepository) Create(download *entities.Download) (*entities.Download, error) { + if doc := i.collection[download.ID]; doc != nil { + return nil, errors.New("download request already exists") + } + + i.collection[download.ID] = download + + return download, nil +} + +func (i *inMemoryRepository) Update(download *entities.Download) error { + + if doc := i.collection[download.ID]; doc == nil { + return errors.New("download request doesn't exist exists") + } + + i.collection[download.ID] = download + + return nil +} + +func (i *inMemoryRepository) GetById(id string) (*entities.Download, error) { + if download := i.collection[id]; download != nil { + return download, nil + } else { + return nil, errors.New("download was not found in the database") + } +} + +// Service +type localService struct { + repository Repository + uuidGen uuid.Gen + BackgroundService BackgroundService +} + +func NewLocalService(repository Repository, uuidGen uuid.Gen, backgroundService BackgroundService) Service { + return &localService{ + repository: repository, + uuidGen: uuidGen, + BackgroundService: backgroundService, + } +} + +func (l *localService) Schedule(provider string, link string) (*entities.Download, error) { + download, err := entities.NewDownload(link, provider)(l.uuidGen) + if err != nil { + return nil, err + } + + persistedDownloadRequest, uploadErr := l.repository.Create(download) + if uploadErr != nil { + return nil, uploadErr + } + + err = l.BackgroundService.Run(persistedDownloadRequest) + if err != nil { + return nil, err + } + + return persistedDownloadRequest, nil +} + +func (l *localService) Get(id string) (*entities.Download, error) { + return l.repository.GetById(id) +} + +// Background service +type localBackgroundService struct { + repository Repository +} + +func NewLocalBackgroundService(repository Repository) BackgroundService { + return &localBackgroundService{repository: repository} +} + +func (l localBackgroundService) Run(download *entities.Download) error { + go func() { + time.Sleep(time.Second * 5) + download.Status = "done" + err := l.repository.Update(download) + if err != nil { + fmt.Printf("Download request: %s failed\n", download.ID) + panic(err) + } else { + fmt.Printf("Download request: %s done\n", download.ID) + } + }() + + return nil +} diff --git a/api/pkg/common/uuid/uuid.go b/api/pkg/common/uuid/uuid.go new file mode 100644 index 0000000..4d81f71 --- /dev/null +++ b/api/pkg/common/uuid/uuid.go @@ -0,0 +1,18 @@ +package uuid + +import "github.com/google/uuid" + +type Gen interface { + Create() string +} + +type uuidGen struct { +} + +func New() *uuidGen { + return &uuidGen{} +} + +func (u uuidGen) Create() string { + return uuid.New().String() +}