Add base site, needs clean-up
This commit is contained in:
parent
ed4475149a
commit
ec313045f1
@ -6,6 +6,7 @@ require (
|
|||||||
github.com/fatih/color v1.13.0 // indirect
|
github.com/fatih/color v1.13.0 // indirect
|
||||||
github.com/ghodss/yaml v1.0.0 // indirect
|
github.com/ghodss/yaml v1.0.0 // indirect
|
||||||
github.com/go-chi/chi v1.5.4 // indirect
|
github.com/go-chi/chi v1.5.4 // indirect
|
||||||
|
github.com/go-chi/cors v1.2.0 // indirect
|
||||||
github.com/go-chi/render v1.0.1 // indirect
|
github.com/go-chi/render v1.0.1 // indirect
|
||||||
github.com/google/uuid v1.3.0 // indirect
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
@ -7,6 +7,8 @@ github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
|||||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs=
|
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/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg=
|
||||||
|
github.com/go-chi/cors v1.2.0 h1:tV1g1XENQ8ku4Bq3K9ub2AtgG+p16SmzeMSGTwrOKdE=
|
||||||
|
github.com/go-chi/cors v1.2.0/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||||
github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8=
|
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/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 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package download
|
package download
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"downloader/internal/app/api/common/responses"
|
"downloader/internal/app/api/common/responses"
|
||||||
"downloader/internal/core/entities"
|
"downloader/internal/core/entities"
|
||||||
"errors"
|
"errors"
|
||||||
@ -48,6 +49,12 @@ func (a *api) requestDownload(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
func (a *api) getDownloads(writer http.ResponseWriter, request *http.Request) {
|
func (a *api) getDownloads(writer http.ResponseWriter, request *http.Request) {
|
||||||
ctx := request.Context()
|
ctx := request.Context()
|
||||||
|
|
||||||
|
if done := request.URL.Query().Get("done"); done == "true" {
|
||||||
|
_ = a.getDoneDownloads(writer, request, ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
active := request.URL.Query().Get("active") == "true"
|
active := request.URL.Query().Get("active") == "true"
|
||||||
downloads, err := a.drService.GetAll(ctx, active)
|
downloads, err := a.drService.GetAll(ctx, active)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -59,7 +66,20 @@ func (a *api) getDownloads(writer http.ResponseWriter, request *http.Request) {
|
|||||||
_ = render.Render(writer, request, responses.ErrInvalidRequest(err))
|
_ = render.Render(writer, request, responses.ErrInvalidRequest(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *api) getDoneDownloads(writer http.ResponseWriter, request *http.Request, ctx context.Context) bool {
|
||||||
|
downloads, err := a.drService.GetDone(ctx)
|
||||||
|
if err != nil {
|
||||||
|
_ = render.Render(writer, request, responses.ErrInvalidRequest(err))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = render.RenderList(writer, request, newDownloadsResponse(downloads)); err != nil {
|
||||||
|
_ = render.Render(writer, request, responses.ErrInvalidRequest(err))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *api) getDownloadById(w http.ResponseWriter, r *http.Request) {
|
func (a *api) getDownloadById(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -10,13 +10,13 @@ func New() *zap.SugaredLogger {
|
|||||||
consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())
|
consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())
|
||||||
jsonEncoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
|
jsonEncoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
|
||||||
|
|
||||||
consoleDebugging := zapcore.Lock(os.Stdout)
|
|
||||||
lowPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
|
lowPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
|
||||||
return lvl < zapcore.ErrorLevel
|
return lvl < zapcore.ErrorLevel
|
||||||
})
|
})
|
||||||
highPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
|
highPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
|
||||||
return lvl >= zapcore.ErrorLevel
|
return lvl >= zapcore.ErrorLevel
|
||||||
})
|
})
|
||||||
|
consoleDebugging := zapcore.Lock(os.Stdout)
|
||||||
consoleErrors := zapcore.Lock(os.Stderr)
|
consoleErrors := zapcore.Lock(os.Stderr)
|
||||||
core := zapcore.NewTee(
|
core := zapcore.NewTee(
|
||||||
zapcore.NewCore(jsonEncoder, consoleErrors, highPriority),
|
zapcore.NewCore(jsonEncoder, consoleErrors, highPriority),
|
||||||
|
@ -17,6 +17,7 @@ import (
|
|||||||
"downloader/pkg/common/uuid"
|
"downloader/pkg/common/uuid"
|
||||||
"github.com/go-chi/chi"
|
"github.com/go-chi/chi"
|
||||||
"github.com/go-chi/chi/middleware"
|
"github.com/go-chi/chi/middleware"
|
||||||
|
"github.com/go-chi/cors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@ -49,7 +50,14 @@ func (router *router) setupMiddleware() *router {
|
|||||||
router.internalRouter.Use(middleware.RealIP)
|
router.internalRouter.Use(middleware.RealIP)
|
||||||
router.internalRouter.Use(middleware.Recoverer)
|
router.internalRouter.Use(middleware.Recoverer)
|
||||||
router.internalRouter.Use(middleware.Timeout(time.Second * 60))
|
router.internalRouter.Use(middleware.Timeout(time.Second * 60))
|
||||||
|
router.internalRouter.Use(cors.Handler(cors.Options{
|
||||||
|
AllowedOrigins: []string{"https://*", "http://*"},
|
||||||
|
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||||
|
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
|
||||||
|
ExposedHeaders: []string{"Link"},
|
||||||
|
AllowCredentials: false,
|
||||||
|
MaxAge: 300,
|
||||||
|
}))
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,6 +88,6 @@ func setupDownloadRoute(router *router) {
|
|||||||
downloadApi := download.New(drService)
|
downloadApi := download.New(drService)
|
||||||
downloadApi.SetupDownloadApi(router.internalRouter)
|
downloadApi.SetupDownloadApi(router.internalRouter)
|
||||||
|
|
||||||
cleanupJob := cleanup.New(drRepository, newLogger)
|
cleanupJob := cleanup.New(drRepository, newLogger, fileOrchestrator)
|
||||||
cleanupJob.RunOnSchedule()
|
cleanupJob.RunOnSchedule()
|
||||||
}
|
}
|
||||||
|
@ -12,4 +12,5 @@ type Repository interface {
|
|||||||
Get(ctx context.Context, active bool) ([]*entities.Download, error)
|
Get(ctx context.Context, active bool) ([]*entities.Download, error)
|
||||||
GetOldOrStuck(ctx context.Context) ([]*entities.Download, error)
|
GetOldOrStuck(ctx context.Context) ([]*entities.Download, error)
|
||||||
BatchDelete(ctx context.Context, requests []*entities.Download) error
|
BatchDelete(ctx context.Context, requests []*entities.Download) error
|
||||||
|
GetDone(ctx context.Context) ([]*entities.Download, error)
|
||||||
}
|
}
|
||||||
|
@ -80,13 +80,22 @@ func (r repository) Update(ctx context.Context, download *entities.Download) err
|
|||||||
|
|
||||||
func (r repository) Get(ctx context.Context, active bool) ([]*entities.Download, error) {
|
func (r repository) Get(ctx context.Context, active bool) ([]*entities.Download, error) {
|
||||||
var downloads []Download
|
var downloads []Download
|
||||||
err := r.db.NewSelect().
|
query := r.db.NewSelect().
|
||||||
Model(&downloads).
|
Model(&downloads).
|
||||||
Column("id", "status", "link").
|
Column("id", "status", "link")
|
||||||
Where("status LIKE ?", "in-progress%").
|
|
||||||
Limit(20).
|
var err error
|
||||||
|
if active {
|
||||||
|
err = query.
|
||||||
|
Where("status LIKE ? OR status = ?", "in-progress%", "scheduled").
|
||||||
Order("created_at ASC").
|
Order("created_at ASC").
|
||||||
Scan(ctx)
|
Scan(ctx)
|
||||||
|
} else {
|
||||||
|
err = query.
|
||||||
|
Order("created_at ASC").
|
||||||
|
Scan(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -109,7 +118,7 @@ func (r repository) GetOldOrStuck(ctx context.Context) ([]*entities.Download, er
|
|||||||
err := r.db.NewSelect().
|
err := r.db.NewSelect().
|
||||||
Model(&downloads).
|
Model(&downloads).
|
||||||
Column("id", "status", "link").
|
Column("id", "status", "link").
|
||||||
Where("status LIKE ?", "in-progress%").
|
Where("status LIKE ? OR status = ?", "in-progress%", "scheduled").
|
||||||
Where("updated_at < now() - interval '1 minutes'").
|
Where("updated_at < now() - interval '1 minutes'").
|
||||||
Limit(20).
|
Limit(20).
|
||||||
Order("created_at ASC").
|
Order("created_at ASC").
|
||||||
@ -152,3 +161,28 @@ func (r repository) BatchDelete(ctx context.Context, requests []*entities.Downlo
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r repository) GetDone(ctx context.Context) ([]*entities.Download, error) {
|
||||||
|
var downloads []Download
|
||||||
|
err := r.db.NewSelect().
|
||||||
|
Model(&downloads).
|
||||||
|
Column("id", "status", "link").
|
||||||
|
Where("status = ?", "done").
|
||||||
|
Order("created_at ASC").
|
||||||
|
Scan(ctx)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var responseDownloads []*entities.Download
|
||||||
|
for _, download := range downloads {
|
||||||
|
responseDownloads = append(responseDownloads, &entities.Download{
|
||||||
|
ID: download.ID,
|
||||||
|
Status: download.Status,
|
||||||
|
Link: download.Link,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseDownloads, nil
|
||||||
|
}
|
||||||
|
@ -35,5 +35,8 @@ func (l localSourceHandler) Prepare(link string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l localSourceHandler) CleanUp(sourcePath string) error {
|
func (l localSourceHandler) CleanUp(sourcePath string) error {
|
||||||
return os.Remove(sourcePath)
|
if _, err := os.Stat(sourcePath); os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return os.RemoveAll(sourcePath)
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package cleanup
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"downloader/internal/core/ports/download_request"
|
"downloader/internal/core/ports/download_request"
|
||||||
|
"downloader/internal/core/ports/fileorchestrator"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@ -10,10 +11,11 @@ import (
|
|||||||
type cleanUp struct {
|
type cleanUp struct {
|
||||||
repository download_request.Repository
|
repository download_request.Repository
|
||||||
logger *zap.SugaredLogger
|
logger *zap.SugaredLogger
|
||||||
|
fo *fileorchestrator.FileOrchestrator
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(repository download_request.Repository, logger *zap.SugaredLogger) *cleanUp {
|
func New(repository download_request.Repository, logger *zap.SugaredLogger, fo *fileorchestrator.FileOrchestrator) *cleanUp {
|
||||||
return &cleanUp{repository: repository, logger: logger}
|
return &cleanUp{repository: repository, logger: logger, fo: fo}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cleanUp) RunOnSchedule() {
|
func (c *cleanUp) RunOnSchedule() {
|
||||||
@ -21,14 +23,27 @@ func (c *cleanUp) RunOnSchedule() {
|
|||||||
go func() {
|
go func() {
|
||||||
for true {
|
for true {
|
||||||
requests, err := c.repository.GetOldOrStuck(ctx)
|
requests, err := c.repository.GetOldOrStuck(ctx)
|
||||||
if err == nil {
|
if err != nil {
|
||||||
|
c.logger.Warn("could not process old or stuck in-progress jobs")
|
||||||
|
time.Sleep(5 * time.Minute)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
c.logger.Debugw("Cleaning up downloads",
|
c.logger.Debugw("Cleaning up downloads",
|
||||||
"downloads", requests)
|
"downloads", requests)
|
||||||
_ = c.repository.BatchDelete(ctx, requests)
|
|
||||||
} else {
|
for _, request := range requests {
|
||||||
c.logger.Warn("could not process old or stuck in-progress jobs")
|
basePath, err := c.fo.Begin(request.Link)
|
||||||
|
if err != nil {
|
||||||
|
c.logger.Warnw("could not process request",
|
||||||
|
"downloadId", request.ID)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.fo.CleanUp(basePath)
|
||||||
|
}
|
||||||
|
_ = c.repository.BatchDelete(ctx, requests)
|
||||||
|
|
||||||
time.Sleep(time.Minute)
|
time.Sleep(time.Minute)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
@ -56,3 +56,7 @@ func (l *localService) Get(ctx context.Context, id string) (*entities.Download,
|
|||||||
func (l *localService) GetAll(ctx context.Context, active bool) ([]*entities.Download, error) {
|
func (l *localService) GetAll(ctx context.Context, active bool) ([]*entities.Download, error) {
|
||||||
return l.repository.Get(ctx, active)
|
return l.repository.Get(ctx, active)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *localService) GetDone(ctx context.Context) ([]*entities.Download, error) {
|
||||||
|
return l.repository.GetDone(ctx)
|
||||||
|
}
|
||||||
|
@ -9,4 +9,5 @@ type Service interface {
|
|||||||
Schedule(ctx context.Context, link string) (*entities.Download, error)
|
Schedule(ctx context.Context, link string) (*entities.Download, error)
|
||||||
Get(ctx context.Context, id string) (*entities.Download, error)
|
Get(ctx context.Context, id string) (*entities.Download, error)
|
||||||
GetAll(ctx context.Context, active bool) ([]*entities.Download, error)
|
GetAll(ctx context.Context, active bool) ([]*entities.Download, error)
|
||||||
|
GetDone(ctx context.Context) ([]*entities.Download, error)
|
||||||
}
|
}
|
||||||
|
34
client/.gitignore
vendored
Normal file
34
client/.gitignore
vendored
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
5
client/.idea/.gitignore
vendored
Normal file
5
client/.idea/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
12
client/.idea/client.iml
Normal file
12
client/.idea/client.iml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
8
client/.idea/modules.xml
Normal file
8
client/.idea/modules.xml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/client.iml" filepath="$PROJECT_DIR$/.idea/client.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
6
client/.idea/vcs.xml
Normal file
6
client/.idea/vcs.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
27
client/README.md
Normal file
27
client/README.md
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# Next.js + Tailwind CSS Example
|
||||||
|
|
||||||
|
This example shows how to use [Tailwind CSS](https://tailwindcss.com/) [(v3.0)](https://tailwindcss.com/blog/tailwindcss-v3) with Next.js. It follows the steps outlined in the official [Tailwind docs](https://tailwindcss.com/docs/guides/nextjs).
|
||||||
|
|
||||||
|
## Preview
|
||||||
|
|
||||||
|
Preview the example live on [StackBlitz](http://stackblitz.com/):
|
||||||
|
|
||||||
|
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/vercel/next.js/tree/canary/examples/with-tailwindcss)
|
||||||
|
|
||||||
|
## Deploy your own
|
||||||
|
|
||||||
|
Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example):
|
||||||
|
|
||||||
|
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/with-tailwindcss&project-name=with-tailwindcss&repository-name=with-tailwindcss)
|
||||||
|
|
||||||
|
## How to use
|
||||||
|
|
||||||
|
Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx create-next-app --example with-tailwindcss with-tailwindcss-app
|
||||||
|
# or
|
||||||
|
yarn create next-app --example with-tailwindcss with-tailwindcss-app
|
||||||
|
```
|
||||||
|
|
||||||
|
Deploy it to the cloud with [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)).
|
5
client/next-env.d.ts
vendored
Normal file
5
client/next-env.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
21
client/package.json
Normal file
21
client/package.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^0.24.0",
|
||||||
|
"next": "latest",
|
||||||
|
"react": "^17.0.2",
|
||||||
|
"react-dom": "^17.0.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^17.0.38",
|
||||||
|
"autoprefixer": "^10.4.0",
|
||||||
|
"postcss": "^8.4.4",
|
||||||
|
"tailwindcss": "^3.0.0",
|
||||||
|
"typescript": "^4.5.4"
|
||||||
|
}
|
||||||
|
}
|
7
client/pages/_app.tsx
Normal file
7
client/pages/_app.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import '../styles/globals.css'
|
||||||
|
|
||||||
|
function MyApp({ Component, pageProps }) {
|
||||||
|
return <Component {...pageProps} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MyApp
|
186
client/pages/index.tsx
Normal file
186
client/pages/index.tsx
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
import {Fragment, useEffect, useState} from "react";
|
||||||
|
import axios, {AxiosResponse} from 'axios'
|
||||||
|
|
||||||
|
interface DownloadRequestResponse {
|
||||||
|
id: string;
|
||||||
|
status: string;
|
||||||
|
link: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DownloadRequest {
|
||||||
|
link: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const calculateProgressAndAppend = (di: DownloadRequestResponse): { di: DownloadRequestResponse, progress?: number } => {
|
||||||
|
if (!di.status?.match) {
|
||||||
|
return {di}
|
||||||
|
}
|
||||||
|
const matches = di.status.match(/in-progress:\s([\d\.]+)/)
|
||||||
|
if (!matches || matches.length < 2) {
|
||||||
|
return {di}
|
||||||
|
}
|
||||||
|
const progress = matches[1]
|
||||||
|
const progressNum = parseInt(progress)
|
||||||
|
if (typeof (progressNum) !== "number") {
|
||||||
|
console.log(progress)
|
||||||
|
return {di}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {di, progress: progressNum}
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayDownloads = (displayItems: DownloadRequestResponse[]) => {
|
||||||
|
if (displayItems.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!displayItems.map) {
|
||||||
|
console.log(displayItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = displayItems.map(calculateProgressAndAppend)
|
||||||
|
|
||||||
|
const renderListItem = (di: DownloadRequestResponse, progress?: number) => {
|
||||||
|
|
||||||
|
if (typeof (progress) !== 'number') {
|
||||||
|
return <div className="space-y-1">
|
||||||
|
<div>
|
||||||
|
<a href={di.link} target="_blank">{di.link}</a> : {di.status}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p>
|
||||||
|
<a href={di.link} target="_blank">
|
||||||
|
{di.link}
|
||||||
|
</a> : {progress}%
|
||||||
|
</p>
|
||||||
|
<div className="w-full bg-gray-200 rounded-full h-2.5">
|
||||||
|
<div className="bg-orange-500 h-2.5 rounded-full" style={{width: `${progress}%`}}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="flex flex-col max-h-80 overflow-scroll overflow-x-hidden">
|
||||||
|
<ul className="space-y-3">
|
||||||
|
{
|
||||||
|
items
|
||||||
|
.sort((a, b) => a.progress - b.progress)
|
||||||
|
.map(({di, progress}, i) => (
|
||||||
|
<Fragment key={di.id}>
|
||||||
|
{i !== 0 && (<hr className="border-gray-400 border-1 rounded-full mx-8 m-auto"/>)}
|
||||||
|
|
||||||
|
<li>
|
||||||
|
{renderListItem(di, progress)}
|
||||||
|
</li>
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
const [downloadUrl, setDownloadUrl] = useState("")
|
||||||
|
const [downloadingItems, setDownloadingItems] = useState<DownloadRequestResponse[]>([])
|
||||||
|
const [downloadedItems, setDownloadedItems] = useState<DownloadRequestResponse[]>([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getInitialDownloads()
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
getInitialDownloads()
|
||||||
|
}, 3000)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(interval)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
function setMockedDownloadingItems() {
|
||||||
|
setDownloadingItems([
|
||||||
|
{id: "some-id-1", status: "in-progress: 10.3", link: "https://youtube.com"},
|
||||||
|
{id: "some-id-2", status: "in-progress: 0", link: "https://youtube.com"},
|
||||||
|
{id: "some-id-3", status: "in-progress: 100.0", link: "https://youtube.com"},
|
||||||
|
{id: "some-id-4", status: "in-progress: 0", link: "https://youtube.com"},
|
||||||
|
{id: "some-id-5", status: "in-progress: 100.0", link: "https://youtube.com"},
|
||||||
|
{id: "some-id-6", status: "in-progress: 0", link: "https://youtube.com"},
|
||||||
|
{id: "some-id-7", status: "in-progress: 100.0", link: "https://youtube.com"},
|
||||||
|
{id: "some-id-8", status: "in-progress: 0", link: "https://youtube.com"},
|
||||||
|
{id: "some-id-9", status: "in-progress: 100.0", link: "https://youtube.com"},
|
||||||
|
{id: "some-id-10", status: "in-progress: 10.3", link: "https://youtube.com"},
|
||||||
|
{id: "some-id-11", status: "in-progress: 10.3", link: "https://youtube.com"},
|
||||||
|
{id: "some-id-12", status: "in-progress: 10.3", link: "https://youtube.com"},
|
||||||
|
{id: "some-id-13", status: "in-progress: 10.3", link: "https://youtube.com"},
|
||||||
|
{id: "some-id-14", status: "in-progress: 10.3", link: "https://youtube.com"},
|
||||||
|
{id: "some-id-15", status: "in-progress: 10.3", link: "https://youtube.com"},
|
||||||
|
{id: "some-id-16", status: "in-progress: 10.3", link: "https://youtube.com"},
|
||||||
|
{id: "some-id-17", status: "in-progress: 10.3", link: "https://youtube.com"},
|
||||||
|
{id: "some-id-18", status: "in-progress: 10.3", link: "https://youtube.com"},
|
||||||
|
{id: "some-id-19", status: "in-progress: 10.3", link: "https://youtube.com"},
|
||||||
|
{id: "some-id-20", status: "in-progress: 10.3", link: "https://youtube.com"},
|
||||||
|
{id: "some-id-21", status: "in-progress: 10.3", link: "https://youtube.com"},
|
||||||
|
{id: "some-id-22", status: "in-progress: 10.3", link: "https://youtube.com"},
|
||||||
|
{id: "some-id-23", status: "in-progress: 10.3", link: "https://youtube.com"},
|
||||||
|
{id: "some-id-24", status: "scheduled", link: "https://youtube.com"},
|
||||||
|
{id: "some-id-25", status: "done", link: "https://youtube.com"},
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
const getInitialDownloads = () => {
|
||||||
|
axios.get<DownloadRequestResponse[]>("http://localhost:3333/downloads?active=true"
|
||||||
|
).then(res => {
|
||||||
|
setDownloadingItems(res.data)
|
||||||
|
//setMockedDownloadingItems();
|
||||||
|
})
|
||||||
|
|
||||||
|
axios.get<DownloadRequestResponse[]>("http://localhost:3333/downloads?done=true"
|
||||||
|
).then(res => {
|
||||||
|
setDownloadedItems(res.data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const download = (url: string) => {
|
||||||
|
axios.post<DownloadRequest, AxiosResponse<DownloadRequestResponse>>("http://localhost:3333/downloads", {
|
||||||
|
link: url
|
||||||
|
}).then(res => {
|
||||||
|
setDownloadingItems([{
|
||||||
|
id: res.data.id,
|
||||||
|
status: res.data.status,
|
||||||
|
link: res.data.link
|
||||||
|
}, ...downloadingItems.filter(di => di.id !== res.data.id)])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="min-w-full min-h-screen flex justify-center items-center h-screen bg-cover bg-center absolute z-0 bg-[url('https://images.unsplash.com/photo-1539693565400-356293b6df0f?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1470&q=80')]"
|
||||||
|
>
|
||||||
|
<div className="bg-gray-200 p-4 rounded-xl w-full mx-2 sm:mx-10 lg:mx-40 space-y-6">
|
||||||
|
<div className="flex flex-col md:flex-row gap-2">
|
||||||
|
<input
|
||||||
|
className="bg-gray-200 border-2 border-gray-400 focus:border-gray-600 px-4 py-2 flex-1 rounded-xl outline-none appearance-none"
|
||||||
|
placeholder="Your download url"
|
||||||
|
value={downloadUrl} onChange={(e) => setDownloadUrl(e.target.value)}>
|
||||||
|
</input>
|
||||||
|
<button type="button"
|
||||||
|
className="py-2 px-4 bg-orange-600 rounded-xl text-white active:bg-orange-800"
|
||||||
|
onClick={() => {
|
||||||
|
if (downloadUrl) {
|
||||||
|
download(downloadUrl)
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
Download
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{downloadingItems && downloadingItems.length > 0 && displayDownloads(downloadingItems)}
|
||||||
|
{downloadedItems && downloadedItems.length > 0 && displayDownloads(downloadedItems)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
8
client/postcss.config.js
Normal file
8
client/postcss.config.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// If you want to use other PostCSS plugins, see the following:
|
||||||
|
// https://tailwindcss.com/docs/using-with-preprocessors
|
||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
BIN
client/public/favicon.ico
Normal file
BIN
client/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
4
client/public/vercel.svg
Normal file
4
client/public/vercel.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
3
client/styles/globals.css
Normal file
3
client/styles/globals.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
10
client/tailwind.config.js
Normal file
10
client/tailwind.config.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
module.exports = {
|
||||||
|
content: [
|
||||||
|
'./pages/**/*.{js,ts,jsx,tsx}',
|
||||||
|
'./components/**/*.{js,ts,jsx,tsx}',
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
30
client/tsconfig.json
Normal file
30
client/tsconfig.json
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": false,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"incremental": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
2333
client/yarn.lock
Normal file
2333
client/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user