Merge pull request #1 from WHUPRJ/develop

Version 1.0.0
This commit is contained in:
Paul Pan 2022-10-24 16:08:58 +08:00 committed by GitHub
commit 72c89f2e43
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
139 changed files with 4655 additions and 251 deletions

View File

@ -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 }}

View File

@ -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

2
.gitignore vendored
View File

@ -1,5 +1,7 @@
### Project
/tmp
/server
/runner
my.secrets
### JetBrains template

11
Dockerfile.runner Normal file
View File

@ -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"]

View File

@ -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"]

View File

@ -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 ./...

View File

@ -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
}

75
cmd/common.go Normal file
View File

@ -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
}

34
cmd/runner/main.go Normal file
View File

@ -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)
}

34
cmd/server/main.go Normal file
View File

@ -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)
}

View File

@ -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'

40
go.mod
View File

@ -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
)

210
go.sum
View File

@ -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=

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -7,7 +7,7 @@ import (
"go.uber.org/zap"
)
// randomString godoc
// randomString
// @Summary random string
// @Description generate random string with length = 32
// @Tags debug

View File

@ -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)
}

View File

@ -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(),
})
}

View File

@ -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)
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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,
})
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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,
})
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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",
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -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())
}
}

View File

@ -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
}

23
internal/global/jwt.go Normal file
View File

@ -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
}

View File

@ -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())
}
}

7
internal/global/repo.go Normal file
View File

@ -0,0 +1,7 @@
package global
type Repo interface {
Setup(*Global)
Get() interface{}
Close() error
}

11
internal/global/router.go Normal file
View File

@ -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)
}

View File

@ -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())
}
}

23
internal/model/Problem.go Normal file
View File

@ -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"`
}

9
internal/model/Role.go Normal file
View File

@ -0,0 +1,9 @@
package model
type Role int
const (
RoleGuest Role = 10
RoleGeneral Role = 20
RoleAdmin Role = 30
)

16
internal/model/Status.go Normal file
View File

@ -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"`
}

View File

@ -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"`
}

42
internal/model/Task.go Normal file
View File

@ -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
}

14
internal/model/User.go Normal file
View File

@ -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"`
}

View File

@ -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)
}

View File

@ -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()

View File

@ -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)

View File

@ -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,

View File

@ -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
}

View File

@ -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()
}

View File

@ -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},
}

View File

@ -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

View File

@ -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()
}
}
}

View File

@ -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,
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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),
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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,
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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),
}
}

View File

@ -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
}

View File

@ -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,
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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),
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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),
}
}

View File

@ -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
}

View File

@ -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
}

Some files were not shown because too many files have changed in this diff Show More