diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 8b0cb23..e8a654c 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -2,10 +2,10 @@ name: Docker Image on: push: - branches: [ "master" ] + branches: [ "master", "develop" ] tags: [ "v*" ] pull_request: - branches: [ "master" ] + branches: [ "master", "develop" ] jobs: @@ -18,14 +18,15 @@ jobs: with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Docker meta - id: meta + - name: Server Meta + id: server_meta uses: docker/metadata-action@v4 with: images: panpaul/woj-server - - name: Build and Push the Docker Image + - name: Build and Push the Server Image uses: docker/build-push-action@v3 with: push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} + tags: ${{ steps.server_meta.outputs.tags }} + file: ./Dockerfile.server + labels: ${{ steps.server_meta.outputs.labels }} diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 757de0e..fd08158 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -2,10 +2,10 @@ name: Lint on: push: - branches: [ "master" ] + branches: [ "master", "develop" ] tags: [ "v*" ] pull_request: - branches: [ "master" ] + branches: [ "master", "develop" ] permissions: contents: read diff --git a/.gitignore b/.gitignore index 2d4c8dc..8826357 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ ### Project +/tmp /server +/runner my.secrets ### JetBrains template diff --git a/Dockerfile.runner b/Dockerfile.runner new file mode 100644 index 0000000..af56a80 --- /dev/null +++ b/Dockerfile.runner @@ -0,0 +1,11 @@ +FROM golang:latest AS builder +WORKDIR /builder +COPY . /builder +RUN make runner + +FROM alpine:latest +WORKDIR /app +RUN apk --no-cache add tzdata ca-certificates libc6-compat +COPY --from=builder /builder/server /app +COPY --from=builder /builder/resource/runner /app/resource/runner +ENTRYPOINT ["/app/runner"] diff --git a/Dockerfile b/Dockerfile.server similarity index 73% rename from Dockerfile rename to Dockerfile.server index 4ddf587..a66ed21 100644 --- a/Dockerfile +++ b/Dockerfile.server @@ -1,12 +1,12 @@ FROM golang:latest AS builder WORKDIR /builder COPY . /builder -RUN make build +RUN make server FROM alpine:latest WORKDIR /app RUN apk --no-cache add tzdata ca-certificates libc6-compat COPY --from=builder /builder/server /app -COPY --from=builder /builder/resource /app/resource +COPY --from=builder /builder/resource/frontend /app/resource/frontend EXPOSE 8000 ENTRYPOINT ["/app/server"] diff --git a/Makefile b/Makefile index a139c2e..af08537 100644 --- a/Makefile +++ b/Makefile @@ -1,31 +1,36 @@ -PROJECT=server - GO := go -LDFLAGS += -X main.BuildTime=$(shell date -u '+%Y-%m-%d-%I-%M-%S') -LDFLAGS += -X main.Version=$(shell cat VERSION)+$(shell git rev-parse HEAD) +LDFLAGS += -X cmd.BuildTime=$(shell date -u '+%Y-%m-%d-%I-%M-%S') +LDFLAGS += -X cmd.Version=$(shell cat VERSION)+$(shell git rev-parse HEAD) LDFLAGS += -s -w -GOBUILD := $(GO) build -o $(PROJECT) -ldflags '$(LDFLAGS)' ./cmd/app +GOBUILD := $(GO) build -ldflags '$(LDFLAGS)' +GOBIN := $(shell go env GOPATH)/bin -.PHONY: all build clean run dep swagger +.PHONY: all server runner build clean dep swagger fmt default: all all: clean build -build: swagger dep - $(GOBUILD) +server: swagger dep + $(GOBUILD) -o server ./cmd/server + +runner: dep + $(GOBUILD) -o runner ./cmd/runner + +build: runner server clean: - rm -f $(PROJECT) - -run: clean swagger dep build - ./$(PROJECT) run + rm -f runner + rm -f server dep: go mod tidy && go mod download swagger: go install github.com/swaggo/swag/cmd/swag@latest - swag init -g internal/router/api.go -o internal/router/docs + $(GOBIN)/swag init -g internal/router/api.go -o internal/router/docs + +fmt: + go fmt ./... diff --git a/cmd/app/main.go b/cmd/app/main.go deleted file mode 100644 index 7089bfa..0000000 --- a/cmd/app/main.go +++ /dev/null @@ -1,69 +0,0 @@ -package main - -import ( - "github.com/WHUPRJ/woj-server/internal/app" - "github.com/WHUPRJ/woj-server/internal/global" - "github.com/urfave/cli/v2" - "log" - "os" - "time" -) - -func main() { - a := &cli.App{ - Name: "OJ", - Compiled: getBuildTime(), - Version: Version, - EnableBashCompletion: true, - Authors: []*cli.Author{ - { - Name: "Paul", - Email: "i@0x7f.app", - }, - }, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "config", - Aliases: []string{"c"}, - Usage: "path to the config file", - Value: "config.yaml", - EnvVars: []string{"APP_CONFIG"}, - }, - }, - Commands: []*cli.Command{ - { - Name: "run", - Aliases: []string{"r"}, - Usage: "start the server", - Action: func(c *cli.Context) error { - g := new(global.Global) - g.Setup(c.String("config")) - defer func() { _ = g.Log.Sync() }() - //g.SetupRedis() - - g.Log.Info("starting server...") - return app.Run(g) - }, - }, - }, - } - - err := a.Run(os.Args) - if err != nil { - log.Fatal(err) - } -} - -var ( - BuildTime = "2022-09-06-01-00-00" - Version = "1.0.0+None" -) - -func getBuildTime() time.Time { - build, err := time.Parse("2006-01-02-15-04-05", BuildTime) - if err != nil { - log.Printf("failed to parse build time: %v", err) - build = time.Now() - } - return build -} diff --git a/cmd/common.go b/cmd/common.go new file mode 100644 index 0000000..377871f --- /dev/null +++ b/cmd/common.go @@ -0,0 +1,75 @@ +package cmd + +import ( + "github.com/WHUPRJ/woj-server/internal/global" + "github.com/urfave/cli/v2" + "log" + "math/rand" + "time" +) + +var App = &cli.App{ + Name: "WOJ", + EnableBashCompletion: true, + Authors: []*cli.Author{ + { + Name: "Paul", + Email: "i@0x7f.app", + }, + { + Name: "cxy004", + Email: "cxy004@qq.com", + }, + { + Name: "wzt", + Email: "w.zhongtao@qq.com", + }, + }, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "config", + Aliases: []string{"c"}, + Usage: "path to the config file", + Value: "config.yaml", + EnvVars: []string{"APP_CONFIG"}, + }, + }, +} + +var ( + BuildTime string + Version string +) + +func init() { + if BuildTime == "" { + BuildTime = "2022-09-06-01-00-00" + } + App.Compiled = getBuildTime() + + if Version == "" { + Version = "0.0.0+None" + } + App.Version = Version +} + +func getBuildTime() time.Time { + build, err := time.Parse("2006-01-02-15-04-05", BuildTime) + if err != nil { + log.Printf("failed to parse build time: %v", err) + build = time.Now() + } + return build +} + +func CommonSetup(c *cli.Context) *global.Global { + rand.Seed(time.Now().Unix()) + + g := new(global.Global) + g.SetupConfig(c.String("config")) + g.SetupZap() + + g.Log.Info("starting...") + + return g +} diff --git a/cmd/runner/main.go b/cmd/runner/main.go new file mode 100644 index 0000000..aa7f406 --- /dev/null +++ b/cmd/runner/main.go @@ -0,0 +1,34 @@ +package main + +import ( + "github.com/WHUPRJ/woj-server/cmd" + "github.com/WHUPRJ/woj-server/internal/app/runner" + "github.com/urfave/cli/v2" + "log" + "os" +) + +func main() { + a := cmd.App + a.Usage = "woj-runner" + a.Commands = []*cli.Command{ + { + Name: "run", + Aliases: []string{"r"}, + Usage: "start the runner", + Action: run, + }, + } + + err := a.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} + +func run(c *cli.Context) error { + g := cmd.CommonSetup(c) + defer func() { _ = g.Log.Sync() }() + + return runner.RunRunner(g) +} diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..4b23945 --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,34 @@ +package main + +import ( + "github.com/WHUPRJ/woj-server/cmd" + "github.com/WHUPRJ/woj-server/internal/app/server" + "github.com/urfave/cli/v2" + "log" + "os" +) + +func main() { + a := cmd.App + a.Usage = "woj-server" + a.Commands = []*cli.Command{ + { + Name: "run", + Aliases: []string{"r"}, + Usage: "start the server", + Action: run, + }, + } + + err := a.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} + +func run(c *cli.Context) error { + g := cmd.CommonSetup(c) + defer func() { _ = g.Log.Sync() }() + + return server.RunServer(g) +} diff --git a/config.yaml b/config.yaml index 1e10658..206536b 100644 --- a/config.yaml +++ b/config.yaml @@ -1,12 +1,33 @@ WebServer: Address: 0.0.0.0 Port: 8000 + JwtSigningKey: 'rq67SdQIRABhHq40' + JwtExpireHour: 12 Redis: Db: 0 + QueueDb: 1 Address: '127.0.0.1:6379' Password: '' +Database: + Host: '127.0.0.1' + Port: 5432 + User: 'dev' + Password: 'password' + Database: 'dev' + Prefix: 'oj_' + MaxOpenConns: 100 + MaxIdleConns: 60 + ConnMaxLifetime: 60 + +Storage: + Endpoint: '127.0.0.1:9000' + UseSSL: false + AccessKey: 'EHd5Zj56QrTivhFI' + SecretKey: 'FUHy4RW1mn0Kbr5pibDZ6R2F9116FZKY' + Bucket: 'woj' + Metrics: Namespace: 'OJ' Subsystem: 'server' diff --git a/go.mod b/go.mod index d50b2c5..b42a8c2 100644 --- a/go.mod +++ b/go.mod @@ -7,13 +7,23 @@ require ( github.com/gin-contrib/pprof v1.4.0 github.com/gin-contrib/zap v0.0.2 github.com/gin-gonic/gin v1.8.1 + github.com/go-redis/redis/v8 v8.11.5 + github.com/golang-jwt/jwt/v4 v4.4.2 + github.com/hibiken/asynq v0.23.0 + github.com/jackc/pgtype v1.11.0 + github.com/minio/minio-go/v7 v7.0.42 github.com/prometheus/client_golang v1.13.0 github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a github.com/swaggo/gin-swagger v1.5.3 github.com/swaggo/swag v1.8.5 github.com/urfave/cli/v2 v2.14.1 go.uber.org/zap v1.23.0 + golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa + golang.org/x/text v0.3.7 gopkg.in/yaml.v3 v3.0.1 + gorm.io/driver/postgres v1.3.9 + gorm.io/gorm v1.23.8 + moul.io/zapgorm2 v1.1.3 ) require ( @@ -23,6 +33,8 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/dustin/go-humanize v1.0.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.6 // indirect @@ -33,28 +45,46 @@ require ( github.com/go-playground/validator/v10 v10.10.0 // indirect github.com/goccy/go-json v0.9.7 // indirect github.com/golang/protobuf v1.5.2 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/jackc/chunkreader/v2 v2.0.1 // indirect + github.com/jackc/pgconn v1.12.1 // indirect + github.com/jackc/pgio v1.0.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgproto3/v2 v2.3.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect + github.com/jackc/pgx/v4 v4.16.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.4 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.15.9 // indirect + github.com/klauspost/cpuid/v2 v2.1.0 // indirect github.com/leodido/go-urn v1.2.1 // indirect github.com/mailru/easyjson v0.7.6 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect + github.com/minio/md5-simd v1.1.2 // indirect + github.com/minio/sha256-simd v1.0.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.0.1 // indirect github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect + github.com/rs/xid v1.4.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/sirupsen/logrus v1.9.0 // indirect + github.com/spf13/cast v1.5.0 // indirect github.com/ugorji/go/codec v1.2.7 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect go.uber.org/atomic v1.7.0 // indirect - go.uber.org/multierr v1.6.0 // indirect - golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect - golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 // indirect - golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect - golang.org/x/text v0.3.7 // indirect + go.uber.org/multierr v1.7.0 // indirect + golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect + golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect + golang.org/x/time v0.0.0-20220922220347-f3bd1da661af // indirect golang.org/x/tools v0.1.10 // indirect google.golang.org/protobuf v1.28.1 // indirect + gopkg.in/ini.v1 v1.66.6 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 26fc6b7..28e388a 100644 --- a/go.sum +++ b/go.sum @@ -35,6 +35,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= @@ -60,17 +62,30 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= +github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g= github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs= @@ -117,10 +132,17 @@ github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0= github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= +github.com/go-redis/redis/v8 v8.11.2/go.mod h1:DLomh7y2e3ggQXQLd1YgmvIfecPJoFl7WU5SOQ/r06M= +github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= +github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM= github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= +github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= +github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -160,6 +182,7 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= @@ -172,12 +195,69 @@ github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hibiken/asynq v0.23.0 h1:kmKkNFgqiXBatC8oz94Mer6uvKoGn4STlIVDV5wnKyE= +github.com/hibiken/asynq v0.23.0/go.mod h1:K70jPVx+CAmmQrXot7Dru0D52EO7ob4BIun3ri5z1Qw= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= +github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= +github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= +github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= +github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= +github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= +github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= +github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= +github.com/jackc/pgconn v1.12.1 h1:rsDFzIpRk7xT4B8FufgpCCeyjdNpKyghZeSefViE5W8= +github.com/jackc/pgconn v1.12.1/go.mod h1:ZkhRC59Llhrq3oSfrikvwQ5NaxYExr6twkdkMLaKono= +github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= +github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= +github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= +github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= +github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.3.0 h1:brH0pCGBDkBW07HWlN/oSBXrmo3WB0UvZd1pIuDcL8Y= +github.com/jackc/pgproto3/v2 v2.3.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= +github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= +github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= +github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= +github.com/jackc/pgtype v1.11.0 h1:u4uiGPz/1hryuXzyaBhSk6dnIyyG2683olG2OV+UUgs= +github.com/jackc/pgtype v1.11.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= +github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= +github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= +github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= +github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= +github.com/jackc/pgx/v4 v4.16.1 h1:JzTglcal01DrghUqt+PmzWsZx/Yh7SC/CTQmSBMTd0Y= +github.com/jackc/pgx/v4 v4.16.1/go.mod h1:SIhx0D5hoADaiXZVyv+3gSm3LCIIINTVO0PficsvWGQ= +github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.4 h1:tHnRBy1i5F2Dh8BAFxqFzxKqqvezXrL2OW1TnX+Mlas= +github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 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/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= @@ -193,7 +273,14 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY= +github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= +github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.1.0 h1:eyi1Ad2aNJMW95zcSbmGg7Cg6cq3ADwLpMAP96d8rF0= +github.com/klauspost/cpuid/v2 v2.1.0/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -201,21 +288,37 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= +github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.42 h1:fP56plNR/Tkw/+Xczw9NL5TGxe5gJDvgd8LidNR3BEI= +github.com/minio/minio-go/v7 v7.0.42/go.mod h1:nCrRzjoSUQh8hgKKtu3Y708OLvRLtuASMg2/nvmbarw= +github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= +github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -226,6 +329,16 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48= +github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U= github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= @@ -265,25 +378,44 @@ github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1 github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY= +github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= +github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -309,30 +441,53 @@ github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsr github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v0.10.0/go.mod h1:VCZuO8V8mFPlL0F5J5GK1rtHV3DrFcQ1R8ryq7FK0aI= go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= -go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec= +go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= +go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY= go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= +golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -367,6 +522,7 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -379,6 +535,7 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -389,10 +546,13 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= @@ -400,8 +560,9 @@ golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -417,13 +578,17 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -431,7 +596,12 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -451,7 +621,9 @@ golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -464,8 +636,11 @@ golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI= +golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 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.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -473,12 +648,15 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/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.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220922220347-f3bd1da661af h1:Yx9k8YCG3dvF87UAn2tu2HQLf2dt/eR1bXxpLMWeH+Y= +golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -487,14 +665,18 @@ golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -502,6 +684,7 @@ golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= @@ -520,10 +703,13 @@ golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= +golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -615,6 +801,12 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= +gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI= +gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -629,6 +821,12 @@ gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/postgres v1.3.9 h1:lWGiVt5CijhQAg0PWB7Od1RNcBw/jS4d2cAScBcSDXg= +gorm.io/driver/postgres v1.3.9/go.mod h1:qw/FeqjxmYqW5dBcYNBsnhQULIApQdk7YuuDPktVi1U= +gorm.io/gorm v1.23.1/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= +gorm.io/gorm v1.23.7/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= +gorm.io/gorm v1.23.8 h1:h8sGJ+biDgBA1AD1Ha9gFCx7h8npU7AsLdlkX0n2TpE= +gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -636,6 +834,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +moul.io/zapgorm2 v1.1.3 h1:PP9224dk0l2f56KE1anr3vcS2HzKV9PusKUE6UT9ncI= +moul.io/zapgorm2 v1.1.3/go.mod h1:HTO6sXgHhQD0s2D9HA4xcnJ+qxFRFwsCUxIeFDnKtq0= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/internal/api/consumer/handler.go b/internal/api/consumer/handler.go new file mode 100644 index 0000000..3c8b840 --- /dev/null +++ b/internal/api/consumer/handler.go @@ -0,0 +1,36 @@ +package consumer + +import ( + "context" + "github.com/WHUPRJ/woj-server/internal/global" + "github.com/WHUPRJ/woj-server/internal/service/problem" + "github.com/WHUPRJ/woj-server/internal/service/status" + "github.com/WHUPRJ/woj-server/internal/service/task" + "github.com/hibiken/asynq" + "go.uber.org/zap" +) + +var _ Handler = (*handler)(nil) + +type Handler interface { + ProblemUpdate(_ context.Context, t *asynq.Task) error + SubmitUpdate(_ context.Context, t *asynq.Task) error +} + +type handler struct { + log *zap.Logger + problemService problem.Service + statusService status.Service + taskService task.Service +} + +func NewConsumer(g *global.Global) Handler { + hnd := &handler{ + log: g.Log, + problemService: problem.NewService(g), + statusService: status.NewService(g), + taskService: task.NewService(g), + } + + return hnd +} diff --git a/internal/api/consumer/problemUpdate.go b/internal/api/consumer/problemUpdate.go new file mode 100644 index 0000000..d5187d0 --- /dev/null +++ b/internal/api/consumer/problemUpdate.go @@ -0,0 +1,40 @@ +package consumer + +import ( + "context" + "encoding/json" + "fmt" + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/model" + "github.com/hibiken/asynq" + "github.com/jackc/pgtype" + "go.uber.org/zap" +) + +func (h *handler) ProblemUpdate(_ context.Context, t *asynq.Task) error { + p := new(model.ProblemUpdatePayload) + if err := json.Unmarshal(t.Payload(), &p); err != nil { + return fmt.Errorf("json.Unmarshal failed: %v: %w", err, asynq.SkipRetry) + } + + if p.Status != e.Success { + h.log.Warn("RunnerError", zap.Any("payload", p)) + return nil + } + + status := h.problemService.UpdateVersion( + p.ProblemVersionID, + map[string]interface{}{ + "Context": pgtype.JSON{ + Bytes: []byte(p.Context), + Status: pgtype.Present, + }, + "IsEnabled": true, + }, + ) + + if status != e.Success { + return fmt.Errorf(status.String()) + } + return nil +} diff --git a/internal/api/consumer/submitUpdate.go b/internal/api/consumer/submitUpdate.go new file mode 100644 index 0000000..8b9c64b --- /dev/null +++ b/internal/api/consumer/submitUpdate.go @@ -0,0 +1,37 @@ +package consumer + +import ( + "context" + "encoding/json" + "fmt" + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/model" + "github.com/WHUPRJ/woj-server/internal/service/status" + "github.com/hibiken/asynq" + "go.uber.org/zap" +) + +func (h *handler) SubmitUpdate(_ context.Context, t *asynq.Task) error { + p := new(model.SubmitUpdatePayload) + if err := json.Unmarshal(t.Payload(), &p); err != nil { + return fmt.Errorf("json.Unmarshal failed: %v: %w", err, asynq.SkipRetry) + } + + if p.Status != e.Success { + h.log.Warn("RunnerError", zap.Any("payload", p)) + return nil + } + + createData := &status.CreateData{ + SubmissionID: p.SubmissionID, + ProblemVersionID: p.ProblemVersionID, + Context: p.Context, + Point: p.Point, + } + _, eStatus := h.statusService.Create(createData) + + if eStatus != e.Success { + return fmt.Errorf(eStatus.String()) + } + return nil +} diff --git a/internal/controller/debug/handler.go b/internal/api/debug/handler.go similarity index 100% rename from internal/controller/debug/handler.go rename to internal/api/debug/handler.go diff --git a/internal/controller/debug/random.go b/internal/api/debug/random.go similarity index 96% rename from internal/controller/debug/random.go rename to internal/api/debug/random.go index 96dd6a5..25b839f 100644 --- a/internal/controller/debug/random.go +++ b/internal/api/debug/random.go @@ -7,7 +7,7 @@ import ( "go.uber.org/zap" ) -// randomString godoc +// randomString // @Summary random string // @Description generate random string with length = 32 // @Tags debug diff --git a/internal/api/problem/createVersion.go b/internal/api/problem/createVersion.go new file mode 100644 index 0000000..2456abc --- /dev/null +++ b/internal/api/problem/createVersion.go @@ -0,0 +1,67 @@ +package problem + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/global" + "github.com/WHUPRJ/woj-server/internal/model" + "github.com/WHUPRJ/woj-server/internal/service/problem" + "github.com/gin-gonic/gin" +) + +type createVersionRequest struct { + ProblemID uint `form:"pid" binding:"required"` + StorageKey string `form:"storage_key" binding:"required"` +} + +// CreateVersion +// @Summary create a problem version +// @Description create a problem version +// @Accept application/x-www-form-urlencoded +// @Produce json +// @Param pid formData int true "problem id" +// @Param storage_key formData string true "storage key" +// @Response 200 {object} e.Response "" +// @Security Authentication +// @Router /v1/problem/create_version [post] +func (h *handler) CreateVersion(c *gin.Context) { + claim, exist := c.Get("claim") + if !exist { + e.Pong(c, e.UserUnauthenticated, nil) + return + } + + // uid := claim.(*global.Claim).UID + + role := claim.(*global.Claim).Role + req := new(createVersionRequest) + + if err := c.ShouldBind(req); err != nil { + e.Pong(c, e.InvalidParameter, err.Error()) + return + } + + // guest can not submit + if role < model.RoleAdmin { + e.Pong(c, e.UserUnauthorized, nil) + return + } + + // TODO: check pid exist + + createVersionData := &problem.CreateVersionData{ + ProblemID: req.ProblemID, + StorageKey: req.StorageKey, + } + pv, status := h.problemService.CreateVersion(createVersionData) + if status != e.Success { + e.Pong(c, status, nil) + return + } + + payload := &model.ProblemBuildPayload{ + ProblemVersionID: pv.ID, + StorageKey: pv.StorageKey, + } + _, status = h.taskService.ProblemBuild(payload) + e.Pong(c, status, nil) +} diff --git a/internal/api/problem/details.go b/internal/api/problem/details.go new file mode 100644 index 0000000..3df21e6 --- /dev/null +++ b/internal/api/problem/details.go @@ -0,0 +1,47 @@ +package problem + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/global" + "github.com/WHUPRJ/woj-server/internal/model" + "github.com/gin-gonic/gin" +) + +type detailsRequest struct { + Pid uint `form:"pid"` +} + +// Details +// @Summary get details of a problem +// @Description get details of a problem +// @Accept application/x-www-form-urlencoded +// @Produce json +// @Param pid formData int true "problem id" +// @Response 200 {object} e.Response "problem details" +// @Router /v1/problem/details [post] +func (h *handler) Details(c *gin.Context) { + req := new(detailsRequest) + + if err := c.ShouldBind(req); err != nil { + e.Pong(c, e.InvalidParameter, err.Error()) + return + } + + claim, exist := c.Get("claim") + shouldEnable := !exist || claim.(*global.Claim).Role < model.RoleAdmin + + p, status := h.problemService.Query(req.Pid, true, shouldEnable) + if status != e.Success { + e.Pong(c, status, nil) + return + } + + pv, status := h.problemService.QueryLatestVersion(req.Pid) + if status != e.Success { + e.Pong(c, status, nil) + } + e.Pong(c, e.Success, gin.H{ + "problem": p, + "context": pv.Context.Get(), + }) +} diff --git a/internal/api/problem/handler.go b/internal/api/problem/handler.go new file mode 100644 index 0000000..53bdda2 --- /dev/null +++ b/internal/api/problem/handler.go @@ -0,0 +1,43 @@ +package problem + +import ( + "github.com/WHUPRJ/woj-server/internal/global" + "github.com/WHUPRJ/woj-server/internal/service/problem" + "github.com/WHUPRJ/woj-server/internal/service/storage" + "github.com/WHUPRJ/woj-server/internal/service/task" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +var _ Handler = (*handler)(nil) + +type Handler interface { + Details(c *gin.Context) + Search(c *gin.Context) + Update(c *gin.Context) + Upload(c *gin.Context) +} + +type handler struct { + log *zap.Logger + jwtService global.JwtService + problemService problem.Service + taskService task.Service + storageService storage.Service +} + +func RouteRegister(g *global.Global, group *gin.RouterGroup) { + app := &handler{ + log: g.Log, + jwtService: g.Jwt, + problemService: problem.NewService(g), + taskService: task.NewService(g), + storageService: storage.NewService(g), + } + + group.POST("/search", app.Search) + group.POST("/details", app.jwtService.Handler(false), app.Details) + group.POST("/update", app.jwtService.Handler(true), app.Update) + group.POST("/upload", app.jwtService.Handler(true), app.Upload) + group.POST("/create_version", app.jwtService.Handler(true), app.CreateVersion) +} diff --git a/internal/api/problem/search.go b/internal/api/problem/search.go new file mode 100644 index 0000000..9baeab1 --- /dev/null +++ b/internal/api/problem/search.go @@ -0,0 +1,39 @@ +package problem + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/gin-gonic/gin" +) + +type searchRequest struct { + Search string `form:"search"` +} + +// Search +// @Summary get detail of a problem +// @Description get detail of a problem +// @Accept application/x-www-form-urlencoded +// @Produce json +// @Param search formData string false "word search" +// @Response 200 {object} e.Response "problemset" +// @Router /v1/problem/search [post] +func (h *handler) Search(c *gin.Context) { + req := new(searchRequest) + + if err := c.ShouldBind(req); err != nil { + e.Pong(c, e.InvalidParameter, err.Error()) + return + } + + // TODO: pagination + if req.Search == "" { + // TODO: query without LIKE + problems, status := h.problemService.QueryFuzz(req.Search, true, true) + e.Pong(c, status, problems) + return + } else { + problems, status := h.problemService.QueryFuzz(req.Search, true, true) + e.Pong(c, status, problems) + return + } +} diff --git a/internal/api/problem/update.go b/internal/api/problem/update.go new file mode 100644 index 0000000..6905b16 --- /dev/null +++ b/internal/api/problem/update.go @@ -0,0 +1,79 @@ +package problem + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/global" + "github.com/WHUPRJ/woj-server/internal/model" + "github.com/WHUPRJ/woj-server/internal/service/problem" + "github.com/gin-gonic/gin" +) + +type updateRequest struct { + Pid uint `form:"pid"` + Title string `form:"title" binding:"required"` + Statement string `form:"statement" binding:"required"` + IsEnabled bool `form:"is_enabled"` +} + +// Update +// @Summary create or update a problem +// @Description create or update a problem +// @Accept application/x-www-form-urlencoded +// @Produce json +// @Param pid formData int false "problem id, 0 for create" +// @Param title formData string true "title" +// @Param statement formData string true "statement" +// @Param is_enabled formData bool false "is enabled" +// @Response 200 {object} e.Response "problem info without provider information" +// @Security Authentication +// @Router /v1/problem/update [post] +func (h *handler) Update(c *gin.Context) { + claim, exist := c.Get("claim") + if !exist { + e.Pong(c, e.UserUnauthenticated, nil) + return + } + + uid := claim.(*global.Claim).UID + role := claim.(*global.Claim).Role + if role < model.RoleAdmin { + e.Pong(c, e.UserUnauthorized, nil) + return + } + + req := new(updateRequest) + if err := c.ShouldBind(req); err != nil { + e.Pong(c, e.InvalidParameter, err.Error()) + return + } + + if req.Pid == 0 { + createData := &problem.CreateData{ + Title: req.Title, + Statement: req.Statement, + ProviderID: uid, + IsEnabled: false, + } + p, status := h.problemService.Create(createData) + e.Pong(c, status, p) + return + } else { + p, status := h.problemService.Query(req.Pid, true, false) + if status != e.Success { + e.Pong(c, status, nil) + return + } + if p.ProviderID != uid { + e.Pong(c, e.UserUnauthorized, nil) + return + } + + p.Title = req.Title + p.Statement = req.Statement + p.IsEnabled = req.IsEnabled + + p, status = h.problemService.Update(p) + e.Pong(c, status, p) + return + } +} diff --git a/internal/api/problem/upload.go b/internal/api/problem/upload.go new file mode 100644 index 0000000..a83edc3 --- /dev/null +++ b/internal/api/problem/upload.go @@ -0,0 +1,44 @@ +package problem + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/global" + "github.com/WHUPRJ/woj-server/internal/model" + "github.com/WHUPRJ/woj-server/pkg/utils" + "github.com/gin-gonic/gin" + "time" +) + +// Upload +// @Summary get upload url +// @Description get upload url +// @Produce json +// @Response 200 {object} e.Response "upload url and key" +// @Security Authentication +// @Router /v1/problem/upload [post] +func (h *handler) Upload(c *gin.Context) { + claim, exist := c.Get("claim") + if !exist { + e.Pong(c, e.UserUnauthenticated, nil) + return + } + + role := claim.(*global.Claim).Role + if role < model.RoleAdmin { + e.Pong(c, e.UserUnauthorized, nil) + return + } + + key := utils.RandomString(16) + url, status := h.storageService.Upload(key, time.Second*60*60) + + if status != e.Success { + e.Pong(c, status, nil) + return + } + + e.Pong(c, e.Success, gin.H{ + "key": key, + "url": url, + }) +} diff --git a/internal/api/runner/build.go b/internal/api/runner/build.go new file mode 100644 index 0000000..58d9463 --- /dev/null +++ b/internal/api/runner/build.go @@ -0,0 +1,50 @@ +package runner + +import ( + "context" + "encoding/json" + "fmt" + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/model" + "github.com/hibiken/asynq" + "go.uber.org/zap" + "time" +) + +func (h *handler) Build(_ context.Context, t *asynq.Task) error { + var p model.ProblemBuildPayload + if err := json.Unmarshal(t.Payload(), &p); err != nil { + return fmt.Errorf("json.Unmarshal failed: %v: %w", err, asynq.SkipRetry) + } + + h.log.Info("build", zap.Any("payload", p)) + + status, ctx := func() (e.Status, string) { + url, status := h.storageService.Get(p.StorageKey, time.Second*60*5) + if status != e.Success { + return e.InternalError, "{}" + } + + config, status := h.runnerService.NewProblem(p.ProblemVersionID, url, true) + if status != e.Success { + return e.InternalError, "{}" + } + + for i := range config.Languages { + config.Languages[i].Type = "" + config.Languages[i].Script = "" + config.Languages[i].Cmp = "" + } + + b, _ := json.Marshal(config) + return e.Success, string(b) + }() + + h.taskService.ProblemUpdate(&model.ProblemUpdatePayload{ + Status: status, + ProblemVersionID: p.ProblemVersionID, + Context: ctx, + }) + + return nil +} diff --git a/internal/api/runner/handler.go b/internal/api/runner/handler.go new file mode 100644 index 0000000..ad89428 --- /dev/null +++ b/internal/api/runner/handler.go @@ -0,0 +1,44 @@ +package runner + +import ( + "context" + "errors" + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/global" + "github.com/WHUPRJ/woj-server/internal/service/runner" + "github.com/WHUPRJ/woj-server/internal/service/storage" + "github.com/WHUPRJ/woj-server/internal/service/task" + "github.com/hibiken/asynq" + "go.uber.org/zap" +) + +var _ Handler = (*handler)(nil) + +type Handler interface { + Build(_ context.Context, t *asynq.Task) error + Judge(_ context.Context, t *asynq.Task) error +} + +type handler struct { + log *zap.Logger + runnerService runner.Service + taskService task.Service + storageService storage.Service +} + +func NewRunner(g *global.Global) (Handler, error) { + hnd := &handler{ + log: g.Log, + runnerService: runner.NewService(g), + taskService: task.NewService(g), + storageService: storage.NewService(g), + } + + status := hnd.runnerService.EnsureDeps(false) + if status != e.Success { + g.Log.Error("failed to ensure runner dependencies", zap.String("status", status.String())) + return nil, errors.New("failed to ensure dependencies") + } + + return hnd, nil +} diff --git a/internal/api/runner/judge.go b/internal/api/runner/judge.go new file mode 100644 index 0000000..1a8e563 --- /dev/null +++ b/internal/api/runner/judge.go @@ -0,0 +1,77 @@ +package runner + +import ( + "context" + "encoding/json" + "fmt" + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/model" + "github.com/WHUPRJ/woj-server/internal/service/runner" + "github.com/WHUPRJ/woj-server/pkg/utils" + "github.com/hibiken/asynq" + "go.uber.org/zap" + "path/filepath" + "time" +) + +func (h *handler) Judge(_ context.Context, t *asynq.Task) error { + var p model.SubmitJudgePayload + if err := json.Unmarshal(t.Payload(), &p); err != nil { + return fmt.Errorf("json.Unmarshal failed: %v: %w", err, asynq.SkipRetry) + } + + user := utils.RandomString(16) + h.log.Info("judge", zap.Any("payload", p), zap.String("user", user)) + + status, point, ctx := func() (e.Status, int32, runner.JudgeStatus) { + systemError := runner.JudgeStatus{Message: "System Error"} + + // 1. write user code + userCode := filepath.Join(runner.UserDir, user, fmt.Sprintf("%s.%s", user, p.Submission.Language)) + if !utils.FileTouch(userCode) { + return e.InternalError, 0, systemError + } + err := utils.FileWrite(userCode, []byte(p.Submission.Code)) + if err != nil { + return e.InternalError, 0, systemError + } + + // 2. check problem + if !h.runnerService.ProblemExists(p.ProblemVersionID) { + url, status := h.storageService.Get(p.StorageKey, time.Second*60*5) + if status != e.Success { + return e.InternalError, 0, systemError + } + + _, status = h.runnerService.NewProblem(p.ProblemVersionID, url, false) + if status != e.Success { + return e.InternalError, 0, systemError + } + } + + // 3. compile + compileResult, status := h.runnerService.Compile(p.ProblemVersionID, user, p.Submission.Language) + if status != e.Success { + return e.Success, 0, compileResult + } + + // 4. config + config, err := h.runnerService.ParseConfig(p.ProblemVersionID, true) + if err != nil { + return e.InternalError, 0, systemError + } + + // 5. run and judge + result, point, status := h.runnerService.RunAndJudge(p.ProblemVersionID, user, p.Submission.Language, &config) + return utils.If(status != e.Success, e.InternalError, e.Success).(e.Status), point, result + }() + + h.taskService.SubmitUpdate(&model.SubmitUpdatePayload{ + Status: status, + SubmissionID: p.Submission.ID, + ProblemVersionID: p.ProblemVersionID, + Point: point, + }, ctx) + + return nil +} diff --git a/internal/api/status/handler.go b/internal/api/status/handler.go new file mode 100644 index 0000000..1b85a77 --- /dev/null +++ b/internal/api/status/handler.go @@ -0,0 +1,32 @@ +package status + +import ( + "github.com/WHUPRJ/woj-server/internal/global" + "github.com/WHUPRJ/woj-server/internal/service/status" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +var _ Handler = (*handler)(nil) + +type Handler interface { + Query(c *gin.Context) + QueryByProblemVersion(c *gin.Context) +} + +type handler struct { + log *zap.Logger + statusService status.Service + jwtService global.JwtService +} + +func RouteRegister(g *global.Global, group *gin.RouterGroup) { + app := &handler{ + log: g.Log, + statusService: status.NewService(g), + jwtService: g.Jwt, + } + + group.POST("/query", app.Query) + group.POST("/query/problem_version", app.jwtService.Handler(true), app.QueryByProblemVersion) +} diff --git a/internal/api/status/query.go b/internal/api/status/query.go new file mode 100644 index 0000000..13faf0f --- /dev/null +++ b/internal/api/status/query.go @@ -0,0 +1,31 @@ +package status + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/gin-gonic/gin" +) + +type queryRequest struct { + SubmissionID uint `form:"sid" binding:"required"` +} + +// Query +// @Summary query submissions by via submission id +// @Description query submissions by via submission id +// @Accept application/x-www-form-urlencoded +// @Produce json +// @Param sid formData uint true "submission id" +// @Response 200 {object} e.Response "model.status" +// @Router /v1/status/query [post] +func (h *handler) Query(c *gin.Context) { + req := new(queryRequest) + + if err := c.ShouldBind(req); err != nil { + e.Pong(c, e.InvalidParameter, err.Error()) + return + } + + status, eStatus := h.statusService.Query(req.SubmissionID, true) + + e.Pong(c, eStatus, status) +} diff --git a/internal/api/status/queryByVersion.go b/internal/api/status/queryByVersion.go new file mode 100644 index 0000000..3a738c0 --- /dev/null +++ b/internal/api/status/queryByVersion.go @@ -0,0 +1,51 @@ +package status + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/global" + "github.com/WHUPRJ/woj-server/internal/model" + "github.com/gin-gonic/gin" +) + +type queryByVersionRequest struct { + ProblemVersionID uint `form:"pvid" binding:"required"` + Offset int `form:"offset"` + Limit int `form:"limit" binding:"required"` +} + +// QueryByProblemVersion +// @Summary query submissions by problem version (admin only) +// @Description query submissions by problem version (admin only) +// @Accept application/x-www-form-urlencoded +// @Produce json +// @Param pvid formData uint true "problem version id" +// @Param offset formData int true "start position" +// @Param limit formData int true "limit number of records" +// @Response 200 {object} e.Response "[]*model.status" +// @Router /v1/status/query/problem_version [post] +func (h *handler) QueryByProblemVersion(c *gin.Context) { + + claim, exist := c.Get("claim") + if !exist { + e.Pong(c, e.UserUnauthenticated, nil) + return + } + + role := claim.(*global.Claim).Role + + req := new(queryByVersionRequest) + + if err := c.ShouldBind(req); err != nil { + e.Pong(c, e.InvalidParameter, err.Error()) + return + } + + if role < model.RoleAdmin { + e.Pong(c, e.UserUnauthorized, nil) + return + } + + statuses, eStatus := h.statusService.QueryByVersion(req.ProblemVersionID, req.Offset, req.Limit) + + e.Pong(c, eStatus, statuses) +} diff --git a/internal/api/submission/create.go b/internal/api/submission/create.go new file mode 100644 index 0000000..5743285 --- /dev/null +++ b/internal/api/submission/create.go @@ -0,0 +1,77 @@ +package submission + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/global" + "github.com/WHUPRJ/woj-server/internal/model" + "github.com/WHUPRJ/woj-server/internal/service/submission" + "github.com/gin-gonic/gin" +) + +type createRequest struct { + Pid uint `form:"pid" binding:"required"` + Language string `form:"language" binding:"required"` + Code string `form:"code" binding:"required"` +} + +// Create +// @Summary create a submission +// @Description create a submission +// @Accept application/x-www-form-urlencoded +// @Produce json +// @Param pid formData int true "problem id" +// @Param language formData string true "language" +// @Param code formData string true "code" +// @Response 200 {object} e.Response "" +// @Security Authentication +// @Router /v1/submission/create [post] +func (h *handler) Create(c *gin.Context) { + claim, exist := c.Get("claim") + if !exist { + e.Pong(c, e.UserUnauthenticated, nil) + return + } + + uid := claim.(*global.Claim).UID + + role := claim.(*global.Claim).Role + req := new(createRequest) + + if err := c.ShouldBind(req); err != nil { + e.Pong(c, e.InvalidParameter, err.Error()) + return + } + + // guest can not submit + if role < model.RoleGeneral { + e.Pong(c, e.UserUnauthorized, nil) + return + } + + createData := &submission.CreateData{ + ProblemID: req.Pid, + UserID: uid, + Language: req.Language, + Code: req.Code, + } + s, status := h.submissionService.Create(createData) + if status != e.Success { + e.Pong(c, status, nil) + return + } + + pv, status := h.problemService.QueryLatestVersion(req.Pid) + if status != e.Success { + e.Pong(c, status, nil) + return + } + + payload := &model.SubmitJudgePayload{ + ProblemVersionID: pv.ID, + StorageKey: pv.StorageKey, + Submission: *s, + } + _, status = h.taskService.SubmitJudge(payload) + + e.Pong(c, status, nil) +} diff --git a/internal/api/submission/handler.go b/internal/api/submission/handler.go new file mode 100644 index 0000000..a23aff5 --- /dev/null +++ b/internal/api/submission/handler.go @@ -0,0 +1,43 @@ +package submission + +import ( + "github.com/WHUPRJ/woj-server/internal/global" + "github.com/WHUPRJ/woj-server/internal/service/problem" + "github.com/WHUPRJ/woj-server/internal/service/status" + "github.com/WHUPRJ/woj-server/internal/service/submission" + "github.com/WHUPRJ/woj-server/internal/service/task" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +var _ Handler = (*handler)(nil) + +type Handler interface { + Create(c *gin.Context) + Query(c *gin.Context) + Rejudge(c *gin.Context) +} + +type handler struct { + log *zap.Logger + jwtService global.JwtService + problemService problem.Service + statusService status.Service + submissionService submission.Service + taskService task.Service +} + +func RouteRegister(g *global.Global, group *gin.RouterGroup) { + app := &handler{ + log: g.Log, + jwtService: g.Jwt, + problemService: problem.NewService(g), + statusService: status.NewService(g), + submissionService: submission.NewService(g), + taskService: task.NewService(g), + } + + group.POST("/create", app.jwtService.Handler(true), app.Create) + group.POST("/query", app.Query) + group.POST("/rejudge", app.jwtService.Handler(true), app.Rejudge) +} diff --git a/internal/api/submission/query.go b/internal/api/submission/query.go new file mode 100644 index 0000000..e3e6126 --- /dev/null +++ b/internal/api/submission/query.go @@ -0,0 +1,71 @@ +package submission + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/model" + "github.com/gin-gonic/gin" +) + +type queryRequest struct { + Pid uint `form:"pid"` + Uid uint `form:"uid"` + Offset int `form:"offset"` + Limit int `form:"limit" binding:"required"` +} + +type queryResponse struct { + Submission model.Submission `json:"submission"` + Point int32 `json:"point"` +} + +// Query +// @Summary Query submissions +// @Description Query submissions +// @Accept application/x-www-form-urlencoded +// @Produce json +// @Param pid formData uint true "problem id" +// @Param uid formData uint true "user id" +// @Param offset formData int true "start position" +// @Param limit formData int true "limit number of records" +// @Response 200 {object} e.Response "queryResponse" +// @Router /v1/submission/query [post] + +func (h *handler) Query(c *gin.Context) { + req := new(queryRequest) + + if err := c.ShouldBind(req); err != nil { + e.Pong(c, e.InvalidParameter, err.Error()) + return + } + + if req.Pid == 0 && req.Uid == 0 { + e.Pong(c, e.InvalidParameter, nil) + return + } + + submissions, status := h.submissionService.Query(req.Pid, req.Uid, req.Offset, req.Limit) + + var response []*queryResponse + + for _, submission := range submissions { + currentStatus, _ := h.statusService.Query(submission.ID, false) + var currentPoint int32 + + if currentStatus == nil { + currentPoint = -1 + } else { + currentPoint = currentStatus.Point + } + + newResponse := &queryResponse{ + Submission: *submission, + Point: currentPoint, + } + + newResponse.Submission.Code = "" + + response = append(response, newResponse) + } + + e.Pong(c, status, response) +} diff --git a/internal/api/submission/rejudge.go b/internal/api/submission/rejudge.go new file mode 100644 index 0000000..7d87250 --- /dev/null +++ b/internal/api/submission/rejudge.go @@ -0,0 +1,63 @@ +package submission + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/global" + "github.com/WHUPRJ/woj-server/internal/model" + "github.com/gin-gonic/gin" +) + +type rejudgeRequest struct { + Sid uint `form:"sid" binding:"required"` +} + +// Rejudge +// @Summary rejudge a submission +// @Description rejudge a submission +// @Accept application/x-www-form-urlencoded +// @Produce json +// @Param sid formData int true "submission id" +// @Response 200 {object} e.Response "" +// @Security Authentication +// @Router /v1/submission/rejudge [post] +func (h *handler) Rejudge(c *gin.Context) { + claim, exist := c.Get("claim") + if !exist { + e.Pong(c, e.UserUnauthenticated, nil) + return + } + + role := claim.(*global.Claim).Role + req := new(rejudgeRequest) + + if err := c.ShouldBind(req); err != nil { + e.Pong(c, e.InvalidParameter, err.Error()) + return + } + + // only admin can rejudge + if role < model.RoleAdmin { + e.Pong(c, e.UserUnauthorized, nil) + return + } + + s, status := h.submissionService.QueryBySid(req.Sid, false) + if status != e.Success { + e.Pong(c, status, nil) + return + } + + pv, status := h.problemService.QueryLatestVersion(s.ProblemID) + if status != e.Success { + e.Pong(c, status, nil) + return + } + + _, status = h.taskService.SubmitJudge(&model.SubmitJudgePayload{ + ProblemVersionID: pv.ID, + StorageKey: pv.StorageKey, + Submission: *s, + }) + + e.Pong(c, status, nil) +} diff --git a/internal/api/user/create.go b/internal/api/user/create.go new file mode 100644 index 0000000..ba224e1 --- /dev/null +++ b/internal/api/user/create.go @@ -0,0 +1,58 @@ +package user + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/global" + "github.com/WHUPRJ/woj-server/internal/service/user" + "github.com/gin-gonic/gin" +) + +type createRequest struct { + UserName string `form:"username" binding:"required"` + Password string `form:"password" binding:"required"` + NickName string `form:"nickname" binding:"required"` +} + +// Create +// @Summary create a new user +// @Description create a new user +// @Accept application/x-www-form-urlencoded +// @Produce json +// @Param username formData string true "username" +// @Param nickname formData string true "nickname" +// @Param password formData string true "password" +// @Response 200 {object} e.Response "jwt token" +// @Router /v1/user/create [post] +func (h *handler) Create(c *gin.Context) { + req := new(createRequest) + + if err := c.ShouldBind(req); err != nil { + e.Pong(c, e.InvalidParameter, err.Error()) + return + } + + createData := &user.CreateData{ + UserName: req.UserName, + Password: req.Password, + NickName: req.NickName, + } + u, status := h.userService.Create(createData) + if status != e.Success { + e.Pong(c, status, nil) + return + } + + version, status := h.userService.IncrVersion(u.ID) + if status != e.Success { + e.Pong(c, status, nil) + return + } + + claim := &global.Claim{ + UID: u.ID, + Role: u.Role, + Version: version, + } + token, status := h.jwtService.SignClaim(claim) + e.Pong(c, status, token) +} diff --git a/internal/api/user/handler.go b/internal/api/user/handler.go new file mode 100644 index 0000000..70785b1 --- /dev/null +++ b/internal/api/user/handler.go @@ -0,0 +1,36 @@ +package user + +import ( + "github.com/WHUPRJ/woj-server/internal/global" + "github.com/WHUPRJ/woj-server/internal/service/user" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +var _ Handler = (*handler)(nil) + +type Handler interface { + Create(c *gin.Context) + Login(c *gin.Context) + Logout(c *gin.Context) + Profile(c *gin.Context) +} + +type handler struct { + log *zap.Logger + jwtService global.JwtService + userService user.Service +} + +func RouteRegister(g *global.Global, group *gin.RouterGroup) { + app := &handler{ + log: g.Log, + jwtService: g.Jwt, + userService: user.NewService(g), + } + + group.POST("/create", app.Create) + group.POST("/login", app.Login) + group.POST("/logout", app.jwtService.Handler(true), app.Logout) + group.POST("/profile", app.jwtService.Handler(true), app.Profile) +} diff --git a/internal/api/user/login.go b/internal/api/user/login.go new file mode 100644 index 0000000..fdd3243 --- /dev/null +++ b/internal/api/user/login.go @@ -0,0 +1,59 @@ +package user + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/global" + "github.com/WHUPRJ/woj-server/internal/service/user" + "github.com/gin-gonic/gin" +) + +type loginRequest struct { + UserName string `form:"username" binding:"required"` + Password string `form:"password" binding:"required"` +} + +// Login +// @Summary login +// @Description login and return token +// @Accept application/x-www-form-urlencoded +// @Produce json +// @Param username formData string true "username" +// @Param password formData string true "password" +// @Response 200 {object} e.Response "jwt token and user nickname" +// @Router /v1/user/login [post] +func (h *handler) Login(c *gin.Context) { + req := new(loginRequest) + + if err := c.ShouldBind(req); err != nil { + e.Pong(c, e.InvalidParameter, nil) + return + } + + // check password + loginData := &user.LoginData{ + UserName: req.UserName, + Password: req.Password, + } + u, status := h.userService.Login(loginData) + if status != e.Success { + e.Pong(c, status, nil) + return + } + + // sign and return token + version, status := h.userService.IncrVersion(u.ID) + if status != e.Success { + e.Pong(c, status, nil) + return + } + claim := &global.Claim{ + UID: u.ID, + Role: u.Role, + Version: version, + } + token, status := h.jwtService.SignClaim(claim) + e.Pong(c, status, gin.H{ + "token": token, + "nickname": u.NickName, + }) +} diff --git a/internal/api/user/logout.go b/internal/api/user/logout.go new file mode 100644 index 0000000..f9a288c --- /dev/null +++ b/internal/api/user/logout.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/global" + "github.com/gin-gonic/gin" +) + +// Logout +// @Summary logout +// @Description logout +// @Accept application/x-www-form-urlencoded +// @Produce json +// @Response 200 {object} e.Response "nil" +// @Security Authentication +// @Router /v1/user/logout [post] +func (h *handler) Logout(c *gin.Context) { + claim, exist := c.Get("claim") + if !exist { + e.Pong(c, e.UserUnauthenticated, nil) + return + } + + _, status := h.userService.IncrVersion(claim.(*global.Claim).UID) + e.Pong(c, status, nil) +} diff --git a/internal/api/user/profile.go b/internal/api/user/profile.go new file mode 100644 index 0000000..bc23f9e --- /dev/null +++ b/internal/api/user/profile.go @@ -0,0 +1,54 @@ +package user + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/global" + "github.com/WHUPRJ/woj-server/internal/model" + "github.com/gin-gonic/gin" +) + +type profileRequest struct { + UID uint `form:"uid"` +} + +// Profile +// @Summary profile +// @Description fetch user profile +// @Accept application/x-www-form-urlencoded +// @Produce json +// @Param uid formData int false "user id" +// @Response 200 {object} e.Response "user info" +// @Security Authentication +// @Router /v1/user/profile [post] +func (h *handler) Profile(c *gin.Context) { + // TODO: create a new struct for profile (user info & solve info) + + claim, exist := c.Get("claim") + if !exist { + e.Pong(c, e.UserUnauthenticated, nil) + return + } + + uid := claim.(*global.Claim).UID + role := claim.(*global.Claim).Role + + req := new(profileRequest) + + if err := c.ShouldBind(req); err != nil { + e.Pong(c, e.InvalidParameter, nil) + return + } + + if req.UID == 0 { + req.UID = uid + } else if req.UID != uid && role < model.RoleGeneral { + e.Pong(c, e.UserUnauthorized, nil) + return + } + + user, status := h.userService.Profile(req.UID) + + // TODO: >= admin can see is_enable + + e.Pong(c, status, user) +} diff --git a/internal/app/app.go b/internal/app/app.go deleted file mode 100644 index 91c2a2f..0000000 --- a/internal/app/app.go +++ /dev/null @@ -1,47 +0,0 @@ -package app - -import ( - "context" - "fmt" - "github.com/WHUPRJ/woj-server/internal/global" - "github.com/WHUPRJ/woj-server/internal/router" - "go.uber.org/zap" - "net/http" - "os" - "os/signal" - "syscall" - "time" -) - -func Run(g *global.Global) error { - routersInit := router.InitRouters(g) - - addr := fmt.Sprintf("%s:%d", g.Conf.WebServer.Address, g.Conf.WebServer.Port) - - server := &http.Server{ - Addr: addr, - Handler: routersInit, - } - - go func() { - if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { - g.Log.Fatal("ListenAndServe Failed", zap.Error(err)) - } - }() - - // Handle SIGINT and SIGTERM. - quit := make(chan os.Signal, 1) - signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) - <-quit - g.Log.Info("Shutting down server ...") - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - err := server.Shutdown(ctx) - if err != nil { - g.Log.Fatal("Server Shutdown Failed", zap.Error(err)) - } - - return err -} diff --git a/internal/app/runner/runner.go b/internal/app/runner/runner.go new file mode 100644 index 0000000..a57aa36 --- /dev/null +++ b/internal/app/runner/runner.go @@ -0,0 +1,43 @@ +package runner + +import ( + "github.com/WHUPRJ/woj-server/internal/api/runner" + "github.com/WHUPRJ/woj-server/internal/global" + "github.com/WHUPRJ/woj-server/internal/model" + "github.com/WHUPRJ/woj-server/pkg/utils" + "github.com/WHUPRJ/woj-server/pkg/zapasynq" + "github.com/hibiken/asynq" + "go.uber.org/zap" + "runtime" +) + +func RunRunner(g *global.Global) error { + hnd, err := runner.NewRunner(g) + if err != nil { + return err + } + + mux := asynq.NewServeMux() + mux.HandleFunc(model.TypeProblemBuild, hnd.Build) + mux.HandleFunc(model.TypeSubmitJudge, hnd.Judge) + + srv := asynq.NewServer( + asynq.RedisClientOpt{ + Addr: g.Conf.Redis.Address, + Password: g.Conf.Redis.Password, + DB: g.Conf.Redis.QueueDb, + }, + asynq.Config{ + Concurrency: utils.If(runtime.NumCPU() > 1, runtime.NumCPU()-1, 1).(int), + Logger: zapasynq.New(g.Log), + Queues: map[string]int{model.QueueRunner: 1}, + }, + ) + + if err := srv.Run(mux); err != nil { + g.Log.Warn("could not run server", zap.Error(err)) + return err + } + + return nil +} diff --git a/internal/app/server/server.go b/internal/app/server/server.go new file mode 100644 index 0000000..c46b94a --- /dev/null +++ b/internal/app/server/server.go @@ -0,0 +1,103 @@ +package server + +import ( + "context" + "fmt" + "github.com/WHUPRJ/woj-server/internal/api/consumer" + "github.com/WHUPRJ/woj-server/internal/global" + "github.com/WHUPRJ/woj-server/internal/model" + "github.com/WHUPRJ/woj-server/internal/repo/postgresql" + "github.com/WHUPRJ/woj-server/internal/repo/redis" + "github.com/WHUPRJ/woj-server/internal/router" + "github.com/WHUPRJ/woj-server/internal/service/jwt" + "github.com/WHUPRJ/woj-server/pkg/utils" + "github.com/WHUPRJ/woj-server/pkg/zapasynq" + "github.com/hibiken/asynq" + "go.uber.org/zap" + "net/http" + "os" + "os/signal" + "runtime" + "syscall" + "time" +) + +func RunServer(g *global.Global) error { + // Setup Database + g.Db = new(postgresql.Repo) + g.Db.Setup(g) + + // Setup Redis + g.Redis = new(redis.Repo) + g.Redis.Setup(g) + + // Setup JWT + g.Jwt = jwt.NewJwtService(g) + + // Prepare Router + routers := router.InitRouters(g) + + // Create Server + addr := fmt.Sprintf("%s:%d", g.Conf.WebServer.Address, g.Conf.WebServer.Port) + server := &http.Server{ + Addr: addr, + Handler: routers, + } + + // Run Server + go func() { + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + g.Log.Fatal("ListenAndServe Failed", zap.Error(err)) + } + }() + + // Create Queue + queueMux := asynq.NewServeMux() + { + handler := consumer.NewConsumer(g) + queueMux.HandleFunc(model.TypeProblemUpdate, handler.ProblemUpdate) + queueMux.HandleFunc(model.TypeSubmitUpdate, handler.SubmitUpdate) + } + queueSrv := asynq.NewServer( + asynq.RedisClientOpt{ + Addr: g.Conf.Redis.Address, + Password: g.Conf.Redis.Password, + DB: g.Conf.Redis.QueueDb, + }, + asynq.Config{ + Concurrency: utils.If(runtime.NumCPU() > 1, runtime.NumCPU()-1, 1).(int), + Logger: zapasynq.New(g.Log), + Queues: map[string]int{model.QueueServer: 1}, + }, + ) + + // Run Queue + if err := queueSrv.Start(queueMux); err != nil { + g.Log.Fatal("queueSrv.Start Failed", zap.Error(err)) + } + + // Handle SIGINT and SIGTERM. + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + g.Log.Info("Shutting down server ...") + + // Graceful Shutdown Server + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + err := server.Shutdown(ctx) + if err != nil { + g.Log.Warn("Server Shutdown Failed", zap.Error(err)) + } + + // Graceful Shutdown Queue + queueSrv.Shutdown() + + // Graceful Shutdown Database + err = g.Db.Close() + if err != nil { + g.Log.Warn("Database Close Failed", zap.Error(err)) + } + + return err +} diff --git a/internal/e/code.go b/internal/e/code.go index cdbd8d6..df0f552 100644 --- a/internal/e/code.go +++ b/internal/e/code.go @@ -1,16 +1,117 @@ package e const ( - Fallback Err = 0 - Success Err = 1 - InternalError Err = 100 - InvalidParameter Err = 101 - NotFound Err = 102 + Success Status = iota + Unknown ) -var msgText = map[Err]string{ - Success: "Success", +const ( + InternalError Status = 100 + iota + InvalidParameter + NotFound + DatabaseError + RedisError +) + +const ( + TokenUnknown Status = 200 + iota + TokenEmpty + TokenMalformed + TokenTimeError + TokenInvalid + TokenSignError + TokenRevoked +) + +const ( + UserNotFound Status = 300 + iota + UserWrongPassword + UserDuplicated + UserUnauthenticated + UserUnauthorized + UserDisabled +) + +const ( + ProblemNotFound Status = 500 + iota + ProblemNotAvailable + ProblemVersionNotFound + ProblemVersionNotAvailable + SubmissionNotFound + StatusNotFound +) + +const ( + TaskEnqueueFailed Status = 600 + iota + TaskGetInfoFailed +) + +const ( + RunnerDepsBuildFailed Status = 700 + iota + RunnerDownloadFailed + RunnerUnzipFailed + RunnerProblemNotExist + RunnerProblemPrebuildFailed + RunnerProblemParseFailed + RunnerUserNotExist + RunnerUserCompileFailed + RunnerRunFailed + RunnerJudgeFailed +) + +const ( + StorageUploadFailed Status = 800 + iota + StorageGetFailed +) + +var msgText = map[Status]string{ + Success: "Success", + Unknown: "Unknown error", + InternalError: "Internal Error", InvalidParameter: "Invalid Parameter", NotFound: "Not Found", + DatabaseError: "Database Error", + RedisError: "Redis Error", + + TokenUnknown: "Unknown Error (Token)", + TokenEmpty: "Token Empty", + TokenMalformed: "Token Malformed", + TokenTimeError: "Token Time Error", + TokenInvalid: "Token Invalid", + TokenSignError: "Token Sign Error", + TokenRevoked: "Token Revoked", + + UserNotFound: "User Not Found", + UserWrongPassword: "User Wrong Password", + UserDuplicated: "User Duplicated", + UserUnauthenticated: "User Unauthenticated", + UserUnauthorized: "User Unauthorized", + UserDisabled: "User Disabled", + + ProblemNotFound: "Problem Not Found", + ProblemNotAvailable: "Problem Not Available", + ProblemVersionNotFound: "Problem Version Not Found", + ProblemVersionNotAvailable: "Problem Version Not Available", + + SubmissionNotFound: "Submission Not Found", + + StatusNotFound: "Status Not Found", + + TaskEnqueueFailed: "Task Enqueue Failed", + TaskGetInfoFailed: "Task Get Info Failed", + + RunnerDepsBuildFailed: "Runner Deps Build Failed", + RunnerDownloadFailed: "Runner Download Failed", + RunnerUnzipFailed: "Runner Unzip Failed", + RunnerProblemNotExist: "Runner Problem Not Exist", + RunnerProblemPrebuildFailed: "Runner Problem Prebuild Failed", + RunnerProblemParseFailed: "Runner Problem Parse Failed", + RunnerUserNotExist: "Runner User Not Exist", + RunnerUserCompileFailed: "Runner User Compile Failed", + RunnerRunFailed: "Runner Run Failed", + RunnerJudgeFailed: "Runner Judge Failed", + + StorageUploadFailed: "Storage Upload Failed", + StorageGetFailed: "Storage Get Failed", } diff --git a/internal/e/err.go b/internal/e/err.go index 240f897..44204e0 100644 --- a/internal/e/err.go +++ b/internal/e/err.go @@ -1,8 +1,8 @@ package e -type Err int +type Status int -func (code Err) String() string { +func (code Status) String() string { msg, ok := msgText[code] if ok { return msg diff --git a/internal/e/resp.go b/internal/e/resp.go index 44b6b66..4ea2ae5 100644 --- a/internal/e/resp.go +++ b/internal/e/resp.go @@ -12,14 +12,24 @@ type Response struct { Body interface{} `json:"body"` } -func Wrap(err Err, body interface{}) interface{} { +func Wrap(status Status, body interface{}) interface{} { return Response{ - Code: int(err), - Msg: err.String(), - Body: utils.If(err == Success, body, nil), + Code: int(status), + Msg: status.String(), + Body: utils.If(status == Success, body, nil), } } -func Pong(c *gin.Context, err Err, body interface{}) { - c.JSON(http.StatusOK, Wrap(err, body)) +func Pong(c *gin.Context, status Status, body interface{}) { + c.Set("err", status) + c.JSON(http.StatusOK, Wrap(status, body)) +} + +type Endpoint func(*gin.Context) (Status, interface{}) + +func PongWrapper(handler Endpoint) func(*gin.Context) { + return func(c *gin.Context) { + status, body := handler(c) + Pong(c, status, body) + } } diff --git a/internal/global/config.go b/internal/global/config.go index 1326491..0eaaa19 100644 --- a/internal/global/config.go +++ b/internal/global/config.go @@ -1,22 +1,39 @@ package global -import ( - "github.com/WHUPRJ/woj-server/pkg/utils" - "gopkg.in/yaml.v3" - "log" -) - type ConfigWebServer struct { - Address string `yaml:"Address"` - Port int `yaml:"Port"` + Address string `yaml:"Address"` + Port int `yaml:"Port"` + JwtSigningKey string `yaml:"JwtSigningKey"` + JwtExpireHour int `yaml:"JwtExpireHour"` } type ConfigRedis struct { Db int `yaml:"Db"` + QueueDb int `yaml:"QueueDb"` Address string `yaml:"Address"` Password string `yaml:"Password"` } +type ConfigDatabase struct { + Host string `yaml:"Host"` + Port int `yaml:"Port"` + User string `yaml:"User"` + Password string `yaml:"Password"` + Database string `yaml:"Database"` + Prefix string `yaml:"Prefix"` + MaxOpenConns int `yaml:"MaxOpenConns"` + MaxIdleConns int `yaml:"MaxIdleConns"` + ConnMaxLifetime int `yaml:"ConnMaxLifetime"` +} + +type ConfigStorage struct { + Endpoint string `yaml:"Endpoint"` + UseSSL bool `yaml:"UseSSL"` + AccessKey string `yaml:"AccessKey"` + SecretKey string `yaml:"SecretKey"` + Bucket string `yaml:"Bucket"` +} + type ConfigMetrics struct { Namespace string `yaml:"Namespace"` Subsystem string `yaml:"Subsystem"` @@ -25,18 +42,8 @@ type ConfigMetrics struct { type Config struct { WebServer ConfigWebServer `yaml:"WebServer"` Redis ConfigRedis `yaml:"Redis"` + Database ConfigDatabase `yaml:"Database"` + Storage ConfigStorage `yaml:"Storage"` Metrics ConfigMetrics `yaml:"Metrics"` Development bool `yaml:"Development"` } - -func (g *Global) setupConfig(configFile string) { - data, err := utils.FileRead(configFile) - if err != nil { - log.Fatalf("Failed to setup config: %s\n", err.Error()) - } - - err = yaml.Unmarshal(data, &g.Conf) - if err != nil { - log.Fatalf("Failed to setup config: %s\n", err.Error()) - } -} diff --git a/internal/global/model.go b/internal/global/global.go similarity index 55% rename from internal/global/model.go rename to internal/global/global.go index 2939d42..609895a 100644 --- a/internal/global/model.go +++ b/internal/global/global.go @@ -6,8 +6,10 @@ import ( ) type Global struct { - Log *zap.Logger - Conf *Config - Stat *metrics.Metrics - // Redis *redis.Client + Log *zap.Logger + Conf *Config + Stat *metrics.Metrics + Db Repo + Redis Repo + Jwt JwtService } diff --git a/internal/global/jwt.go b/internal/global/jwt.go new file mode 100644 index 0000000..b39faf2 --- /dev/null +++ b/internal/global/jwt.go @@ -0,0 +1,23 @@ +package global + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/model" + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v4" +) + +type Claim struct { + UID uint `json:"id"` + Role model.Role `json:"role"` + Version int64 `json:"version"` + jwt.RegisteredClaims +} + +type JwtService interface { + ParseToken(tokenText string) (*Claim, e.Status) + SignClaim(claim *Claim) (string, e.Status) + Validate(claim *Claim) bool + + Handler(forced bool) gin.HandlerFunc +} diff --git a/internal/global/log.go b/internal/global/log.go deleted file mode 100644 index 6284967..0000000 --- a/internal/global/log.go +++ /dev/null @@ -1,27 +0,0 @@ -package global - -import ( - "github.com/WHUPRJ/woj-server/pkg/utils" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" - "log" -) - -func (g *Global) setupZap() { - cfg := zap.Config{ - Level: zap.NewAtomicLevelAt( - utils.If(g.Conf.Development, zapcore.DebugLevel, zapcore.InfoLevel).(zapcore.Level), - ), - Development: g.Conf.Development, - Encoding: "console", // or json - EncoderConfig: zap.NewDevelopmentEncoderConfig(), - OutputPaths: []string{"stdout"}, - ErrorOutputPaths: []string{"stderr"}, - } - - var err error - g.Log, err = cfg.Build() - if err != nil { - log.Fatalf("Failed to setup Zap: %s\n", err.Error()) - } -} diff --git a/internal/global/repo.go b/internal/global/repo.go new file mode 100644 index 0000000..89b8b28 --- /dev/null +++ b/internal/global/repo.go @@ -0,0 +1,7 @@ +package global + +type Repo interface { + Setup(*Global) + Get() interface{} + Close() error +} diff --git a/internal/global/router.go b/internal/global/router.go new file mode 100644 index 0000000..8ee2f78 --- /dev/null +++ b/internal/global/router.go @@ -0,0 +1,11 @@ +package global + +import ( + "github.com/gin-gonic/gin" +) + +type EndpointInfo struct { + Version string + Path string + Register func(g *Global, group *gin.RouterGroup) +} diff --git a/internal/global/setup.go b/internal/global/setup.go index ca6c8ac..d04d91c 100644 --- a/internal/global/setup.go +++ b/internal/global/setup.go @@ -1,20 +1,40 @@ package global import ( - "github.com/WHUPRJ/woj-server/internal/pkg/metrics" - "math/rand" - "time" + "github.com/WHUPRJ/woj-server/pkg/utils" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "gopkg.in/yaml.v3" + "log" ) -func (g *Global) Setup(configFile string) { - rand.Seed(time.Now().Unix()) +func (g *Global) SetupZap() { + cfg := zap.Config{ + Level: zap.NewAtomicLevelAt( + utils.If(g.Conf.Development, zapcore.DebugLevel, zapcore.InfoLevel).(zapcore.Level), + ), + Development: g.Conf.Development, + Encoding: "console", // or json + EncoderConfig: zap.NewDevelopmentEncoderConfig(), + OutputPaths: []string{"stdout"}, + ErrorOutputPaths: []string{"stderr"}, + } - g.setupConfig(configFile) - g.setupZap() - - g.Stat = new(metrics.Metrics) - g.Stat.Setup(g.Conf.Metrics.Namespace, g.Conf.Metrics.Subsystem) - g.Stat.SetLogPaths([]string{ - "/api", - }) + var err error + g.Log, err = cfg.Build() + if err != nil { + log.Fatalf("Failed to setup Zap: %s\n", err.Error()) + } +} + +func (g *Global) SetupConfig(configFile string) { + data, err := utils.FileRead(configFile) + if err != nil { + log.Fatalf("Failed to setup config: %s\n", err.Error()) + } + + err = yaml.Unmarshal(data, &g.Conf) + if err != nil { + log.Fatalf("Failed to setup config: %s\n", err.Error()) + } } diff --git a/internal/model/Problem.go b/internal/model/Problem.go new file mode 100644 index 0000000..c72b0b9 --- /dev/null +++ b/internal/model/Problem.go @@ -0,0 +1,23 @@ +package model + +import ( + "github.com/jackc/pgtype" + "gorm.io/gorm" +) + +type Problem struct { + gorm.Model `json:"meta"` + Title string `json:"title" gorm:"not null"` + Statement string `json:"statement" gorm:"not null"` + ProviderID uint `json:"-" gorm:"not null;index"` + Provider User `json:"provider" gorm:"foreignKey:ProviderID"` + IsEnabled bool `json:"is_enabled" gorm:"not null;index"` +} + +type ProblemVersion struct { + gorm.Model `json:"meta"` + ProblemID uint `json:"-" gorm:"not null;index"` + Context pgtype.JSON `json:"context" gorm:"type:json"` + StorageKey string `json:"-" gorm:"not null"` + IsEnabled bool `json:"is_enabled" gorm:"not null;index"` +} diff --git a/internal/model/Role.go b/internal/model/Role.go new file mode 100644 index 0000000..184c77f --- /dev/null +++ b/internal/model/Role.go @@ -0,0 +1,9 @@ +package model + +type Role int + +const ( + RoleGuest Role = 10 + RoleGeneral Role = 20 + RoleAdmin Role = 30 +) diff --git a/internal/model/Status.go b/internal/model/Status.go new file mode 100644 index 0000000..2e14cf1 --- /dev/null +++ b/internal/model/Status.go @@ -0,0 +1,16 @@ +package model + +import ( + "github.com/jackc/pgtype" + "gorm.io/gorm" +) + +type Status struct { + gorm.Model `json:"meta"` + SubmissionID uint `json:"-" gorm:"not null;index"` + Submission Submission `json:"submission" gorm:"foreignKey:SubmissionID"` + ProblemVersionID uint `json:"problem_version_id" gorm:"not null;index"` + Context pgtype.JSON `json:"context" gorm:"type:json;not null"` + Point int32 `json:"point" gorm:"not null"` + IsEnabled bool `json:"is_enabled" gorm:"not null;index"` +} diff --git a/internal/model/Submission.go b/internal/model/Submission.go new file mode 100644 index 0000000..37bb14d --- /dev/null +++ b/internal/model/Submission.go @@ -0,0 +1,12 @@ +package model + +import "gorm.io/gorm" + +type Submission struct { + gorm.Model `json:"meta"` + ProblemID uint `json:"problem_id" gorm:"not null;index"` + UserID uint `json:"-" gorm:"not null;index"` + User User `json:"user" gorm:"foreignKey:UserID"` + Language string `json:"language" gorm:"not null"` + Code string `json:"code" gorm:"not null"` +} diff --git a/internal/model/Task.go b/internal/model/Task.go new file mode 100644 index 0000000..fc8a529 --- /dev/null +++ b/internal/model/Task.go @@ -0,0 +1,42 @@ +package model + +import ( + "github.com/WHUPRJ/woj-server/internal/e" +) + +const ( + TypeProblemBuild = "problem:build" + TypeProblemUpdate = "problem:update" + TypeSubmitJudge = "submit:judge" + TypeSubmitUpdate = "submit:update" +) + +const ( + QueueServer = "server" + QueueRunner = "runner" +) + +type ProblemBuildPayload struct { + ProblemVersionID uint + StorageKey string +} + +type ProblemUpdatePayload struct { + Status e.Status + ProblemVersionID uint + Context string +} + +type SubmitJudgePayload struct { + ProblemVersionID uint + StorageKey string + Submission Submission +} + +type SubmitUpdatePayload struct { + Status e.Status + SubmissionID uint + ProblemVersionID uint + Point int32 + Context string +} diff --git a/internal/model/User.go b/internal/model/User.go new file mode 100644 index 0000000..cc6507a --- /dev/null +++ b/internal/model/User.go @@ -0,0 +1,14 @@ +package model + +import ( + "gorm.io/gorm" +) + +type User struct { + gorm.Model `json:"meta"` + UserName string `json:"user_name" gorm:"not null;uniqueIndex"` + NickName string `json:"nick_name" gorm:"not null"` + Role Role `json:"role" gorm:"not null"` + Password []byte `json:"-" gorm:"not null"` + IsEnabled bool `json:"is_enabled" gorm:"not null;index"` +} diff --git a/internal/model/router.go b/internal/model/router.go deleted file mode 100644 index 52e5666..0000000 --- a/internal/model/router.go +++ /dev/null @@ -1,12 +0,0 @@ -package model - -import ( - "github.com/WHUPRJ/woj-server/internal/global" - "github.com/gin-gonic/gin" -) - -type EndpointInfo struct { - Version string - Path string - Register func(g *global.Global, group *gin.RouterGroup) -} diff --git a/internal/pkg/cast/cast.go b/internal/pkg/cast/cast.go index 16639fa..d73466d 100644 --- a/internal/pkg/cast/cast.go +++ b/internal/pkg/cast/cast.go @@ -13,7 +13,7 @@ func ToString(obj interface{}) string { return strconv.FormatBool(t) case []byte: return string(t) - case e.Err: + case e.Status: return t.String() case error: return t.Error() diff --git a/internal/pkg/metrics/middleware.go b/internal/pkg/metrics/middleware.go index 32cc4d0..e1fd20e 100644 --- a/internal/pkg/metrics/middleware.go +++ b/internal/pkg/metrics/middleware.go @@ -30,10 +30,12 @@ func (m *Metrics) Handler() gin.HandlerFunc { status := c.Writer.Status() success := !c.IsAborted() && (status == http.StatusOK) errCode, _ := c.Get("err") - err, ok := errCode.(e.Err) + err, ok := errCode.(e.Status) if !ok { success = false - err = e.Fallback + err = e.Unknown + } else if err != e.Success { + success = false } m.Record(method, url, success, status, err, elapsed) diff --git a/internal/pkg/metrics/prometheus.go b/internal/pkg/metrics/prometheus.go index 4940b09..8426604 100644 --- a/internal/pkg/metrics/prometheus.go +++ b/internal/pkg/metrics/prometheus.go @@ -44,7 +44,7 @@ func (m *Metrics) Setup(namespace string, subsystem string) { prometheus.MustRegister(m.counter, m.hist) } -func (m *Metrics) Record(method, url string, success bool, httpCode int, errCode e.Err, elapsed float64) { +func (m *Metrics) Record(method, url string, success bool, httpCode int, errCode e.Status, elapsed float64) { m.counter.With(prometheus.Labels{ "method": method, "url": url, diff --git a/internal/repo/postgresql/postgresql.go b/internal/repo/postgresql/postgresql.go new file mode 100644 index 0000000..4a5ae90 --- /dev/null +++ b/internal/repo/postgresql/postgresql.go @@ -0,0 +1,113 @@ +package postgresql + +import ( + "database/sql" + "errors" + "fmt" + "github.com/WHUPRJ/woj-server/internal/global" + "github.com/WHUPRJ/woj-server/internal/model" + "go.uber.org/zap" + "gorm.io/driver/postgres" + "gorm.io/gorm" + "gorm.io/gorm/schema" + "moul.io/zapgorm2" + "time" +) + +var _ global.Repo = (*Repo)(nil) + +type Repo struct { + db *gorm.DB + log *zap.Logger +} + +func (r *Repo) Get() interface{} { + return r.db +} + +func (r *Repo) Close() error { + db, err := r.db.DB() + if err != nil { + return err + } + return db.Close() +} + +func (r *Repo) Setup(g *global.Global) { + r.log = g.Log + + r.log.Info("Connecting to database...") + + logger := zapgorm2.New(r.log) + logger.IgnoreRecordNotFoundError = true + + dsn := fmt.Sprintf("user=%s password=%s dbname=%s host=%s port=%d sslmode=disable TimeZone=Asia/Shanghai", + g.Conf.Database.User, + g.Conf.Database.Password, + g.Conf.Database.Database, + g.Conf.Database.Host, + g.Conf.Database.Port) + + var err error + r.db, err = gorm.Open( + postgres.Open(dsn), + &gorm.Config{ + NamingStrategy: schema.NamingStrategy{ + SingularTable: true, + TablePrefix: g.Conf.Database.Prefix, + }, + PrepareStmt: true, + Logger: logger, + }) + + if err != nil { + r.log.Fatal("Failed to connect to database", zap.Error(err)) + return + } + + db, err := r.checkAlive(3) + if err != nil { + r.log.Fatal("Database is not alive", zap.Error(err)) + return + } + + db.SetMaxOpenConns(g.Conf.Database.MaxOpenConns) + db.SetMaxIdleConns(g.Conf.Database.MaxIdleConns) + db.SetConnMaxLifetime(time.Duration(g.Conf.Database.ConnMaxLifetime) * time.Minute) + + r.migrateDatabase() +} + +func (r *Repo) migrateDatabase() { + r.log.Info("Auto Migrating database...") + + _ = r.db.AutoMigrate(&model.User{}) + _ = r.db.AutoMigrate(&model.Problem{}) + _ = r.db.AutoMigrate(&model.ProblemVersion{}) + _ = r.db.AutoMigrate(&model.Submission{}) + _ = r.db.AutoMigrate(&model.Status{}) +} + +// checkAlive deprecated +func (r *Repo) checkAlive(retry int) (*sql.DB, error) { + if retry <= 0 { + return nil, errors.New("all retries are used up. failed to connect to database") + } + + db, err := r.db.DB() + if err != nil { + r.log.Warn("failed to get sql.DB instance", zap.Error(err)) + time.Sleep(5 * time.Second) + return r.checkAlive(retry - 1) + } + + err = db.Ping() + if err != nil { + r.log.Warn("failed to ping database", zap.Error(err)) + time.Sleep(5 * time.Second) + return r.checkAlive(retry - 1) + } + + r.log.Info("database connect established") + return db, nil +} diff --git a/internal/repo/redis/redis.go b/internal/repo/redis/redis.go new file mode 100644 index 0000000..d23da71 --- /dev/null +++ b/internal/repo/redis/redis.go @@ -0,0 +1,39 @@ +package redis + +import ( + "context" + "github.com/WHUPRJ/woj-server/internal/global" + "github.com/go-redis/redis/v8" + "go.uber.org/zap" +) + +var _ global.Repo = (*Repo)(nil) + +type Repo struct { + client *redis.Client + log *zap.Logger +} + +func (r *Repo) Setup(g *global.Global) { + r.log = g.Log + + r.client = redis.NewClient(&redis.Options{ + Addr: g.Conf.Redis.Address, + Password: g.Conf.Redis.Password, + DB: g.Conf.Redis.Db, + }) + + _, err := r.client.Ping(context.Background()).Result() + if err != nil { + r.log.Fatal("Redis ping failed", zap.Error(err)) + return + } +} + +func (r *Repo) Get() interface{} { + return r.client +} + +func (r *Repo) Close() error { + return r.client.Close() +} diff --git a/internal/router/api.go b/internal/router/api.go index d36ded2..ce7fa19 100644 --- a/internal/router/api.go +++ b/internal/router/api.go @@ -1,15 +1,21 @@ package router import ( - "github.com/WHUPRJ/woj-server/internal/controller/debug" + "github.com/WHUPRJ/woj-server/internal/api/debug" + "github.com/WHUPRJ/woj-server/internal/api/problem" + "github.com/WHUPRJ/woj-server/internal/api/status" + "github.com/WHUPRJ/woj-server/internal/api/submission" + "github.com/WHUPRJ/woj-server/internal/api/user" "github.com/WHUPRJ/woj-server/internal/global" - "github.com/WHUPRJ/woj-server/internal/model" "github.com/gin-gonic/gin" ) // @title OJ Server API Documentation -// @version 1.0 -// @BasePath /api +// @version 1.0 +// @BasePath /api +// @securityDefinitions.apikey Authentication +// @in header +// @name Authorization func setupApi(g *global.Global, root *gin.RouterGroup) { for _, v := range endpoints { group := root.Group(v.Version).Group(v.Path) @@ -17,6 +23,10 @@ func setupApi(g *global.Global, root *gin.RouterGroup) { } } -var endpoints = []model.EndpointInfo{ +var endpoints = []global.EndpointInfo{ {Version: "", Path: "/debug", Register: debug.RouteRegister}, + {Version: "/v1", Path: "/user", Register: user.RouteRegister}, + {Version: "/v1", Path: "/problem", Register: problem.RouteRegister}, + {Version: "/v1", Path: "/submission", Register: submission.RouteRegister}, + {Version: "/v1", Path: "/status", Register: status.RouteRegister}, } diff --git a/internal/router/router.go b/internal/router/router.go index b688f53..a5669f2 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -2,6 +2,7 @@ package router import ( "github.com/WHUPRJ/woj-server/internal/global" + "github.com/WHUPRJ/woj-server/internal/pkg/metrics" _ "github.com/WHUPRJ/woj-server/internal/router/docs" "github.com/WHUPRJ/woj-server/pkg/utils" "github.com/gin-contrib/cors" @@ -42,6 +43,9 @@ func InitRouters(g *global.Global) *gin.Engine { })) // Prometheus middleware + g.Stat = new(metrics.Metrics) + g.Stat.Setup(g.Conf.Metrics.Namespace, g.Conf.Metrics.Subsystem) + g.Stat.SetLogPaths([]string{"/api"}) r.Use(g.Stat.Handler()) // metrics diff --git a/internal/service/jwt/middleware.go b/internal/service/jwt/middleware.go new file mode 100644 index 0000000..2d2eddc --- /dev/null +++ b/internal/service/jwt/middleware.go @@ -0,0 +1,41 @@ +package jwt + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/global" + "github.com/gin-gonic/gin" + "strings" +) + +func (s *service) Handler(forced bool) gin.HandlerFunc { + return func(c *gin.Context) { + claim, status := func() (*global.Claim, e.Status) { + const tokenPrefix = "bearer " + tokenHeader := c.GetHeader("Authorization") + if tokenHeader == "" || !strings.HasPrefix(strings.ToLower(tokenHeader), tokenPrefix) { + return nil, e.TokenEmpty + } + + token := tokenHeader[len(tokenPrefix):] + claim, status := s.ParseToken(token) + if status != e.Success { + return nil, status + } + + if !s.Validate(claim) { + return nil, e.TokenRevoked + } + return claim, e.Success + }() + + if status == e.Success { + c.Set("claim", claim) + } + if forced && status != e.Success { + e.Pong(c, status, nil) + c.Abort() + } else { + c.Next() + } + } +} diff --git a/internal/service/jwt/service.go b/internal/service/jwt/service.go new file mode 100644 index 0000000..3cb372d --- /dev/null +++ b/internal/service/jwt/service.go @@ -0,0 +1,25 @@ +package jwt + +import ( + "github.com/WHUPRJ/woj-server/internal/global" + "github.com/go-redis/redis/v8" + "go.uber.org/zap" +) + +var _ global.JwtService = (*service)(nil) + +type service struct { + log *zap.Logger + redis *redis.Client + SigningKey []byte + ExpireHour int +} + +func NewJwtService(g *global.Global) global.JwtService { + return &service{ + log: g.Log, + redis: g.Redis.Get().(*redis.Client), + SigningKey: []byte(g.Conf.WebServer.JwtSigningKey), + ExpireHour: g.Conf.WebServer.JwtExpireHour, + } +} diff --git a/internal/service/jwt/token.go b/internal/service/jwt/token.go new file mode 100644 index 0000000..876757e --- /dev/null +++ b/internal/service/jwt/token.go @@ -0,0 +1,76 @@ +package jwt + +import ( + "context" + "fmt" + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/global" + "github.com/WHUPRJ/woj-server/pkg/utils" + "github.com/golang-jwt/jwt/v4" + "go.uber.org/zap" + "time" +) + +func (s *service) ParseToken(tokenText string) (*global.Claim, e.Status) { + if tokenText == "" { + return nil, e.TokenEmpty + } + + token, err := jwt.ParseWithClaims( + tokenText, + new(global.Claim), + func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return s.SigningKey, nil + }) + + if ve, ok := err.(*jwt.ValidationError); ok { + if ve.Errors&jwt.ValidationErrorMalformed != 0 { + return nil, e.TokenMalformed + } else if ve.Errors&(jwt.ValidationErrorExpired|jwt.ValidationErrorNotValidYet) != 0 { + // Token is either expired or not active yet + return nil, e.TokenTimeError + } else { + return nil, e.TokenInvalid + } + } else if err != nil { + s.log.Warn("JWT Token Parse Error", zap.Error(err)) + return nil, e.TokenUnknown + } + + if token.Valid { + c := token.Claims.(*global.Claim) + return c, e.Success + } + + return nil, e.TokenInvalid +} + +func (s *service) SignClaim(claim *global.Claim) (string, e.Status) { + now := time.Now() + + claim.IssuedAt = jwt.NewNumericDate(now) + claim.ExpiresAt = jwt.NewNumericDate(now.Add(time.Duration(s.ExpireHour) * time.Hour)) + claim.ID = utils.RandomString(16) + // TODO: use per-user claim.Version to tracker invalidation + claim.NotBefore = jwt.NewNumericDate(time.Now()) + + token := jwt.NewWithClaims(jwt.SigningMethodHS512, claim) + ss, err := token.SignedString(s.SigningKey) + if err != nil { + s.log.Warn("jwt.SignedString error", zap.Error(err)) + return "", e.TokenSignError + } + return ss, e.Success +} + +func (s *service) Validate(claim *global.Claim) bool { + curVersion, err := s.redis.Get(context.Background(), fmt.Sprintf("Version:%d", claim.UID)).Int64() + if err != nil { + s.log.Debug("redis.Get error", zap.Error(err)) + return false + } + return curVersion == claim.Version +} diff --git a/internal/service/problem/create.go b/internal/service/problem/create.go new file mode 100644 index 0000000..d79c4dd --- /dev/null +++ b/internal/service/problem/create.go @@ -0,0 +1,31 @@ +package problem + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/model" + "go.uber.org/zap" +) + +type CreateData struct { + Title string + Statement string + ProviderID uint + IsEnabled bool +} + +func (s *service) Create(data *CreateData) (*model.Problem, e.Status) { + problem := &model.Problem{ + Title: data.Title, + Statement: data.Statement, + ProviderID: data.ProviderID, + IsEnabled: data.IsEnabled, + } + + err := s.db.Create(problem).Error + if err != nil { + s.log.Warn("DatabaseError", zap.Error(err), zap.Any("problem", problem)) + return nil, e.DatabaseError + } + + return problem, e.Success +} diff --git a/internal/service/problem/createVersion.go b/internal/service/problem/createVersion.go new file mode 100644 index 0000000..b63a9d0 --- /dev/null +++ b/internal/service/problem/createVersion.go @@ -0,0 +1,29 @@ +package problem + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/model" + "github.com/jackc/pgtype" + "go.uber.org/zap" +) + +type CreateVersionData struct { + ProblemID uint + StorageKey string +} + +func (s *service) CreateVersion(data *CreateVersionData) (*model.ProblemVersion, e.Status) { + problemVersion := &model.ProblemVersion{ + ProblemID: data.ProblemID, + Context: pgtype.JSON{Status: pgtype.Null}, + StorageKey: data.StorageKey, + } + + err := s.db.Create(problemVersion).Error + if err != nil { + s.log.Warn("DatabaseError", zap.Error(err), zap.Any("problemVersion", problemVersion)) + return nil, e.DatabaseError + } + + return problemVersion, e.Success +} diff --git a/internal/service/problem/query.go b/internal/service/problem/query.go new file mode 100644 index 0000000..aa10be5 --- /dev/null +++ b/internal/service/problem/query.go @@ -0,0 +1,32 @@ +package problem + +import ( + "errors" + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/model" + "go.uber.org/zap" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +func (s *service) Query(pid uint, associations bool, shouldEnable bool) (*model.Problem, e.Status) { + problem := new(model.Problem) + + query := s.db + if associations { + query = query.Preload(clause.Associations) + } + err := query.First(&problem, pid).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, e.ProblemNotFound + } + if err != nil { + s.log.Warn("DatabaseError", zap.Error(err), zap.Any("pid", pid)) + return nil, e.DatabaseError + } + + if shouldEnable && !problem.IsEnabled { + return nil, e.ProblemNotAvailable + } + return problem, e.Success +} diff --git a/internal/service/problem/queryFuzz.go b/internal/service/problem/queryFuzz.go new file mode 100644 index 0000000..a461e37 --- /dev/null +++ b/internal/service/problem/queryFuzz.go @@ -0,0 +1,30 @@ +package problem + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/model" + "go.uber.org/zap" + "gorm.io/gorm/clause" +) + +func (s *service) QueryFuzz(search string, associations bool, shouldEnable bool) ([]*model.Problem, e.Status) { + problems := make([]*model.Problem, 0) + + query := s.db + if associations { + query = query.Preload(clause.Associations) + } + if shouldEnable { + query = query.Where("is_enabled = true") + } + query = query. + Where(s.db.Where("title LIKE ?", "%"+search+"%"). + Or("statement LIKE ?", "%"+search+"%")) + err := query.Find(&problems).Error + if err != nil { + s.log.Warn("DatabaseError", zap.Error(err), zap.Any("search", search)) + return nil, e.DatabaseError + } + + return problems, e.Success +} diff --git a/internal/service/problem/queryLatestVersion.go b/internal/service/problem/queryLatestVersion.go new file mode 100644 index 0000000..55f0fc8 --- /dev/null +++ b/internal/service/problem/queryLatestVersion.go @@ -0,0 +1,29 @@ +package problem + +import ( + "errors" + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/model" + "go.uber.org/zap" + "gorm.io/gorm" +) + +func (s *service) QueryLatestVersion(pid uint) (*model.ProblemVersion, e.Status) { + problemVersion := &model.ProblemVersion{ + ProblemID: pid, + IsEnabled: true, + } + + err := s.db. + Where(problemVersion). + Last(&problemVersion).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, e.ProblemVersionNotFound + } + if err != nil { + s.log.Warn("DatabaseError", zap.Error(err), zap.Any("problemVersion", problemVersion)) + return nil, e.DatabaseError + } + + return problemVersion, e.Success +} diff --git a/internal/service/problem/queryVersion.go b/internal/service/problem/queryVersion.go new file mode 100644 index 0000000..9ca08ae --- /dev/null +++ b/internal/service/problem/queryVersion.go @@ -0,0 +1,27 @@ +package problem + +import ( + "errors" + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/model" + "go.uber.org/zap" + "gorm.io/gorm" +) + +func (s *service) QueryVersion(pvid uint, shouldEnable bool) (*model.ProblemVersion, e.Status) { + problemVersion := new(model.ProblemVersion) + + err := s.db.First(&problemVersion, pvid).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, e.ProblemVersionNotFound + } + if err != nil { + s.log.Warn("DatabaseError", zap.Error(err), zap.Any("pvid", pvid)) + return nil, e.DatabaseError + } + + if shouldEnable && !problemVersion.IsEnabled { + return nil, e.ProblemVersionNotAvailable + } + return problemVersion, e.Success +} diff --git a/internal/service/problem/service.go b/internal/service/problem/service.go new file mode 100644 index 0000000..0dd05af --- /dev/null +++ b/internal/service/problem/service.go @@ -0,0 +1,35 @@ +package problem + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/global" + "github.com/WHUPRJ/woj-server/internal/model" + "go.uber.org/zap" + "gorm.io/gorm" +) + +var _ Service = (*service)(nil) + +type Service interface { + Create(data *CreateData) (*model.Problem, e.Status) + Update(problem *model.Problem) (*model.Problem, e.Status) + Query(pid uint, associations bool, shouldEnable bool) (*model.Problem, e.Status) + QueryFuzz(search string, associations bool, shouldEnable bool) ([]*model.Problem, e.Status) + + CreateVersion(data *CreateVersionData) (*model.ProblemVersion, e.Status) + UpdateVersion(pvid uint, values interface{}) e.Status + QueryVersion(pvid uint, shouldEnable bool) (*model.ProblemVersion, e.Status) + QueryLatestVersion(pid uint) (*model.ProblemVersion, e.Status) +} + +type service struct { + log *zap.Logger + db *gorm.DB +} + +func NewService(g *global.Global) Service { + return &service{ + log: g.Log, + db: g.Db.Get().(*gorm.DB), + } +} diff --git a/internal/service/problem/update.go b/internal/service/problem/update.go new file mode 100644 index 0000000..026b934 --- /dev/null +++ b/internal/service/problem/update.go @@ -0,0 +1,17 @@ +package problem + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/model" + "go.uber.org/zap" +) + +func (s *service) Update(problem *model.Problem) (*model.Problem, e.Status) { + err := s.db.Save(problem).Error + if err != nil { + s.log.Warn("DatabaseError", zap.Error(err), zap.Any("problem", problem)) + return nil, e.DatabaseError + } + + return problem, e.Success +} diff --git a/internal/service/problem/updateVersion.go b/internal/service/problem/updateVersion.go new file mode 100644 index 0000000..de8230c --- /dev/null +++ b/internal/service/problem/updateVersion.go @@ -0,0 +1,17 @@ +package problem + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/model" + "go.uber.org/zap" +) + +func (s *service) UpdateVersion(pvid uint, values interface{}) e.Status { + err := s.db.Model(&model.ProblemVersion{}).Where("id = ?", pvid).Updates(values).Error + if err != nil { + s.log.Warn("DatabaseError", zap.Error(err), zap.Any("pvid", pvid), zap.Any("values", values)) + return e.DatabaseError + } + + return e.Success +} diff --git a/internal/service/runner/common.go b/internal/service/runner/common.go new file mode 100644 index 0000000..24eba47 --- /dev/null +++ b/internal/service/runner/common.go @@ -0,0 +1,75 @@ +package runner + +import ( + "fmt" + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/pkg/utils" + "go.uber.org/zap" + "os" + "os/exec" + "path" + "path/filepath" +) + +var ( + Prefix = "./resource/runner" + ProblemDir = "./problem/" + ScriptsDir = "./scripts/" + UserDir = "./user/" + TmpDir = "./tmp/" +) + +func init() { + wd, err := os.Getwd() + if err != nil { + panic(err) + } + + Prefix = path.Join(wd, Prefix) + ProblemDir = path.Join(Prefix, ProblemDir) + ScriptsDir = path.Join(Prefix, ScriptsDir) + UserDir = path.Join(Prefix, UserDir) + TmpDir = path.Join(Prefix, TmpDir) +} + +func (s *service) execute(script string, args ...string) error { + p := filepath.Join(ScriptsDir, script) + cmd := exec.Command(p, args...) + cmd.Dir = ScriptsDir + return cmd.Run() +} + +func (s *service) checkAndExecute(version uint, user string, lang string, script string, fail e.Status) e.Status { + if !s.ProblemExists(version) { + s.log.Info("problem not exists", zap.Uint("version", version)) + return e.RunnerProblemNotExist + } + + if !s.userExists(user, fmt.Sprintf("%s.%s", user, lang)) { + s.log.Info("user program not exists", zap.String("user", user), zap.String("lang", lang)) + return e.RunnerUserNotExist + } + + err := s.execute(script, fmt.Sprintf("%d", version), user, lang) + + if err != nil { + s.log.Info("execute failed", + zap.Error(err), + zap.Uint("version", version), + zap.String("user", user), + zap.String("lang", lang)) + return fail + } + + return e.Success +} + +func (s *service) ProblemExists(version uint) bool { + problemPath := filepath.Join(ProblemDir, fmt.Sprintf("%d", version)) + return utils.FileExist(problemPath) +} + +func (s *service) userExists(user string, file string) bool { + userPath := filepath.Join(UserDir, user, file) + return utils.FileExist(userPath) +} diff --git a/internal/service/runner/compile.go b/internal/service/runner/compile.go new file mode 100644 index 0000000..62ad6c0 --- /dev/null +++ b/internal/service/runner/compile.go @@ -0,0 +1,30 @@ +package runner + +import ( + "fmt" + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/pkg/utils" + "os" + "path/filepath" +) + +func (s *service) Compile(version uint, user string, lang string) (JudgeStatus, e.Status) { + target := filepath.Join(UserDir, user, fmt.Sprintf("%s.out", user)) + + _ = os.Remove(target) + status := s.checkAndExecute(version, user, lang, "problem_compile.sh", e.RunnerUserCompileFailed) + + log := filepath.Join(UserDir, user, fmt.Sprintf("%s.compile.log", user)) + msg, err := utils.FileRead(log) + msg = utils.If(err == nil, msg, nil).([]byte) + msgText := string(msg) + + if !utils.FileExist(target) || utils.FileEmpty(target) { + return JudgeStatus{ + Message: "compile failed", + Tasks: []TaskStatus{{Verdict: VerdictCompileError, Message: msgText}}}, + utils.If(status == e.Success, e.RunnerUserCompileFailed, status).(e.Status) + } + + return JudgeStatus{}, e.Success +} diff --git a/internal/service/runner/config.go b/internal/service/runner/config.go new file mode 100644 index 0000000..94131c0 --- /dev/null +++ b/internal/service/runner/config.go @@ -0,0 +1,125 @@ +package runner + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" +) + +type Config struct { + Runtime struct { + TimeLimit int `json:"TimeLimit"` + MemoryLimit int `json:"MemoryLimit"` + NProcLimit int `json:"NProcLimit"` + } `json:"Runtime"` + Languages []struct { + Lang string `json:"Lang"` + Type string `json:"Type"` + Script string `json:"Script"` + Cmp string `json:"Cmp"` + } `json:"Languages"` + Tasks []struct { + Id int `json:"Id"` + Points int32 `json:"Points"` + } `json:"Tasks"` +} + +func (s *service) ParseConfig(version uint, skipCheck bool) (Config, error) { + base := filepath.Join(ProblemDir, fmt.Sprintf("%d", version)) + file := filepath.Join(base, "config.json") + + data, err := os.ReadFile(file) + if err != nil { + return Config{}, err + } + + config := Config{} + err = json.Unmarshal(data, &config) + if err != nil { + return Config{}, err + } + + if skipCheck { + return config, nil + } + + err = s.checkConfig(&config, base) + if err != nil { + return Config{}, err + } + + return config, nil +} + +func (s *service) checkConfig(config *Config, base string) error { + if config.Runtime.TimeLimit < 0 { + return errors.New("time limit is negative") + } + if config.Runtime.MemoryLimit < 0 { + return errors.New("memory limit is negative") + } + if config.Runtime.NProcLimit < 0 { + return errors.New("nproc limit is negative") + } + + allowedLang := map[string]struct{}{ + "c": {}, + "cpp": {}, + } + for _, lang := range config.Languages { + if _, ok := allowedLang[lang.Lang]; !ok { + return fmt.Errorf("language %s is not allowed", lang.Lang) + } + + if lang.Type != "custom" && lang.Type != "default" { + return fmt.Errorf("language %s has invalid type %s", lang.Lang, lang.Type) + } + + if lang.Type == "custom" { + if lang.Script == "" { + return fmt.Errorf("language %s has empty script", lang.Lang) + } + + file := filepath.Join(base, "judge", lang.Script) + _, err := os.Stat(file) + if err != nil { + return fmt.Errorf("language %s has invalid script %s", lang.Lang, lang.Script) + } + } + + if lang.Type == "default" { + if lang.Cmp == "" { + return fmt.Errorf("language %s has empty cmp", lang.Lang) + } + } + } + + if len(config.Tasks) == 0 { + return errors.New("no tasks") + } + ids := map[int]struct{}{} + total := (1 + len(config.Tasks)) * len(config.Tasks) / 2 + for _, task := range config.Tasks { + if task.Id <= 0 { + return fmt.Errorf("task %d has non-positive id", task.Id) + } + + if task.Points < 0 { + return fmt.Errorf("task %d has negative points", task.Id) + } + + if _, ok := ids[task.Id]; ok { + return fmt.Errorf("task %d has duplicate id", task.Id) + } + + total -= task.Id + ids[task.Id] = struct{}{} + } + if total != 0 { + return errors.New("task ids are not continuous") + } + + return nil +} diff --git a/internal/service/runner/deps.go b/internal/service/runner/deps.go new file mode 100644 index 0000000..96940f5 --- /dev/null +++ b/internal/service/runner/deps.go @@ -0,0 +1,31 @@ +package runner + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/pkg/utils" + "go.uber.org/zap" + "os" + "os/exec" + "path/filepath" +) + +func (s *service) EnsureDeps(force bool) e.Status { + mark := filepath.Join(Prefix, ".mark.docker") + + if force { + _ = os.Remove(mark) + } else if utils.FileExist(mark) { + return e.Success + } + + script := filepath.Join(ScriptsDir, "prepare_container.sh") + cmd := exec.Command(script) + cmd.Dir = ScriptsDir + err := cmd.Run() + if err != nil { + s.log.Warn("prebuild docker images failed", zap.Error(err)) + return e.RunnerDepsBuildFailed + } + + return e.Success +} diff --git a/internal/service/runner/newProblem.go b/internal/service/runner/newProblem.go new file mode 100644 index 0000000..79f969f --- /dev/null +++ b/internal/service/runner/newProblem.go @@ -0,0 +1,79 @@ +package runner + +import ( + "fmt" + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/pkg/down" + "github.com/WHUPRJ/woj-server/pkg/unzip" + "github.com/WHUPRJ/woj-server/pkg/utils" + "go.uber.org/zap" + "os" + "path/filepath" +) + +func (s *service) download(version uint, url string) e.Status { + zipPath := filepath.Join(TmpDir, fmt.Sprintf("%d.zip", version)) + problemPath := filepath.Join(ProblemDir, fmt.Sprintf("%d", version)) + + err := down.Down(zipPath, url) + if err != nil { + s.log.Error("download problem failed", zap.Error(err)) + return e.RunnerDownloadFailed + } + + err = unzip.Unzip(zipPath, problemPath) + if err != nil { + s.log.Warn("unzip problem failed", zap.Error(err)) + return e.RunnerUnzipFailed + } + + return e.Success +} + +func (s *service) prebuild(version uint, force bool) e.Status { + if !s.ProblemExists(version) { + return e.RunnerProblemNotExist + } + + mark := filepath.Join(ProblemDir, fmt.Sprintf("%d", version), ".mark.prebuild") + if force { + _ = os.Remove(mark) + } else if utils.FileExist(mark) { + return e.Success + } + + err := s.execute("problem_prebuild.sh", fmt.Sprintf("%d", version)) + + if err != nil { + s.log.Warn("prebuild problem failed", zap.Error(err), zap.Uint("version", version)) + return e.RunnerProblemPrebuildFailed + } + + return e.Success +} + +func (s *service) NewProblem(version uint, url string, force bool) (Config, e.Status) { + if force { + problemPath := filepath.Join(ProblemDir, fmt.Sprintf("%d", version)) + _ = os.RemoveAll(problemPath) + } + + if !s.ProblemExists(version) { + status := s.download(version, url) + if status != e.Success { + return Config{}, status + } + } + + cfg, err := s.ParseConfig(version, false) + if err != nil { + return Config{}, e.RunnerProblemParseFailed + } + + status := s.prebuild(version, true) + if status != e.Success { + return Config{}, status + } + + return cfg, e.Success +} diff --git a/internal/service/runner/runAndJudge.go b/internal/service/runner/runAndJudge.go new file mode 100644 index 0000000..c093c22 --- /dev/null +++ b/internal/service/runner/runAndJudge.go @@ -0,0 +1,22 @@ +package runner + +import "github.com/WHUPRJ/woj-server/internal/e" + +func (s *service) RunAndJudge(version uint, user string, lang string, config *Config) (JudgeStatus, int32, e.Status) { + // run user program + status := s.checkAndExecute(version, user, lang, "problem_run.sh", e.RunnerRunFailed) + if status != e.Success { + return JudgeStatus{Message: "run failed"}, 0, status + } + + // run judger + status = s.checkAndExecute(version, user, lang, "problem_judge.sh", e.RunnerJudgeFailed) + if status != e.Success { + return JudgeStatus{Message: "judge failed"}, 0, status + } + + // check result + result, pts := s.checkResults(user, config) + + return result, pts, e.Success +} diff --git a/internal/service/runner/service.go b/internal/service/runner/service.go new file mode 100644 index 0000000..d9322ac --- /dev/null +++ b/internal/service/runner/service.go @@ -0,0 +1,36 @@ +package runner + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/global" + "go.uber.org/zap" +) + +var _ Service = (*service)(nil) + +type Service interface { + // EnsureDeps build docker images + EnsureDeps(force bool) e.Status + // NewProblem = Download + Parse + Prebuild + NewProblem(version uint, url string, force bool) (Config, e.Status) + + // Compile compile user submission + Compile(version uint, user string, lang string) (JudgeStatus, e.Status) + // RunAndJudge execute user program + RunAndJudge(version uint, user string, lang string, config *Config) (JudgeStatus, int32, e.Status) + + // ParseConfig parse config file + ParseConfig(version uint, skipCheck bool) (Config, error) + // ProblemExists check if problem exists + ProblemExists(version uint) bool +} + +type service struct { + log *zap.Logger +} + +func NewService(g *global.Global) Service { + return &service{ + log: g.Log, + } +} diff --git a/internal/service/runner/status.go b/internal/service/runner/status.go new file mode 100644 index 0000000..6336b95 --- /dev/null +++ b/internal/service/runner/status.go @@ -0,0 +1,228 @@ +package runner + +import ( + "bytes" + "encoding/json" + "encoding/xml" + "fmt" + "github.com/WHUPRJ/woj-server/pkg/utils" + "golang.org/x/text/encoding/charmap" + "io" + "path/filepath" +) + +const ( + VerdictAccepted = iota + VerdictWrongAnswer + VerdictJuryFailed + VerdictPartialCorrect + VerdictTimeLimitExceeded + VerdictMemoryLimitExceeded + VerdictRuntimeError + VerdictCompileError + VerdictSystemError +) + +type TestLibReport struct { + XMLName xml.Name `xml:"result"` + Outcome string `xml:"outcome,attr"` + PCType int `xml:"pctype,attr"` + Points float64 `xml:"points,attr"` + Result string `xml:",chardata"` +} + +type TaskStatus struct { + Id int `json:"id"` + Points int32 `json:"points"` + RealTime int `json:"real_time"` + CpuTime int `json:"cpu_time"` + Memory int `json:"memory"` + Verdict int `json:"verdict"` + Message string `json:"message"` + + infoText []byte + info map[string]interface{} + judgeText string + judge TestLibReport +} + +type JudgeStatus struct { + Message string `json:"message"` + Tasks []TaskStatus `json:"tasks"` +} + +func (t *TaskStatus) getInfoText(infoFile string) *TaskStatus { + if t.Verdict != VerdictAccepted { + return t + } + + var err error + t.infoText, err = utils.FileRead(infoFile) + if err != nil { + t.Verdict = VerdictSystemError + t.Message = "cannot read info file" + } + + return t +} + +func (t *TaskStatus) getInfo() *TaskStatus { + if t.Verdict != VerdictAccepted { + return t + } + + err := json.Unmarshal(t.infoText, &t.info) + if err != nil { + t.Verdict = VerdictSystemError + t.Message = "cannot parse info file" + } else { + t.RealTime = int(t.info["real_time"].(float64)) + t.CpuTime = int(t.info["cpu_time"].(float64)) + t.Memory = int(t.info["memory"].(float64)) + } + + return t +} + +func (t *TaskStatus) checkExit() *TaskStatus { + if t.Verdict != VerdictAccepted { + return t + } + + if t.info["status"] != "exited" || t.info["code"] != 0.0 { + t.Verdict = VerdictRuntimeError + t.Message = fmt.Sprintf("status: %v, code: %v", t.info["status"], t.info["code"]) + } + + return t +} + +func (t *TaskStatus) checkTime(config *Config) *TaskStatus { + if t.Verdict != VerdictAccepted { + return t + } + + if t.info["real_time"].(float64) > float64(config.Runtime.TimeLimit)+5 { + t.Verdict = VerdictTimeLimitExceeded + t.Message = fmt.Sprintf("real_time: %v cpu_time: %v", t.info["real_time"], t.info["cpu_time"]) + } + + return t +} + +func (t *TaskStatus) checkMemory(config *Config) *TaskStatus { + if t.Verdict != VerdictAccepted { + return t + } + + if t.info["memory"].(float64) > float64((config.Runtime.MemoryLimit+1)*1024) { + t.Verdict = VerdictMemoryLimitExceeded + t.Message = fmt.Sprintf("memory: %v", t.info["memory"]) + } + + return t +} + +func (t *TaskStatus) getJudgeText(judgeFile string) *TaskStatus { + if t.Verdict != VerdictAccepted { + return t + } + + j, err := utils.FileRead(judgeFile) + if err != nil { + t.Verdict = VerdictSystemError + t.Message = "cannot read judge file" + } else { + t.judgeText = string(j) + } + + return t +} + +func (t *TaskStatus) getJudge() *TaskStatus { + if t.Verdict != VerdictAccepted { + return t + } + + b := bytes.NewReader([]byte(t.judgeText)) + d := xml.NewDecoder(b) + d.CharsetReader = func(charset string, input io.Reader) (io.Reader, error) { + switch charset { + case "windows-1251": + return charmap.Windows1251.NewDecoder().Reader(input), nil + default: + return nil, fmt.Errorf("unknown charset: %s", charset) + } + } + + err := d.Decode(&t.judge) + if err != nil { + t.Verdict = VerdictSystemError + t.Message = "cannot parse judge file" + } + + return t +} + +func (t *TaskStatus) checkJudge(pts *map[int]int32) *TaskStatus { + if t.Verdict != VerdictAccepted { + return t + } + + mp := map[string]int{ + "accepted": VerdictAccepted, + "wrong-answer": VerdictWrongAnswer, + "presentation-error": VerdictWrongAnswer, + "points": VerdictPartialCorrect, + "relative-scoring": VerdictPartialCorrect, + } + + if v, ok := mp[t.judge.Outcome]; ok { + t.Verdict = v + t.Message = t.judge.Result + if v == VerdictAccepted { + t.Points = (*pts)[t.Id] + } else if v == VerdictPartialCorrect { + t.Points = int32(t.judge.Points) + int32(t.judge.PCType) + } + } else { + t.Verdict = VerdictJuryFailed + t.Message = fmt.Sprintf("unknown outcome: %v, result: %v", t.judge.Outcome, t.judge.Result) + } + + return t +} + +func (s *service) checkResults(user string, config *Config) (JudgeStatus, int32) { + // CE will be processed in phase compile + + pts := map[int]int32{} + for _, task := range config.Tasks { + pts[task.Id] = task.Points + } + + var results []TaskStatus + dir := filepath.Join(UserDir, user) + var sum int32 = 0 + + for i := 1; i <= len(config.Tasks); i++ { + result := TaskStatus{Id: i, Verdict: VerdictAccepted, Points: 0} + + info := filepath.Join(dir, fmt.Sprintf("%d.info", i)) + judge := filepath.Join(dir, fmt.Sprintf("%d.judge", i)) + + result.getInfoText(info). + getInfo(). + checkTime(config). + checkMemory(config). + checkExit(). + getJudgeText(judge). + getJudge(). + checkJudge(&pts) + + sum += result.Points + results = append(results, result) + } + + return JudgeStatus{Message: "", Tasks: results}, sum +} diff --git a/internal/service/status/create.go b/internal/service/status/create.go new file mode 100644 index 0000000..4c863d6 --- /dev/null +++ b/internal/service/status/create.go @@ -0,0 +1,36 @@ +package status + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/model" + "github.com/jackc/pgtype" + "go.uber.org/zap" +) + +type CreateData struct { + SubmissionID uint + ProblemVersionID uint + Context string + Point int32 +} + +func (s service) Create(data *CreateData) (*model.Status, e.Status) { + status := &model.Status{ + SubmissionID: data.SubmissionID, + ProblemVersionID: data.ProblemVersionID, + Context: pgtype.JSON{ + Bytes: []byte(data.Context), + Status: pgtype.Present, + }, + Point: data.Point, + IsEnabled: true, + } + + err := s.db.Create(status).Error + if err != nil { + s.log.Warn("DatabaseError", zap.Error(err), zap.Any("status", status)) + return nil, e.DatabaseError + } + + return status, e.Success +} diff --git a/internal/service/status/query.go b/internal/service/status/query.go new file mode 100644 index 0000000..dd474b5 --- /dev/null +++ b/internal/service/status/query.go @@ -0,0 +1,56 @@ +package status + +import ( + "errors" + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/model" + "go.uber.org/zap" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +func (s service) Query(sid uint, associations bool) (*model.Status, e.Status) { + + status := &model.Status{ + SubmissionID: sid, + IsEnabled: true, + } + + query := s.db + if associations { + query = query.Preload(clause.Associations) + } + + err := query. + Where(status). + Last(&status).Error + + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, e.StatusNotFound + } + + if err != nil { + s.log.Warn("DatabaseError", zap.Error(err), zap.Any("status", status)) + return nil, e.DatabaseError + } + return status, e.Success +} + +func (s service) QueryByVersion(pvid uint, offset int, limit int) ([]*model.Status, e.Status) { + var statuses []*model.Status + status := &model.Status{ + ProblemVersionID: pvid, + IsEnabled: true, + } + + err := s.db.Preload(clause.Associations). + Where(status). + Limit(limit). + Offset(offset). + Find(&statuses).Error + if err != nil { + s.log.Warn("DatabaseError", zap.Error(err), zap.Any("status", status)) + return nil, e.DatabaseError + } + return statuses, e.Success +} diff --git a/internal/service/status/service.go b/internal/service/status/service.go new file mode 100644 index 0000000..70a95b8 --- /dev/null +++ b/internal/service/status/service.go @@ -0,0 +1,29 @@ +package status + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/global" + "github.com/WHUPRJ/woj-server/internal/model" + "go.uber.org/zap" + "gorm.io/gorm" +) + +var _ Service = (*service)(nil) + +type Service interface { + Create(data *CreateData) (*model.Status, e.Status) + Query(sid uint, associations bool) (*model.Status, e.Status) + QueryByVersion(pvid uint, offset int, limit int) ([]*model.Status, e.Status) +} + +type service struct { + log *zap.Logger + db *gorm.DB +} + +func NewService(g *global.Global) Service { + return &service{ + log: g.Log, + db: g.Db.Get().(*gorm.DB), + } +} diff --git a/internal/service/storage/get.go b/internal/service/storage/get.go new file mode 100644 index 0000000..c65f3cd --- /dev/null +++ b/internal/service/storage/get.go @@ -0,0 +1,30 @@ +package storage + +import ( + "context" + "github.com/WHUPRJ/woj-server/internal/e" + "go.uber.org/zap" + "net/url" + "time" +) + +func (s *service) Get(objectName string, expiry time.Duration) (string, e.Status) { + preSignedURL, err := s.client.PresignedGetObject( + context.Background(), + s.bucket, + objectName, + expiry, + url.Values{}, + ) + + if err != nil { + s.log.Warn("failed to generate pre-signed get url", + zap.Error(err), + zap.String("objectName", objectName), + zap.Duration("expiry", expiry), + ) + return "", e.StorageGetFailed + } + + return preSignedURL.String(), e.Success +} diff --git a/internal/service/storage/service.go b/internal/service/storage/service.go new file mode 100644 index 0000000..633b336 --- /dev/null +++ b/internal/service/storage/service.go @@ -0,0 +1,41 @@ +package storage + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/global" + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" + "go.uber.org/zap" + "time" +) + +var _ Service = (*service)(nil) + +type Service interface { + Upload(objectName string, expiry time.Duration) (string, e.Status) + Get(objectName string, expiry time.Duration) (string, e.Status) +} + +type service struct { + log *zap.Logger + client *minio.Client + bucket string +} + +func NewService(g *global.Global) Service { + minioClient, err := minio.New(g.Conf.Storage.Endpoint, &minio.Options{ + Creds: credentials.NewStaticV4(g.Conf.Storage.AccessKey, g.Conf.Storage.SecretKey, ""), + Secure: g.Conf.Storage.UseSSL, + }) + + if err != nil { + g.Log.Fatal("failed to create minio client", zap.Error(err)) + return nil + } + + return &service{ + log: g.Log, + client: minioClient, + bucket: g.Conf.Storage.Bucket, + } +} diff --git a/internal/service/storage/upload.go b/internal/service/storage/upload.go new file mode 100644 index 0000000..69e7627 --- /dev/null +++ b/internal/service/storage/upload.go @@ -0,0 +1,28 @@ +package storage + +import ( + "context" + "github.com/WHUPRJ/woj-server/internal/e" + "go.uber.org/zap" + "time" +) + +func (s *service) Upload(objectName string, expiry time.Duration) (string, e.Status) { + preSignedURL, err := s.client.PresignedPutObject( + context.Background(), + s.bucket, + objectName, + expiry, + ) + + if err != nil { + s.log.Warn("failed to generate pre-signed upload url", + zap.Error(err), + zap.String("objectName", objectName), + zap.Duration("expiry", expiry), + ) + return "", e.StorageUploadFailed + } + + return preSignedURL.String(), e.Success +} diff --git a/internal/service/submission/create.go b/internal/service/submission/create.go new file mode 100644 index 0000000..2c919d6 --- /dev/null +++ b/internal/service/submission/create.go @@ -0,0 +1,31 @@ +package submission + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/model" + "go.uber.org/zap" +) + +type CreateData struct { + ProblemID uint + UserID uint + Language string + Code string +} + +func (s *service) Create(data *CreateData) (*model.Submission, e.Status) { + submission := &model.Submission{ + ProblemID: data.ProblemID, + UserID: data.UserID, + Language: data.Language, + Code: data.Code, + } + + err := s.db.Create(submission).Error + if err != nil { + s.log.Warn("DatabaseError", zap.Error(err), zap.Any("submission", submission)) + return nil, e.DatabaseError + } + + return submission, e.Success +} diff --git a/internal/service/submission/query.go b/internal/service/submission/query.go new file mode 100644 index 0000000..7dc6e7f --- /dev/null +++ b/internal/service/submission/query.go @@ -0,0 +1,57 @@ +package submission + +import ( + "errors" + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/model" + "go.uber.org/zap" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +func (s *service) Query(pid uint, uid uint, offset int, limit int) ([]*model.Submission, e.Status) { + submissions := make([]*model.Submission, 0) + + submission := &model.Submission{ + ProblemID: pid, + UserID: uid, + } + + err := s.db.Preload(clause.Associations). + Where(submission). + Limit(limit). + Offset(offset). + Find(&submissions).Error + + //if errors.Is(err, gorm.ErrRecordNotFound) { + // return nil, e.ProblemNotFound + //} + + if err != nil { + s.log.Warn("DatabaseError", zap.Error(err), zap.Any("pid", pid), zap.Any("uid", uid)) + return nil, e.DatabaseError + } + return submissions, e.Success +} + +func (s *service) QueryBySid(sid uint, associations bool) (*model.Submission, e.Status) { + submission := new(model.Submission) + + query := s.db + if associations { + query = query.Preload(clause.Associations) + } + + err := query.First(&submission, sid).Error + + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, e.SubmissionNotFound + } + + if err != nil { + s.log.Warn("DatabaseError", zap.Error(err), zap.Any("sid", sid)) + return nil, e.DatabaseError + } + + return submission, e.Success +} diff --git a/internal/service/submission/service.go b/internal/service/submission/service.go new file mode 100644 index 0000000..b5e8095 --- /dev/null +++ b/internal/service/submission/service.go @@ -0,0 +1,29 @@ +package submission + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/global" + "github.com/WHUPRJ/woj-server/internal/model" + "go.uber.org/zap" + "gorm.io/gorm" +) + +var _ Service = (*service)(nil) + +type Service interface { + Create(data *CreateData) (*model.Submission, e.Status) + Query(pid uint, uid uint, offset int, limit int) ([]*model.Submission, e.Status) + QueryBySid(sid uint, associations bool) (*model.Submission, e.Status) +} + +type service struct { + log *zap.Logger + db *gorm.DB +} + +func NewService(g *global.Global) Service { + return &service{ + log: g.Log, + db: g.Db.Get().(*gorm.DB), + } +} diff --git a/internal/service/task/common.go b/internal/service/task/common.go new file mode 100644 index 0000000..e88844c --- /dev/null +++ b/internal/service/task/common.go @@ -0,0 +1,28 @@ +package task + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/hibiken/asynq" + "go.uber.org/zap" +) + +func (s *service) submit(typename string, payload []byte, queue string) (*asynq.TaskInfo, e.Status) { + task := asynq.NewTask(typename, payload) + + info, err := s.queue.Enqueue(task, asynq.Queue(queue)) + if err != nil { + s.log.Warn("failed to enqueue task", zap.Error(err), zap.Any("task", task)) + return nil, e.TaskEnqueueFailed + } + + return info, e.Success +} + +func (s *service) GetTaskInfo(id string, queue string) (*asynq.TaskInfo, e.Status) { + task, err := s.inspector.GetTaskInfo(queue, id) + if err != nil { + s.log.Debug("get task info failed", zap.Error(err), zap.String("id", id)) + return nil, e.TaskGetInfoFailed + } + return task, e.Success +} diff --git a/internal/service/task/problem.go b/internal/service/task/problem.go new file mode 100644 index 0000000..327b003 --- /dev/null +++ b/internal/service/task/problem.go @@ -0,0 +1,38 @@ +package task + +import ( + "encoding/json" + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/model" + "go.uber.org/zap" +) + +func (s *service) ProblemBuild(data *model.ProblemBuildPayload) (string, e.Status) { + payload, err := json.Marshal(data) + if err != nil { + s.log.Warn("json marshal error", + zap.Error(err), + zap.Any("data", data), + ) + return "", e.InternalError + } + + info, status := s.submit(model.TypeProblemBuild, payload, model.QueueRunner) + + return info.ID, status +} + +func (s *service) ProblemUpdate(data *model.ProblemUpdatePayload) (string, e.Status) { + payload, err := json.Marshal(data) + if err != nil { + s.log.Warn("json marshal error", + zap.Error(err), + zap.Any("data", data), + ) + return "", e.InternalError + } + + info, status := s.submit(model.TypeProblemUpdate, payload, model.QueueServer) + + return info.ID, status +} diff --git a/internal/service/task/service.go b/internal/service/task/service.go new file mode 100644 index 0000000..f1931bd --- /dev/null +++ b/internal/service/task/service.go @@ -0,0 +1,40 @@ +package task + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/global" + "github.com/WHUPRJ/woj-server/internal/model" + "github.com/WHUPRJ/woj-server/internal/service/runner" + "github.com/hibiken/asynq" + "go.uber.org/zap" +) + +var _ Service = (*service)(nil) + +type Service interface { + ProblemBuild(data *model.ProblemBuildPayload) (string, e.Status) + ProblemUpdate(data *model.ProblemUpdatePayload) (string, e.Status) + SubmitJudge(data *model.SubmitJudgePayload) (string, e.Status) + SubmitUpdate(data *model.SubmitUpdatePayload, ctx runner.JudgeStatus) (string, e.Status) + + GetTaskInfo(string, string) (*asynq.TaskInfo, e.Status) +} + +type service struct { + log *zap.Logger + queue *asynq.Client + inspector *asynq.Inspector +} + +func NewService(g *global.Global) Service { + redisOpt := asynq.RedisClientOpt{ + Addr: g.Conf.Redis.Address, + Password: g.Conf.Redis.Password, + DB: g.Conf.Redis.QueueDb, + } + return &service{ + log: g.Log, + queue: asynq.NewClient(redisOpt), + inspector: asynq.NewInspector(redisOpt), + } +} diff --git a/internal/service/task/submit.go b/internal/service/task/submit.go new file mode 100644 index 0000000..c588c6b --- /dev/null +++ b/internal/service/task/submit.go @@ -0,0 +1,49 @@ +package task + +import ( + "encoding/json" + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/model" + "github.com/WHUPRJ/woj-server/internal/service/runner" + "go.uber.org/zap" +) + +func (s *service) SubmitJudge(data *model.SubmitJudgePayload) (string, e.Status) { + payload, err := json.Marshal(data) + if err != nil { + s.log.Warn("json marshal error", + zap.Error(err), + zap.Any("data", data), + ) + return "", e.InternalError + } + + info, status := s.submit(model.TypeSubmitJudge, payload, model.QueueRunner) + + return info.ID, status +} + +func (s *service) SubmitUpdate(data *model.SubmitUpdatePayload, ctx runner.JudgeStatus) (string, e.Status) { + ctxText, err := json.Marshal(ctx) + if err != nil { + s.log.Warn("json marshal error", + zap.Error(err), + zap.Any("ctx", ctx)) + return "", e.InternalError + } + + data.Context = string(ctxText) + payload, err := json.Marshal(data) + if err != nil { + s.log.Warn("json marshal error", + zap.Error(err), + zap.Any("data", data), + zap.Any("Context", ctx), + ) + return "", e.InternalError + } + + info, status := s.submit(model.TypeSubmitUpdate, payload, model.QueueServer) + + return info.ID, status +} diff --git a/internal/service/user/create.go b/internal/service/user/create.go new file mode 100644 index 0000000..f91cd53 --- /dev/null +++ b/internal/service/user/create.go @@ -0,0 +1,41 @@ +package user + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/model" + "go.uber.org/zap" + "golang.org/x/crypto/bcrypt" + "strings" +) + +type CreateData struct { + UserName string + Password string + NickName string +} + +func (s *service) Create(data *CreateData) (*model.User, e.Status) { + hashed, err := bcrypt.GenerateFromPassword([]byte(data.Password), bcrypt.DefaultCost) + if err != nil { + s.log.Warn("BcryptError", zap.Error(err), zap.String("password", data.Password)) + return nil, e.InternalError + } + + user := &model.User{ + UserName: data.UserName, + Password: hashed, + NickName: data.NickName, + Role: model.RoleGeneral, + IsEnabled: true, + } + + err = s.db.Create(user).Error + if err != nil && strings.Contains(err.Error(), "duplicate key") { + return nil, e.UserDuplicated + } + if err != nil { + s.log.Warn("DatabaseError", zap.Error(err), zap.Any("user", user)) + return nil, e.DatabaseError + } + return user, e.Success +} diff --git a/internal/service/user/login.go b/internal/service/user/login.go new file mode 100644 index 0000000..d80acbd --- /dev/null +++ b/internal/service/user/login.go @@ -0,0 +1,39 @@ +package user + +import ( + "errors" + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/model" + "go.uber.org/zap" + "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" +) + +type LoginData struct { + UserName string + Password string +} + +func (s *service) Login(data *LoginData) (*model.User, e.Status) { + user := &model.User{UserName: data.UserName} + + err := s.db.Where(user).First(&user).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, e.UserNotFound + } + if err != nil { + s.log.Warn("DatabaseError", zap.Error(err), zap.Any("user", user)) + return nil, e.DatabaseError + } + + if !user.IsEnabled { + return nil, e.UserDisabled + } + + err = bcrypt.CompareHashAndPassword(user.Password, []byte(data.Password)) + if err != nil { + return nil, e.UserWrongPassword + } + + return user, e.Success +} diff --git a/internal/service/user/profile.go b/internal/service/user/profile.go new file mode 100644 index 0000000..7cd706c --- /dev/null +++ b/internal/service/user/profile.go @@ -0,0 +1,24 @@ +package user + +import ( + "errors" + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/model" + "go.uber.org/zap" + "gorm.io/gorm" +) + +func (s *service) Profile(uid uint) (*model.User, e.Status) { + user := new(model.User) + + err := s.db.First(&user, uid).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, e.UserNotFound + } + if err != nil { + s.log.Warn("DatabaseError", zap.Error(err), zap.Any("uid", uid)) + return nil, e.DatabaseError + } + + return user, e.Success +} diff --git a/internal/service/user/service.go b/internal/service/user/service.go new file mode 100644 index 0000000..4baeffd --- /dev/null +++ b/internal/service/user/service.go @@ -0,0 +1,33 @@ +package user + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/global" + "github.com/WHUPRJ/woj-server/internal/model" + "github.com/go-redis/redis/v8" + "go.uber.org/zap" + "gorm.io/gorm" +) + +var _ Service = (*service)(nil) + +type Service interface { + Create(data *CreateData) (*model.User, e.Status) + Login(data *LoginData) (*model.User, e.Status) + IncrVersion(uid uint) (int64, e.Status) + Profile(uid uint) (*model.User, e.Status) +} + +type service struct { + log *zap.Logger + db *gorm.DB + redis *redis.Client +} + +func NewService(g *global.Global) Service { + return &service{ + log: g.Log, + db: g.Db.Get().(*gorm.DB), + redis: g.Redis.Get().(*redis.Client), + } +} diff --git a/internal/service/user/version.go b/internal/service/user/version.go new file mode 100644 index 0000000..8303af8 --- /dev/null +++ b/internal/service/user/version.go @@ -0,0 +1,18 @@ +package user + +import ( + "context" + "fmt" + "github.com/WHUPRJ/woj-server/internal/e" + "go.uber.org/zap" +) + +func (s *service) IncrVersion(uid uint) (int64, e.Status) { + version, err := s.redis.Incr(context.Background(), fmt.Sprintf("Version:%d", uid)).Result() + if err != nil { + s.log.Warn("RedisError", zap.Error(err), zap.Any("uid", uid)) + return -1, e.RedisError + } + + return version, e.Success +} diff --git a/pkg/down/down.go b/pkg/down/down.go new file mode 100644 index 0000000..d4b30b9 --- /dev/null +++ b/pkg/down/down.go @@ -0,0 +1,55 @@ +package down + +import ( + "fmt" + "github.com/WHUPRJ/woj-server/pkg/utils" + "io" + "net/http" + "os" + "path/filepath" +) + +func Down(dest string, url string) error { + dir := filepath.Dir(dest) + err := os.MkdirAll(dir, 0755) + if err != nil { + return err + } + + tmp := fmt.Sprintf("%s.%s", dest, utils.RandomString(5)) + f, err := os.Create(tmp) + if err != nil { + return err + } + + resp, err := http.Get(url) + if err != nil { + return err + } + defer func(Body io.ReadCloser) { + _ = Body.Close() + }(resp.Body) + + if resp.StatusCode != http.StatusOK { + _ = os.Remove(tmp) + return fmt.Errorf("bad status %s when accessing %s", resp.Status, url) + } + + _, err = io.Copy(f, resp.Body) + if err != nil { + return err + } + + err = f.Close() + if err != nil { + return err + } + + err = os.Rename(tmp, dest) + if err != nil { + _ = os.Remove(dest) + return err + } + + return nil +} diff --git a/pkg/unzip/unzip.go b/pkg/unzip/unzip.go new file mode 100644 index 0000000..98bac57 --- /dev/null +++ b/pkg/unzip/unzip.go @@ -0,0 +1,60 @@ +package unzip + +import ( + "archive/zip" + "io" + "os" + "path/filepath" +) + +func Unzip(src, dest string) error { + r, err := zip.OpenReader(src) + if err != nil { + return err + } + defer func(r *zip.ReadCloser) { + _ = r.Close() + }(r) + + handler := func(f *zip.File, dest string) error { + rc, err := f.Open() + if err != nil { + return err + } + defer func(rc io.ReadCloser) { + _ = rc.Close() + }(rc) + + path := filepath.Join(dest, f.Name) + if f.FileInfo().IsDir() { + _ = os.MkdirAll(path, 0755) + } else { + _ = os.MkdirAll(filepath.Dir(path), 0755) + + file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + return err + } + defer func(file *os.File) { + _ = file.Close() + }(file) + + _, err = io.Copy(file, rc) + if err != nil { + return err + } + } + + return nil + } + + for _, f := range r.File { + err := handler(f, dest) + if err != nil { + _ = os.RemoveAll(dest) + return err + } + } + + return nil +} diff --git a/pkg/utils/file.go b/pkg/utils/file.go index c05458c..9ae7939 100644 --- a/pkg/utils/file.go +++ b/pkg/utils/file.go @@ -3,6 +3,7 @@ package utils import ( "io" "os" + "path/filepath" ) func FileRead(filePath string) ([]byte, error) { @@ -22,7 +23,17 @@ func FileExist(filePath string) bool { return If(err == nil || os.IsExist(err), true, false).(bool) } +func FileEmpty(filePath string) bool { + stat, err := os.Stat(filePath) + if err != nil { + return true + } + return stat.Size() == 0 +} + func FileTouch(filePath string) bool { + base := filepath.Dir(filePath) + _ = os.MkdirAll(base, 0755) _, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, 0644) return If(err == nil, true, false).(bool) } diff --git a/pkg/zapasynq/logger.go b/pkg/zapasynq/logger.go new file mode 100644 index 0000000..f23f8b3 --- /dev/null +++ b/pkg/zapasynq/logger.go @@ -0,0 +1,25 @@ +package zapasynq + +import ( + "go.uber.org/zap" +) + +type Logger struct { + logger *zap.Logger +} + +func New(zapLogger *zap.Logger) Logger { + return Logger{ + logger: zapLogger, + } +} + +func (l Logger) Debug(args ...interface{}) { l.logger.Sugar().Debugf(args[0].(string), args[1:]...) } + +func (l Logger) Info(args ...interface{}) { l.logger.Sugar().Infof(args[0].(string), args[1:]...) } + +func (l Logger) Warn(args ...interface{}) { l.logger.Sugar().Warnf(args[0].(string), args[1:]...) } + +func (l Logger) Error(args ...interface{}) { l.logger.Sugar().Errorf(args[0].(string), args[1:]...) } + +func (l Logger) Fatal(args ...interface{}) { l.logger.Sugar().Fatalf(args[0].(string), args[1:]...) } diff --git a/resource/runner/.gitignore b/resource/runner/.gitignore new file mode 100644 index 0000000..0cf8d7b --- /dev/null +++ b/resource/runner/.gitignore @@ -0,0 +1,2 @@ +.mark.* +*.zip diff --git a/resource/runner/framework/scripts/.gitignore b/resource/runner/framework/scripts/.gitignore new file mode 100644 index 0000000..bf3d440 --- /dev/null +++ b/resource/runner/framework/scripts/.gitignore @@ -0,0 +1,2 @@ +/libwoj_sandbox.so +/woj_launcher diff --git a/resource/runner/framework/scripts/setup.sh b/resource/runner/framework/scripts/setup.sh new file mode 100755 index 0000000..b93b328 --- /dev/null +++ b/resource/runner/framework/scripts/setup.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -x + +rm -rf woj-sandbox +git clone https://github.com/WHUPRJ/woj-sandbox.git >/dev/null 2>&1 || exit 1 +cd woj-sandbox && ./build_libseccomp.sh || exit 1 + +mkdir -p build && cd build || exit 1 +cmake .. -DCMAKE_BUILD_TYPE=Release || exit 1 +make -j || exit 1 + +cd ../.. +cp woj-sandbox/build/libwoj_sandbox.so . || exit 1 +cp woj-sandbox/build/woj_launcher . || exit 1 +rm -rf woj-sandbox diff --git a/resource/runner/framework/template/.gitignore b/resource/runner/framework/template/.gitignore new file mode 100644 index 0000000..39fbd8f --- /dev/null +++ b/resource/runner/framework/template/.gitignore @@ -0,0 +1 @@ +/testlib \ No newline at end of file diff --git a/resource/runner/framework/template/Judger.mk b/resource/runner/framework/template/Judger.mk new file mode 100644 index 0000000..724c865 --- /dev/null +++ b/resource/runner/framework/template/Judger.mk @@ -0,0 +1,10 @@ +FCMP = $(TESTLIB)/checkers/fcmp +HCMP = $(TESTLIB)/checkers/hcmp +LCMP = $(TESTLIB)/checkers/lcmp +NCMP = $(TESTLIB)/checkers/ncmp +NYESNO = $(TESTLIB)/checkers/nyesno +RCMP4 = $(TESTLIB)/checkers/rcmp4 +RCMP6 = $(TESTLIB)/checkers/rcmp6 +RCMP9 = $(TESTLIB)/checkers/rcmp9 +WCMP = $(TESTLIB)/checkers/wcmp +YESNO = $(TESTLIB)/checkers/yesno diff --git a/resource/runner/framework/template/c.mk b/resource/runner/framework/template/c.mk new file mode 100644 index 0000000..756b5be --- /dev/null +++ b/resource/runner/framework/template/c.mk @@ -0,0 +1,2 @@ +CC = gcc +CFLAGS = -O2 -pipe -Wall -lm -std=c99 -DONLINE_JUDGE \ No newline at end of file diff --git a/resource/runner/framework/template/cpp.mk b/resource/runner/framework/template/cpp.mk new file mode 100644 index 0000000..6febe43 --- /dev/null +++ b/resource/runner/framework/template/cpp.mk @@ -0,0 +1,2 @@ +CXX = g++ +CFLAGS = -O2 -pipe -Wall -lm -std=c++20 -DONLINE_JUDGE \ No newline at end of file diff --git a/resource/runner/framework/template/default/c.Makefile b/resource/runner/framework/template/default/c.Makefile new file mode 100644 index 0000000..25be642 --- /dev/null +++ b/resource/runner/framework/template/default/c.Makefile @@ -0,0 +1,7 @@ +include ${TEMPLATE}/c.mk ${TEMPLATE}/Judger.mk + +compile: + @$(CC) $(CFLAGS) -o $(PREFIX)/user/$(USER_PROG).out $(PREFIX)/user/$(USER_PROG).$(LANG) + +judge: + $($(CMP)) $(PREFIX)/data/input/$(TEST_NUM).input $(PREFIX)/user/$(TEST_NUM).out.usr $(PREFIX)/data/output/$(TEST_NUM).output $(PREFIX)/user/$(TEST_NUM).judge -appes diff --git a/resource/runner/framework/template/default/cpp.Makefile b/resource/runner/framework/template/default/cpp.Makefile new file mode 100644 index 0000000..ea648d2 --- /dev/null +++ b/resource/runner/framework/template/default/cpp.Makefile @@ -0,0 +1,7 @@ +include ${TEMPLATE}/cpp.mk ${TEMPLATE}/Judger.mk + +compile: + @$(CXX) $(CFLAGS) -o $(PREFIX)/user/$(USER_PROG).out $(PREFIX)/user/$(USER_PROG).$(LANG) + +judge: + $($(CMP)) $(PREFIX)/data/input/$(TEST_NUM).input $(PREFIX)/user/$(TEST_NUM).out.usr $(PREFIX)/data/output/$(TEST_NUM).output $(PREFIX)/user/$(TEST_NUM).judge -appes diff --git a/resource/runner/framework/template/setup.sh b/resource/runner/framework/template/setup.sh new file mode 100755 index 0000000..59c117f --- /dev/null +++ b/resource/runner/framework/template/setup.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -x +rm -rf testlib +git clone --depth=1 https://github.com/MikeMirzayanov/testlib.git >/dev/null 2>&1 || exit 1 +rm -rf testlib/.git +rm -rf testlib/tests +cd testlib/checkers || exit 1 +parallel clang++ -Ofast -march=native -Wall -pipe -I.. {}.cpp -o {} ::: fcmp hcmp lcmp ncmp nyesno rcmp4 rcmp6 rcmp9 wcmp yesno diff --git a/resource/runner/problem/example/.gitignore b/resource/runner/problem/example/.gitignore new file mode 100644 index 0000000..cb3d7fa --- /dev/null +++ b/resource/runner/problem/example/.gitignore @@ -0,0 +1,5 @@ +/judge/*.out +/data/input/2.* +/data/input/4.* +/data/output/2.* +/data/output/4.* diff --git a/resource/runner/problem/example/README.md b/resource/runner/problem/example/README.md new file mode 100644 index 0000000..ba9485d --- /dev/null +++ b/resource/runner/problem/example/README.md @@ -0,0 +1,64 @@ +# 示例题目 + +## 文件构成 + +``` +. +├── config.json # 题目配置信息 +├── data # 数据目录 +│ ├── input # 输入数据 +│ │ ├── (x).input # 第 x 组输入数据 +│ │ └── ... +│ └── output # 输出数据 +│ ├── (x).output # 第 x 组输出数据 +│ └── ... +└── judge # 评测脚本目录 + ├── c.Makefile # (optional) 自定义评测脚本 + ├── prebuild.Makefile # (optional) 题目初始化脚本 + └── ... +``` + +## 详细说明 + +### 题目配置信息 + +```json5 +{ + "Runtime": { + // 运行时配置 + "TimeLimit": 1000, // 时间限制 (ms) + "MemoryLimit": 16, // 内存限制 (MB) + "NProcLimit": 1 // 进(线)程 限制 + }, + "Languages": [ + {"Lang": "c", "Type": "custom", "Script": "XYZ.Makefile", "Cmp": ""}, + {"Lang": "cpp", "Type": "default", "Script": "", "Cmp": "NCMP"} + ], // 支持的语言 + "Tasks": [ + // 评测点信息 + {"Id": 1, "Points": 25}, // 第一个评测点,分值 25 分,使用 ./data/1.? 为测试数据 + {"Id": 2, "Points": 25}, + {"Id": 3, "Points": 25}, + {"Id": 4, "Points": 25} + ] +} +``` + +### 评测脚本 + +见注释 + + + + + + + + + + + + + + + diff --git a/resource/runner/problem/example/config.json b/resource/runner/problem/example/config.json new file mode 100644 index 0000000..54dbaff --- /dev/null +++ b/resource/runner/problem/example/config.json @@ -0,0 +1,17 @@ +{ + "Runtime": { + "TimeLimit": 1000, + "MemoryLimit": 16, + "NProcLimit": 1 + }, + "Languages": [ + {"Lang": "c", "Type": "custom", "Script": "XYZ.Makefile", "Cmp": ""}, + {"Lang": "cpp", "Type": "default", "Script": "", "Cmp": "NCMP"} + ], + "Tasks": [ + {"Id": 1, "Points": 25}, + {"Id": 2, "Points": 25}, + {"Id": 3, "Points": 25}, + {"Id": 4, "Points": 25} + ] +} \ No newline at end of file diff --git a/resource/runner/problem/example/data/input/1.input b/resource/runner/problem/example/data/input/1.input new file mode 100644 index 0000000..8d04f96 --- /dev/null +++ b/resource/runner/problem/example/data/input/1.input @@ -0,0 +1 @@ +1 2 diff --git a/resource/runner/problem/example/data/input/3.input b/resource/runner/problem/example/data/input/3.input new file mode 100644 index 0000000..6fa0970 --- /dev/null +++ b/resource/runner/problem/example/data/input/3.input @@ -0,0 +1 @@ +-1 -2 \ No newline at end of file diff --git a/resource/runner/problem/example/data/output/1.output b/resource/runner/problem/example/data/output/1.output new file mode 100644 index 0000000..00750ed --- /dev/null +++ b/resource/runner/problem/example/data/output/1.output @@ -0,0 +1 @@ +3 diff --git a/resource/runner/problem/example/data/output/3.output b/resource/runner/problem/example/data/output/3.output new file mode 100644 index 0000000..7d105a7 --- /dev/null +++ b/resource/runner/problem/example/data/output/3.output @@ -0,0 +1 @@ +-3 \ No newline at end of file diff --git a/resource/runner/problem/example/judge/XYZ.Makefile b/resource/runner/problem/example/judge/XYZ.Makefile new file mode 100644 index 0000000..d15d183 --- /dev/null +++ b/resource/runner/problem/example/judge/XYZ.Makefile @@ -0,0 +1,22 @@ +# 默认环境变量有 PREFIX, TEMPLATE, TESTLIB +include ${TEMPLATE}/c.mk ${TEMPLATE}/Judger.mk + +# 评测分四个阶段 +# 1. prebuild: 用于提前生成测试数据、评测器、spj等工具,runner 只执行一次 +# 只有 ./data, ./judge 目录可见 +# 2. compile: 用于编译用户提交的程序 +# 只有 ./user/$(USER_PROG).$(LANG) 和 ./judge 目录可见 +# 3. run: 运行用户程序 +# 只有 ./data/input/*.input 和 ./user/$(USER_PROG).out 可见 +# 用户输出存放于 ./user/?.out.usr +# 使用 woj-sandbox 运行,等效于 $(PREFIX)/user/$(USER_PROG).out < $(PREFIX)/data/input/$(TEST_NUM).input > $(PREFIX)/user/$(TEST_NUM).out.usr +# 4. judge: 用于判定输出结果 环境变量 TEST_NUM 表示当前测试点编号 +# 所有目录 ./data ./judge ./user 可见 + +compile: + $(CC) $(CFLAGS) -o $(PREFIX)/user/$(USER_PROG).out $(PREFIX)/user/$(USER_PROG).$(LANG) $(PREFIX)/judge/gadget.c + +judge: + # Rename on *.out.usr or *.judge is not allowed + sed '/gadgets/d' $(PREFIX)/user/$(TEST_NUM).out.usr > $(PREFIX)/user/$(TEST_NUM).out.usr1 + $(NCMP) $(PREFIX)/data/input/$(TEST_NUM).input $(PREFIX)/user/$(TEST_NUM).out.usr1 $(PREFIX)/data/output/$(TEST_NUM).output $(PREFIX)/user/$(TEST_NUM).judge -appes diff --git a/resource/runner/problem/example/judge/gadget.c b/resource/runner/problem/example/judge/gadget.c new file mode 100644 index 0000000..bc08434 --- /dev/null +++ b/resource/runner/problem/example/judge/gadget.c @@ -0,0 +1,3 @@ +#include +void construct() __attribute__((constructor(101))); +void construct() { puts("Greetings from gadgets..."); } diff --git a/resource/runner/problem/example/judge/gen.cpp b/resource/runner/problem/example/judge/gen.cpp new file mode 100644 index 0000000..55b3e38 --- /dev/null +++ b/resource/runner/problem/example/judge/gen.cpp @@ -0,0 +1,9 @@ +#include "testlib.h" + +using namespace std; + +int main(int argc, char *argv[]) { + registerGen(argc, argv, 1); + + println(rnd.next(-1000000000, 1000000000), rnd.next(-1000000000, 1000000000)); +} diff --git a/resource/runner/problem/example/judge/prebuild.Makefile b/resource/runner/problem/example/judge/prebuild.Makefile new file mode 100644 index 0000000..39daf6f --- /dev/null +++ b/resource/runner/problem/example/judge/prebuild.Makefile @@ -0,0 +1,12 @@ +include ${TEMPLATE}/c.mk ${TEMPLATE}/Judger.mk + +prebuild: + clang++ -I$(TESTLIB) -Ofast -o $(PREFIX)/judge/gen.out $(PREFIX)/judge/gen.cpp + @if [ ! -f $(PREFIX)/data/input/2.input ]; then \ + $(PREFIX)/judge/gen.out > $(PREFIX)/data/input/2.input; \ + python3 -c "print(sum(map(int, input().split())))" < $(PREFIX)/data/input/2.input > $(PREFIX)/data/output/2.output; \ + fi + @if [ ! -f $(PREFIX)/data/input/4.input ]; then \ + $(PREFIX)/judge/gen.out > $(PREFIX)/data/input/4.input; \ + python3 -c "print(sum(map(int, input().split())))" < $(PREFIX)/data/input/4.input > $(PREFIX)/data/output/4.output; \ + fi diff --git a/resource/runner/scripts/common.sh b/resource/runner/scripts/common.sh new file mode 100755 index 0000000..cd875a6 --- /dev/null +++ b/resource/runner/scripts/common.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +COLOR_RED="\e[0;31m" +COLOR_GREEN="\e[0;32m" +COLOR_YELLOW="\e[0;33m" +COLOR_NONE="\e[0m" + +function log_info() { echo -e "${COLOR_GREEN}$*${COLOR_NONE}" 1>&2; } +function log_warn() { echo -e "${COLOR_YELLOW}$*${COLOR_NONE}" 1>&2; } +function log_error() { echo -e "${COLOR_RED}$*${COLOR_NONE}" 1>&2; } diff --git a/resource/runner/scripts/prepare_container.sh b/resource/runner/scripts/prepare_container.sh new file mode 100755 index 0000000..e02c989 --- /dev/null +++ b/resource/runner/scripts/prepare_container.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +. common.sh + +cd "$(dirname "$0")"/../ || exit 1 + +if [ -f ./.mark.docker ]; then + log_warn "Docker containers already prepared" + log_warn "If you want to re-prepare the containers, please remove the file $(pwd)/.mark.docker" + exit 1 +fi + +# Full +cat <ubuntu-full.Dockerfile +FROM ubuntu:22.04 +WORKDIR /woj/ + +# Install dependencies +RUN apt-get update && apt-get install -y gcc g++ clang make cmake autoconf m4 libtool gperf git parallel python3 && apt-get clean && rm -rf /var/lib/apt/lists + +# Copy source code +RUN mkdir -p /woj/framework && mkdir -p /woj/problem +COPY framework /woj/framework + +# Build +RUN cd /woj/framework/template && ./setup.sh +RUN cd /woj/framework/scripts && ./setup.sh + +# Environment +ENV WOJ_LAUNCHER=/woj/framework/scripts/woj_launcher +ENV WOJ_SANDBOX=/woj/framework/scripts/libwoj_sandbox.so +ENV TEMPLATE=/woj/framework/template +ENV TESTLIB=/woj/framework/template/testlib +ENV PREFIX=/woj/problem +EOF +docker build -t woj/ubuntu-full -f ubuntu-full.Dockerfile . || exit 1 +rm ubuntu-full.Dockerfile + +# Tiny +cat <ubuntu-run.Dockerfile +FROM woj/ubuntu-full:latest AS builder +FROM ubuntu:22.04 +WORKDIR /woj/problem +RUN mkdir -p /woj/framework/scripts +COPY --from=builder /woj/framework/scripts/libwoj_sandbox.so /woj/framework/scripts/ +COPY --from=builder /woj/framework/scripts/woj_launcher /woj/framework/scripts/ +EOF +docker build -t woj/ubuntu-run -f ubuntu-run.Dockerfile . || exit 1 +rm ubuntu-run.Dockerfile + +touch ./.mark.docker diff --git a/resource/runner/scripts/problem.sh b/resource/runner/scripts/problem.sh new file mode 100755 index 0000000..ef407a2 --- /dev/null +++ b/resource/runner/scripts/problem.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash + +. common.sh + +# get_problem_info +# extract language info and limits +# $1: workspace +# $2: problem name +# $3: language +# exports: Info_Script, Info_Cmp, Info_Num, Info_Limit_Time, Info_Limit_Memory, Info_Limit_NProc +function get_problem_info() { + local err + + if [ ! -f "$1/problem/$2/config.json" ]; then + log_error "problem $2 not found" + return 1 + fi + + parse_language_info "$1" "$2" "$3" + err=$? + if [ "$err" -ne 0 ]; then + return "$err" + fi + + parse_limits "$1" "$2" + err=$? + if [ "$err" -ne 0 ]; then + return "$err" + fi +} + +function parse_language_info() { + export Info_Script + export Info_Cmp + + local lang_config + local lang_type + local lang_script + + lang_config=$(jq ".Languages[] | select(.Lang == \"$3\")" "$1/problem/$2/config.json") + if [ -z "$lang_config" ]; then + log_error "language $3 is not supported" + return 1 + fi + + Info_Cmp=$(echo "$lang_config" | jq -r ".Cmp") + + lang_type=$(echo "$lang_config" | jq -r ".Type") + lang_script=$(echo "$lang_config" | jq -r ".Script") + + if [ "$lang_type" == "custom" ]; then + Info_Script="/woj/problem/judge/$lang_script" + elif [ "$lang_type" == "default" ]; then + Info_Script="/woj/framework/template/default/$3.Makefile" + else + log_warn "Config file might be corrupted!" + log_error "Unknown language type: $lang_type" + return 1 + fi +} + +function parse_limits() { + export Info_Limit_Time + export Info_Limit_Memory + export Info_Limit_NProc + export Info_Num + + local cfg + cfg="$1/problem/$2/config.json" + + Info_Limit_Time=$(jq ".Runtime.TimeLimit" "$cfg") + Info_Limit_Memory=$(jq ".Runtime.MemoryLimit" "$cfg") + Info_Limit_NProc=$(jq ".Runtime.NProcLimit" "$cfg") + Info_Num=$(jq ".Tasks | length" "$1/problem/$2/config.json") +} diff --git a/resource/runner/scripts/problem_compile.sh b/resource/runner/scripts/problem_compile.sh new file mode 100755 index 0000000..8f35ac6 --- /dev/null +++ b/resource/runner/scripts/problem_compile.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +WORKSPACE=$(cd "$(dirname "$0")"/.. && pwd) +. "$WORKSPACE"/scripts/run_timeout.sh +. "$WORKSPACE"/scripts/common.sh +. "$WORKSPACE"/scripts/problem.sh + +if [ "$1" == "" ] || [ ! -d "$WORKSPACE/problem/$1" ] || [ "$2" == "" ] || [ ! -d "$WORKSPACE/user/$2" ] || [ -z "$3" ]; then + log_warn "Usage: $0 " + exit 1 +fi + +get_problem_info "$WORKSPACE" "$1" "$3" + +SRC_FILE="$WORKSPACE"/user/"$2"/"$2"."$3" +EXE_FILE="$WORKSPACE"/user/"$2"/"$2".out +export LOG_FILE="$WORKSPACE"/user/"$2"/"$2".compile.log + +rm -f "$EXE_FILE" && touch "$EXE_FILE" + +export TIMEOUT=${4:-60} +docker_run \ + -v "$WORKSPACE"/problem/"$1"/judge:/woj/problem/judge:ro \ + -v "$SRC_FILE":/woj/problem/user/"$2"."$3":ro \ + -v "$EXE_FILE":/woj/problem/user/"$2".out \ + -e USER_PROG="$2" \ + -e LANG="$3" \ + woj/ubuntu-full \ + sh -c \ + "cd /woj/problem/user && make -f $Info_Script compile" diff --git a/resource/runner/scripts/problem_judge.sh b/resource/runner/scripts/problem_judge.sh new file mode 100755 index 0000000..0142ee2 --- /dev/null +++ b/resource/runner/scripts/problem_judge.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +WORKSPACE=$(cd "$(dirname "$0")"/.. && pwd) +. "$WORKSPACE"/scripts/run_timeout.sh +. "$WORKSPACE"/scripts/common.sh +. "$WORKSPACE"/scripts/problem.sh + +if [ "$1" == "" ] || [ ! -d "$WORKSPACE/problem/$1" ] || [ "$2" == "" ] || [ ! -d "$WORKSPACE/user/$2" ] || [ -z "$3" ]; then + log_warn "Usage: $0 " + exit 1 +fi + +get_problem_info "$WORKSPACE" "$1" "$3" + +export TIMEOUT=${4:-60} +for test_num in $(seq "$Info_Num"); do + std_file="$WORKSPACE/problem/$1/data/output/$test_num.output" + ans_file="$WORKSPACE/user/$2/$test_num.out.usr" + jdg_file="$WORKSPACE/user/$2/$test_num.judge" + + if [ ! -f "$std_file" ] || [ ! -f "$ans_file" ]; then + log_error "Missing test case $test_num" + exit 1 + fi + + log_info "Judging test case $test_num" + + touch "$jdg_file" + + docker_run \ + -v "$WORKSPACE"/problem/"$1"/judge:/woj/problem/judge:ro \ + -v "$WORKSPACE"/problem/"$1"/data:/woj/problem/data:ro \ + -v "$ans_file":/woj/problem/user/"$test_num".out.usr \ + -v "$jdg_file":/woj/problem/user/"$test_num".judge \ + -e TEST_NUM="$test_num" \ + -e CMP="$Info_Cmp" \ + woj/ubuntu-full \ + sh -c \ + "cd /woj/problem/user && make -f $Info_Script judge" +done diff --git a/resource/runner/scripts/problem_prebuild.sh b/resource/runner/scripts/problem_prebuild.sh new file mode 100755 index 0000000..3b97164 --- /dev/null +++ b/resource/runner/scripts/problem_prebuild.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +WORKSPACE=$(cd "$(dirname "$0")"/.. && pwd) +. "$WORKSPACE"/scripts/run_timeout.sh +. "$WORKSPACE"/scripts/common.sh + +if [ "$1" == "" ] || [ ! -d "$WORKSPACE/problem/$1" ]; then + log_warn "Usage: $0 " + exit 1 +fi + +if [ -f "$WORKSPACE/problem/$1/.mark.prebuild" ]; then + log_warn "Problem $1 already prebuilt" + log_warn "If you want to re-prebuild the problem, please remove the file $WORKSPACE/problem/$1/.mark.prebuild" + exit 0 +fi + +if [ ! -f "$WORKSPACE/problem/$1/judge/prebuild.Makefile" ]; then + log_warn "Problem $1 does not have prebuild scripts" + log_warn "$WORKSPACE/problem/$1/.mark.prebuild" + exit 0 +fi + +export TIMEOUT=${2:-300} +docker_run \ + -v "$WORKSPACE/problem/$1/data":/woj/problem/data \ + -v "$WORKSPACE/problem/$1/judge":/woj/problem/judge \ + -e PREFIX=/woj/problem \ + woj/ubuntu-full \ + sh -c "cd /woj/problem/judge && make -f prebuild.Makefile prebuild && touch .mark.prebuild" + +mv "$WORKSPACE/problem/$1/judge/.mark.prebuild" "$WORKSPACE/problem/$1/.mark.prebuild" || exit 1 diff --git a/resource/runner/scripts/problem_run.sh b/resource/runner/scripts/problem_run.sh new file mode 100755 index 0000000..3fb4ae3 --- /dev/null +++ b/resource/runner/scripts/problem_run.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash + +WORKSPACE=$(cd "$(dirname "$0")"/.. && pwd) +. "$WORKSPACE"/scripts/run_timeout.sh +. "$WORKSPACE"/scripts/common.sh +. "$WORKSPACE"/scripts/problem.sh + +if [ "$1" == "" ] || [ ! -d "$WORKSPACE/problem/$1" ] || [ "$2" == "" ] || [ ! -d "$WORKSPACE/user/$2" ] || [ -z "$3" ]; then + log_warn "Usage: $0 " + exit 1 +fi + +if [ ! -f "$WORKSPACE/problem/$1/.mark.prebuild" ]; then + log_warn "Problem $1 has not been prebuilt" + log_warn "Please run 'problem_prebuild.sh $1' first" + exit 1 +fi + +if [ ! -f "$WORKSPACE/user/$2/$2.out" ]; then + log_warn "User $2 has not been compiled" + log_warn "Please run 'problem_compile.sh ...' first" + exit 1 +fi + +parse_limits "$WORKSPACE" "$1" + +log_info "Running problem $1 for user $2" +log_info "TimeLimit: $Info_Limit_Time" +log_info "MemoryLimit: $Info_Limit_Memory" +log_info "NProcLimit: $Info_Limit_NProc" + +# launcher will add 2 more seconds +# here add 3 more seconds +TIMEOUT=$(((LIMIT_TIME + 1000) / 1000 + 4)) +log_info "Timeout: $TIMEOUT" + +for test_num in $(seq "$Info_Num"); do + test_case="$WORKSPACE/problem/$1/data/input/$test_num.input" + exe_file="$WORKSPACE/user/$2/$2.out" + ans_file="$WORKSPACE/user/$2/$test_num.out.usr" + ifo_file="$WORKSPACE/user/$2/$test_num.info" + + if [ ! -f "$test_case" ]; then + log_error "Test case $test_num does not exist" + exit 1 + fi + + log_info "Running test case $test_num" + rm -f "$ans_file" && touch "$ans_file" + rm -f "$ifo_file" && touch "$ifo_file" + docker_run \ + --cpus 1 \ + --network none \ + -v "$test_case":/woj/problem/data/input/"$test_num".input:ro \ + -v "$exe_file":/woj/user/"$2".out:ro \ + -v "$ans_file":/woj/user/"$test_num".out.usr \ + -v "$ifo_file":/woj/user/"$test_num".info \ + woj/ubuntu-run \ + sh -c \ + "cd /woj/user && /woj/framework/scripts/woj_launcher \ + --memory_limit=$Info_Limit_Memory \ + --nproc_limit=$Info_Limit_NProc \ + --time_limit=$Info_Limit_Time \ + --sandbox_path=/woj/framework/scripts/libwoj_sandbox.so \ + --sandbox_template=$3 \ + --sandbox_action=nothing \ + --file_input=/woj/problem/data/input/$test_num.input \ + --file_output=/woj/user/$test_num.out.usr \ + --file_info=/woj/user/$test_num.info \ + --program=/woj/user/$2.out" +done diff --git a/resource/runner/scripts/run_timeout.sh b/resource/runner/scripts/run_timeout.sh new file mode 100755 index 0000000..0cafef9 --- /dev/null +++ b/resource/runner/scripts/run_timeout.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +. common.sh + +function docker_run() { + local timeout=${TIMEOUT:-10} + local log_file=${LOG_FILE:-/dev/stderr} + log_info "Docker run with timeout $timeout" + CONTAINER_NAME=$(uuidgen) + ( + sleep "$timeout" + docker kill "$CONTAINER_NAME" + ) & + docker run --rm --name "$CONTAINER_NAME" "$@" > "$log_file" 2>&1 + pkill -P $$ +} diff --git a/resource/runner/tmp/.gitkeep b/resource/runner/tmp/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/resource/runner/user/.gitignore b/resource/runner/user/.gitignore new file mode 100644 index 0000000..2699330 --- /dev/null +++ b/resource/runner/user/.gitignore @@ -0,0 +1 @@ +/test_user \ No newline at end of file