diff --git a/Runner.Dockerfile b/Runner.Dockerfile index b58e46a..c12d90c 100644 --- a/Runner.Dockerfile +++ b/Runner.Dockerfile @@ -2,17 +2,18 @@ FROM docker.io/library/golang:alpine AS builder ENV GOPROXY=https://goproxy.cn +ENV CGO_ENABLED=0 WORKDIR /builder RUN apk add --no-cache git make -RUN go install github.com/swaggo/swag/cmd/swag@latest +RUN --mount=type=cache,id=golang,target=/go/pkg go install github.com/swaggo/swag/cmd/swag@latest COPY go.mod /builder/go.mod COPY go.sum /builder/go.sum -RUN go mod download +RUN --mount=type=cache,id=golang,target=/go/pkg go mod download COPY . /builder -RUN make build +RUN --mount=type=cache,id=golang,target=/go/pkg make build # main image diff --git a/Server.Dockerfile b/Server.Dockerfile index e82eeae..ceaeb60 100644 --- a/Server.Dockerfile +++ b/Server.Dockerfile @@ -1,19 +1,22 @@ -# builder -FROM docker.io/library/golang:alpine AS builder +# Go builder +FROM docker.io/library/golang:alpine AS go-builder ENV GOPROXY=https://goproxy.cn +ENV CGO_ENABLED=0 WORKDIR /builder RUN apk add --no-cache git make -RUN go install github.com/swaggo/swag/cmd/swag@latest +RUN --mount=type=cache,id=golang,target=/go/pkg go install github.com/swaggo/swag/cmd/swag@latest COPY go.mod /builder/go.mod COPY go.sum /builder/go.sum -RUN go mod download +RUN --mount=type=cache,id=golang,target=/go/pkg go mod download COPY . /builder -RUN make build +RUN --mount=type=cache,id=golang,target=/go/pkg make build +# UI Builder +FROM git.0x7f.app/woj/woj-ui:1.0.0 AS ui-builder # main image FROM docker.io/library/alpine @@ -21,9 +24,11 @@ FROM docker.io/library/alpine WORKDIR /app RUN apk --no-cache add tzdata ca-certificates libc6-compat bash -COPY --from=builder /builder/config.docker.yaml /app -COPY --from=builder /builder/docker-entrypoint.sh /app -COPY --from=builder /builder/resource/frontend /app/resource/frontend -COPY --from=builder /builder/woj /app +COPY --from=go-builder /builder/config.docker.yaml /app +COPY --from=go-builder /builder/docker-entrypoint.sh /app +COPY --from=go-builder /builder/resource/frontend /app/resource/frontend +COPY --from=go-builder /builder/woj /app + +COPY --from=ui-builder /app /app/resource/frontend ENTRYPOINT ["/app/docker-entrypoint.sh"] diff --git a/VERSION b/VERSION index 867e524..cb174d5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.2.0 \ No newline at end of file +1.2.1 \ No newline at end of file diff --git a/build_image.sh b/build_image.sh index 5d104a0..c85bb03 100755 --- a/build_image.sh +++ b/build_image.sh @@ -13,7 +13,7 @@ function build_base() { (log_error "Build Full Image failed" && exit 1) $DOCKER build -t git.0x7f.app/woj/ubuntu-run:latest -f scripts/ubuntu-run.Dockerfile . || (log_error "Build Tiny Image failed" && exit 1) - popd + popd || exit 1 } function push_base() { @@ -53,9 +53,27 @@ function push_runner() { $DOCKER push "git.0x7f.app/woj/woj-runner:$VERSION" } -build_base -push_base -build_server -push_server -build_runner -push_runner +if [ "$1" == "base" ]; then + build_base + push_base + exit 0 +elif [ "$1" == "server" ]; then + build_server + push_server + exit 0 +elif [ "$1" == "runner" ]; then + build_runner + push_runner + exit 0 +elif [ "$1" == "all" ]; then + build_base + push_base + build_server + push_server + build_runner + push_runner + exit 0 +else + log_error "Usage: $0 [base|server|runner|all]" + exit 1 +fi diff --git a/cmd/common.go b/cmd/common.go index 0560068..df6f02b 100644 --- a/cmd/common.go +++ b/cmd/common.go @@ -77,7 +77,7 @@ func setupSentry() { err := sentry.Init(sentry.ClientOptions{ Dsn: SentryDSN, EnableTracing: true, - TracesSampleRate: 1.0, + TracesSampleRate: 0.5, SendDefaultPII: true, Release: GitCommit, }) diff --git a/docker-compose.yml b/docker-compose.yml index 92641ad..619c48b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: server: - image: git.0x7f.app/woj/woj-server:1.1.0 + image: git.0x7f.app/woj/woj-server:1.2.1 restart: unless-stopped healthcheck: test: [ "CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8000/health" ] @@ -33,7 +33,7 @@ services: - "8000:8000" runner: - image: git.0x7f.app/woj/woj-runner:1.1.0 + image: git.0x7f.app/woj/woj-runner:1.2.1 restart: unless-stopped command: runner security_opt: @@ -84,7 +84,7 @@ services: - cache:/data db: - image: docker.io/library/postgres:alpine + image: docker.io/library/postgres:16-alpine restart: unless-stopped healthcheck: test: [ "CMD", "pg_isready", "-U", "dev" ] diff --git a/go.mod b/go.mod index 1c2a5fd..288e5d0 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/gin-contrib/cors v1.5.0 github.com/gin-contrib/pprof v1.4.0 github.com/gin-contrib/zap v0.2.0 + github.com/gin-gonic/contrib v0.0.0-20221130124618-7e01895a63f2 github.com/gin-gonic/gin v1.9.1 github.com/golang-jwt/jwt/v4 v4.5.0 github.com/hibiken/asynq v0.24.1 diff --git a/go.sum b/go.sum index aa3ba5a..7bc814e 100644 --- a/go.sum +++ b/go.sum @@ -52,14 +52,14 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-contrib/zap v0.2.0 h1:HLvt3rZXyC8XC+s2lHzMFow3UDqiEbfrBWJyHHS6L8A= github.com/gin-contrib/zap v0.2.0/go.mod h1:eqfbe9ZmI+GgTZF6nRiC2ZwDeM4DK1Viwc8OxTCphh0= +github.com/gin-gonic/contrib v0.0.0-20221130124618-7e01895a63f2 h1:dyuNlYlG1faymw39NdJddnzJICy6587tiGSVioWhYoE= +github.com/gin-gonic/contrib v0.0.0-20221130124618-7e01895a63f2/go.mod h1:iqneQ2Df3omzIVTkIfn7c1acsVnMGiSLn4XF5Blh3Yg= github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-openapi/jsonpointer v0.20.1 h1:MkK4VEIEZMj4wT9PmjaUmGflVBr9nvud4Q4UVFbDoBE= -github.com/go-openapi/jsonpointer v0.20.1/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q= github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= github.com/go-openapi/jsonreference v0.20.3 h1:EjGcjTW8pD1mRis6+w/gmoBdqv5+RbE9B85D1NgDOVQ= @@ -152,7 +152,6 @@ github.com/jackc/pgx/v5 v5.5.1/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSlj 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.3.0 h1:eHK/5clGOatcjX3oWGBO/MpxpbHzSwud5EWTSCI+MX0= github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= @@ -202,7 +201,6 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= @@ -234,8 +232,6 @@ github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGy github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/redis/go-redis/v9 v9.0.3/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk= -github.com/redis/go-redis/v9 v9.3.0 h1:RiVDjmig62jIWp7Kk4XVLs0hzV6pI3PyTnnL0cnn0u0= -github.com/redis/go-redis/v9 v9.3.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= github.com/redis/go-redis/v9 v9.3.1 h1:KqdY8U+3X6z+iACvumCNxnoluToB+9Me+TvyFa21Mds= github.com/redis/go-redis/v9 v9.3.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= @@ -335,8 +331,6 @@ golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5y 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.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= -golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= diff --git a/internal/api/problem/create_version.go b/internal/api/problem/create_version.go index 6ab028e..457d6aa 100644 --- a/internal/api/problem/create_version.go +++ b/internal/api/problem/create_version.go @@ -8,8 +8,8 @@ import ( ) type createVersionRequest struct { - ProblemID uint `form:"pid" binding:"required"` - StorageKey string `form:"storage_key" binding:"required"` + ProblemID uint `form:"pid" json:"pid" binding:"required"` + StorageKey string `form:"storage_key" json:"storage_key" binding:"required"` } // CreateVersion @@ -44,7 +44,7 @@ func (h *handler) CreateVersion(c *gin.Context) { } // make sure problem exists - _, status := h.problemService.Query(req.ProblemID, false, false) + _, status := h.problemService.Query(&problem.QueryData{ID: req.ProblemID, Associations: false, ShouldEnable: false}) if status != e.Success { e.Pong[any](c, status, nil) return diff --git a/internal/api/problem/details.go b/internal/api/problem/details.go index 337e287..3e659f2 100644 --- a/internal/api/problem/details.go +++ b/internal/api/problem/details.go @@ -3,11 +3,12 @@ package problem import ( "git.0x7f.app/WOJ/woj-server/internal/e" "git.0x7f.app/WOJ/woj-server/internal/model" + "git.0x7f.app/WOJ/woj-server/internal/service/problem" "github.com/gin-gonic/gin" ) type detailsRequest struct { - Pid uint `form:"pid"` + Pid uint `form:"pid" json:"pid"` } type problemDetailsResponse struct { @@ -34,7 +35,7 @@ func (h *handler) Details(c *gin.Context) { claim, exist := c.Get("claim") shouldEnable := !exist || claim.(*model.Claim).Role < model.RoleAdmin - p, status := h.problemService.Query(req.Pid, true, shouldEnable) + p, status := h.problemService.Query(&problem.QueryData{ID: req.Pid, Associations: true, ShouldEnable: shouldEnable}) if status != e.Success { e.Pong[any](c, status, nil) return diff --git a/internal/api/problem/search.go b/internal/api/problem/search.go index 1b64f69..eab1c5a 100644 --- a/internal/api/problem/search.go +++ b/internal/api/problem/search.go @@ -2,12 +2,16 @@ package problem import ( "git.0x7f.app/WOJ/woj-server/internal/e" - _ "git.0x7f.app/WOJ/woj-server/internal/model" // swag requires this + "git.0x7f.app/WOJ/woj-server/internal/model" + "git.0x7f.app/WOJ/woj-server/internal/service/problem" "github.com/gin-gonic/gin" ) type searchRequest struct { - Search string `form:"search"` + Keyword string `form:"keyword" json:"keyword"` + Tag string `form:"tag" json:"tag"` + Offset int `form:"offset" json:"offset"` + Limit int `form:"limit" json:"limit" binding:"required"` } // Search @@ -16,8 +20,11 @@ type searchRequest struct { // @Tags problem // @Accept application/x-www-form-urlencoded // @Produce json -// @Param search formData string false "keyword" -// @Response 200 {object} e.Response[[]model.Problem] "problems found" +// @Param keyword formData string false "keyword" +// @Param tag formData string false "tag" +// @Param offset formData int false "start position" +// @Param limit formData int true "limit number of records" +// @Response 200 {object} e.Response[e.WithCount[model.Problem]] "problems found" // @Router /v1/problem/search [post] func (h *handler) Search(c *gin.Context) { req := new(searchRequest) @@ -26,15 +33,15 @@ func (h *handler) Search(c *gin.Context) { 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 + var count int64 + param := problem.QueryData{ + Keyword: req.Keyword, + Tag: req.Tag, + Offset: req.Offset, + Limit: req.Limit, + Count: &count, } + + problems, status := h.problemService.QueryFuzz(¶m) + e.Pong(c, status, e.WithCount[*model.Problem]{Count: count, Data: problems}) } diff --git a/internal/api/problem/update.go b/internal/api/problem/update.go index 8909108..8c1d5ba 100644 --- a/internal/api/problem/update.go +++ b/internal/api/problem/update.go @@ -6,13 +6,15 @@ import ( "git.0x7f.app/WOJ/woj-server/internal/service/problem" "git.0x7f.app/WOJ/woj-server/pkg/utils" "github.com/gin-gonic/gin" + "github.com/jackc/pgtype" ) type updateRequest struct { - Pid uint `form:"pid"` - Title string `form:"title" binding:"required"` - Statement string `form:"statement" binding:"required"` - IsEnabled bool `form:"is_enabled"` + Pid uint `form:"pid" json:"pid"` + Title string `form:"title" json:"title"` + Statement string `form:"statement" json:"statement"` + Tags []string `form:"tags" json:"tags"` + IsEnabled bool `form:"is_enabled" json:"is_enabled"` } // Update @@ -21,10 +23,11 @@ type updateRequest struct { // @Tags problem,admin // @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" +// @Param pid formData int false "problem id, 0 for create" +// @Param title formData string false "title" +// @Param statement formData string false "statement" +// @Param tags formData []string false "tags" +// @Param is_enabled formData bool false "is enabled" // @Response 200 {object} e.Response[model.Problem] "problem info without provider information" // @Security Authentication // @Router /v1/problem/update [post] @@ -50,22 +53,26 @@ func (h *handler) Update(c *gin.Context) { return } - if req.Pid == 0 { - // create problem + if req.Pid == 0 { // create problem + // Title and Statement are required + if req.Title == "" || req.Statement == "" { + e.Pong[any](c, e.InvalidParameter, nil) + return + } + createData := &problem.CreateData{ Title: req.Title, Statement: req.Statement, + Tags: req.Tags, ProviderID: uid, IsEnabled: false, } p, status := h.problemService.Create(createData) e.Pong(c, status, p) return - } else { - // update problem - + } else { // update problem // check if problem exists - p, status := h.problemService.Query(req.Pid, true, false) + p, status := h.problemService.Query(&problem.QueryData{ID: req.Pid, Associations: true, ShouldEnable: false}) if status != e.Success { e.Pong[any](c, status, nil) return @@ -77,10 +84,24 @@ func (h *handler) Update(c *gin.Context) { return } + // if IsEnabled is set to true, check if the problem has a latest version + if req.IsEnabled { + _, status := h.problemService.QueryLatestVersion(p.ID) + if status != e.Success { + e.Pong[any](c, status, nil) + return + } + p.IsEnabled = true + } + // update problem p.Title = utils.If(req.Title != "", req.Title, p.Title) p.Statement = utils.If(req.Statement != "", req.Statement, p.Statement) - p.IsEnabled = req.IsEnabled + if len(req.Tags) != 0 { + tags := pgtype.TextArray{} + _ = tags.Set(req.Tags) + p.Tags = tags + } p, status = h.problemService.Update(p) e.Pong(c, status, p) diff --git a/internal/api/status/query.go b/internal/api/status/query.go index a686269..16f0f01 100644 --- a/internal/api/status/query.go +++ b/internal/api/status/query.go @@ -8,13 +8,13 @@ import ( ) type queryRequest struct { - Pid uint `form:"pid"` - Uid uint `form:"uid"` - Offset int `form:"offset"` - Limit int `form:"limit" binding:"required"` + Pid uint `form:"pid" json:"pid"` + Uid uint `form:"uid" json:"uid"` + Offset int `form:"offset" json:"offset"` + Limit int `form:"limit" json:"limit" binding:"required"` } -type queryResponse struct { +type submissionWithScore struct { Submission model.Submission `json:"submission"` Point int32 `json:"point"` } @@ -29,7 +29,8 @@ type queryResponse struct { // @Param uid formData uint false "user id" // @Param offset formData int false "start position" // @Param limit formData int true "limit number of records" -// @Response 200 {object} e.Response[[]queryResponse] "queryResponse" +// @Response 200 {object} e.Response[e.WithCount[submissionWithScore]] "status" +// @Security Authentication // @Router /v1/status/query [post] func (h *handler) Query(c *gin.Context) { claim, exist := c.Get("claim") @@ -44,21 +45,21 @@ func (h *handler) Query(c *gin.Context) { return } - if req.Pid == 0 && req.Uid == 0 { - e.Pong[any](c, e.InvalidParameter, nil) - return - } - - submissions, status := h.submissionService.Query(req.Pid, req.Uid, req.Offset, req.Limit) - uid := claim.(*model.Claim).UID role := claim.(*model.Claim).Role - var response []*queryResponse + if req.Pid == 0 && req.Uid == 0 { + req.Uid = uid + } + + var count int64 + submissions, status := h.submissionService.Query(req.Pid, req.Uid, req.Offset, req.Limit, &count) + + var response []*submissionWithScore for _, submission := range submissions { cur, _ := h.statusService.Query(submission.ID, false) point := utils.If(cur == nil, -1, cur.Point) - resp := &queryResponse{ + resp := &submissionWithScore{ Submission: *submission, Point: point, } @@ -71,5 +72,5 @@ func (h *handler) Query(c *gin.Context) { response = append(response, resp) } - e.Pong(c, status, response) + e.Pong(c, status, e.WithCount[*submissionWithScore]{Count: count, Data: response}) } diff --git a/internal/api/status/query_one.go b/internal/api/status/query_one.go index b0e9834..937f07c 100644 --- a/internal/api/status/query_one.go +++ b/internal/api/status/query_one.go @@ -7,7 +7,7 @@ import ( ) type queryOneRequest struct { - SubmissionID uint `form:"sid" binding:"required"` + SubmissionID uint `form:"sid" json:"sid" binding:"required"` } // QueryBySubmissionID @@ -18,6 +18,7 @@ type queryOneRequest struct { // @Produce json // @Param sid formData uint true "submission id" // @Response 200 {object} e.Response[model.Status] "submission status" +// @Security Authentication // @Router /v1/status/query/submission [post] func (h *handler) QueryBySubmissionID(c *gin.Context) { claim, exist := c.Get("claim") diff --git a/internal/api/status/query_version.go b/internal/api/status/query_version.go index 73e1aab..d7b70fc 100644 --- a/internal/api/status/query_version.go +++ b/internal/api/status/query_version.go @@ -7,9 +7,9 @@ import ( ) type queryByVersionRequest struct { - ProblemVersionID uint `form:"pvid" binding:"required"` - Offset int `form:"offset"` - Limit int `form:"limit" binding:"required"` + ProblemVersionID uint `form:"pvid" json:"pvid" binding:"required"` + Offset int `form:"offset" json:"offset"` + Limit int `form:"limit" json:"limit" binding:"required"` } // QueryByProblemVersion @@ -21,7 +21,7 @@ type queryByVersionRequest struct { // @Param pvid formData uint true "problem version" // @Param offset formData int false "start position" // @Param limit formData int true "max number of results" -// @Response 200 {object} e.Response[[]model.Status] "submission status array" +// @Response 200 {object} e.Response[e.WithCount[model.Status]] "submission status array" // @Security Authentication // @Router /v1/status/query/version [post] func (h *handler) QueryByProblemVersion(c *gin.Context) { @@ -44,6 +44,7 @@ func (h *handler) QueryByProblemVersion(c *gin.Context) { return } - submitStatus, status := h.statusService.QueryByVersion(req.ProblemVersionID, req.Offset, req.Limit) - e.Pong(c, status, submitStatus) + var count int64 + submitStatus, status := h.statusService.QueryByVersion(req.ProblemVersionID, req.Offset, req.Limit, &count) + e.Pong(c, status, e.WithCount[*model.Status]{Count: count, Data: submitStatus}) } diff --git a/internal/api/submission/create.go b/internal/api/submission/create.go index 70dcb8a..063deea 100644 --- a/internal/api/submission/create.go +++ b/internal/api/submission/create.go @@ -8,9 +8,9 @@ import ( ) type createRequest struct { - Pid uint `form:"pid" binding:"required"` - Language string `form:"language" binding:"required"` - Code string `form:"code" binding:"required"` + Pid uint `form:"pid" json:"pid" binding:"required"` + Language string `form:"language" json:"language" binding:"required"` + Code string `form:"code" json:"code" binding:"required"` } // Create @@ -47,6 +47,13 @@ func (h *handler) Create(c *gin.Context) { return } + // query latest version + pv, status := h.problemService.QueryLatestVersion(req.Pid) + if status != e.Success { + e.Pong[any](c, status, nil) + return + } + // create submission createData := &submission.CreateData{ ProblemID: req.Pid, @@ -60,13 +67,6 @@ func (h *handler) Create(c *gin.Context) { return } - // query latest version - pv, status := h.problemService.QueryLatestVersion(req.Pid) - if status != e.Success { - e.Pong[any](c, status, nil) - return - } - // submit judge payload := &model.SubmitJudgePayload{ ProblemVersionID: pv.ID, diff --git a/internal/api/submission/rejudge.go b/internal/api/submission/rejudge.go index 08a7df5..f15b66d 100644 --- a/internal/api/submission/rejudge.go +++ b/internal/api/submission/rejudge.go @@ -7,7 +7,7 @@ import ( ) type rejudgeRequest struct { - Sid uint `form:"sid" binding:"required"` + Sid uint `form:"sid" json:"sid" binding:"required"` } // Rejudge diff --git a/internal/api/user/create.go b/internal/api/user/create.go index e8d5148..651e2ca 100644 --- a/internal/api/user/create.go +++ b/internal/api/user/create.go @@ -8,9 +8,9 @@ import ( ) type createRequest struct { - UserName string `form:"username" binding:"required"` - Password string `form:"password" binding:"required"` - NickName string `form:"nickname" binding:"required"` + UserName string `form:"username" json:"username" binding:"required"` + Password string `form:"password" json:"password" binding:"required"` + NickName string `form:"nickname" json:"nickname" binding:"required"` } // Create diff --git a/internal/api/user/login.go b/internal/api/user/login.go index 42c6ed8..7e5998b 100644 --- a/internal/api/user/login.go +++ b/internal/api/user/login.go @@ -8,8 +8,8 @@ import ( ) type loginRequest struct { - UserName string `form:"username" binding:"required"` - Password string `form:"password" binding:"required"` + UserName string `form:"username" json:"username" binding:"required"` + Password string `form:"password" json:"password" binding:"required"` } type loginResponse struct { diff --git a/internal/api/user/profile.go b/internal/api/user/profile.go index 72e798b..cde5f56 100644 --- a/internal/api/user/profile.go +++ b/internal/api/user/profile.go @@ -8,7 +8,7 @@ import ( ) type profileRequest struct { - UID uint `form:"uid"` + UID uint `form:"uid" json:"uid"` } // Profile diff --git a/internal/e/resp.go b/internal/e/resp.go index cd07d7f..edef91d 100644 --- a/internal/e/resp.go +++ b/internal/e/resp.go @@ -12,6 +12,11 @@ type Response[T any] struct { Body T `json:"body"` } +type WithCount[T any] struct { + Count int64 `json:"count"` + Data []T `json:"data"` +} + func wrap[T any](status Status, body T) Response[interface{}] { return Response[interface{}]{ Code: int(status), diff --git a/internal/model/Problem.go b/internal/model/Problem.go index c72b0b9..6f3d0e1 100644 --- a/internal/model/Problem.go +++ b/internal/model/Problem.go @@ -7,11 +7,12 @@ import ( 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"` + Title string `json:"title" gorm:"not null;index"` + Statement string `json:"statement" gorm:"not null"` + Tags pgtype.TextArray `json:"tags" gorm:"type:text[];index"` + 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 { diff --git a/internal/model/Submission.go b/internal/model/Submission.go index 37bb14d..882cbb8 100644 --- a/internal/model/Submission.go +++ b/internal/model/Submission.go @@ -4,9 +4,10 @@ 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"` + ProblemID uint `json:"-" gorm:"not null;index"` + Problem Problem `json:"problem" gorm:"foreignKey:ProblemID"` + UserID uint `json:"-" gorm:"not null;index"` + User User `json:"user" gorm:"foreignKey:UserID"` + Language string `json:"language" gorm:"not null"` + Code string `json:"code" gorm:"not null"` } diff --git a/internal/service/problem/create.go b/internal/service/problem/create.go index 41568e1..27e7c29 100644 --- a/internal/service/problem/create.go +++ b/internal/service/problem/create.go @@ -3,20 +3,25 @@ package problem import ( "git.0x7f.app/WOJ/woj-server/internal/e" "git.0x7f.app/WOJ/woj-server/internal/model" + "github.com/jackc/pgtype" "go.uber.org/zap" ) type CreateData struct { Title string Statement string + Tags []string ProviderID uint IsEnabled bool } func (s *service) Create(data *CreateData) (*model.Problem, e.Status) { + tags := pgtype.TextArray{} + _ = tags.Set(data.Tags) problem := &model.Problem{ Title: data.Title, Statement: data.Statement, + Tags: tags, ProviderID: data.ProviderID, IsEnabled: data.IsEnabled, } diff --git a/internal/service/problem/createVersion.go b/internal/service/problem/create_version.go similarity index 100% rename from internal/service/problem/createVersion.go rename to internal/service/problem/create_version.go diff --git a/internal/service/problem/query.go b/internal/service/problem/query.go index 75a24f5..ea65f6c 100644 --- a/internal/service/problem/query.go +++ b/internal/service/problem/query.go @@ -9,23 +9,24 @@ import ( "gorm.io/gorm/clause" ) -func (s *service) Query(pid uint, associations bool, shouldEnable bool) (*model.Problem, e.Status) { +func (s *service) Query(data *QueryData) (*model.Problem, e.Status) { problem := new(model.Problem) - query := s.db.Get() - if associations { + + if data.Associations { query = query.Preload(clause.Associations) } - err := query.First(&problem, pid).Error + err := query.First(&problem, data.ID).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)) + s.log.Warn("DatabaseError", zap.Error(err), zap.Any("pid", data.ID)) return nil, e.DatabaseError } - if shouldEnable && !problem.IsEnabled { + if data.ShouldEnable && !problem.IsEnabled { return nil, e.ProblemNotAvailable } return problem, e.Success diff --git a/internal/service/problem/queryFuzz.go b/internal/service/problem/queryFuzz.go deleted file mode 100644 index 20f0149..0000000 --- a/internal/service/problem/queryFuzz.go +++ /dev/null @@ -1,30 +0,0 @@ -package problem - -import ( - "git.0x7f.app/WOJ/woj-server/internal/e" - "git.0x7f.app/WOJ/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.Get() - if associations { - query = query.Preload(clause.Associations) - } - if shouldEnable { - query = query.Where("is_enabled = true") - } - query = query. - Where(s.db.Get().Where("title LIKE ?", "%"+search+"%"). - Or("statement LIKE ?", "%"+search+"%")) - err := query.Find(&problems).Error - if err != nil { - s.log.Warn("DatabaseError", zap.Error(err), zap.Any("search", search)) - return nil, e.DatabaseError - } - - return problems, e.Success -} diff --git a/internal/service/problem/query_fuzz.go b/internal/service/problem/query_fuzz.go new file mode 100644 index 0000000..030dc69 --- /dev/null +++ b/internal/service/problem/query_fuzz.go @@ -0,0 +1,60 @@ +package problem + +import ( + "git.0x7f.app/WOJ/woj-server/internal/e" + "git.0x7f.app/WOJ/woj-server/internal/model" + "go.uber.org/zap" + "gorm.io/gorm/clause" +) + +type QueryData struct { + // precise + ID uint + + // fuzz + Keyword string + Tag string + + // common + Associations bool + ShouldEnable bool + + // paging + Offset int + Limit int + Count *int64 +} + +func (s *service) QueryFuzz(data *QueryData) ([]*model.Problem, e.Status) { + problems := make([]*model.Problem, 0) + query := s.db.Get() + + if data.Associations { + query = query.Preload(clause.Associations) + } + + if data.ShouldEnable { + query = query.Where("is_enabled = true") + } + + if data.Keyword != "" { + query = query. + Where(s.db.Get().Where("title LIKE ?", "%"+data.Keyword+"%"). + Or("statement LIKE ?", "%"+data.Keyword+"%")) + } + + if data.Tag != "" { + query = query.Where("EXISTS(SELECT 1 FROM unnest(tags) AS elem WHERE elem LIKE ?)", "%"+data.Tag+"%") + } + + err := query.Order("created_at ASC"). + Offset(data.Offset).Limit(data.Limit).Find(&problems). + Offset(-1).Limit(-1).Count(data.Count). + Error + if err != nil { + s.log.Warn("DatabaseError", zap.Error(err), zap.Any("QueryData", data)) + return nil, e.DatabaseError + } + + return problems, e.Success +} diff --git a/internal/service/problem/queryLatestVersion.go b/internal/service/problem/query_latest_version.go similarity index 100% rename from internal/service/problem/queryLatestVersion.go rename to internal/service/problem/query_latest_version.go diff --git a/internal/service/problem/queryVersion.go b/internal/service/problem/query_version.go similarity index 100% rename from internal/service/problem/queryVersion.go rename to internal/service/problem/query_version.go diff --git a/internal/service/problem/service.go b/internal/service/problem/service.go index f4be009..5996b34 100644 --- a/internal/service/problem/service.go +++ b/internal/service/problem/service.go @@ -14,8 +14,8 @@ 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) + Query(data *QueryData) (*model.Problem, e.Status) + QueryFuzz(data *QueryData) ([]*model.Problem, e.Status) CreateVersion(data *CreateVersionData) (*model.ProblemVersion, e.Status) UpdateVersion(pvid uint, values interface{}) e.Status diff --git a/internal/service/problem/updateVersion.go b/internal/service/problem/update_version.go similarity index 100% rename from internal/service/problem/updateVersion.go rename to internal/service/problem/update_version.go diff --git a/internal/service/status/query.go b/internal/service/status/query.go index 96d9e32..8baed80 100644 --- a/internal/service/status/query.go +++ b/internal/service/status/query.go @@ -18,7 +18,10 @@ func (s *service) Query(sid uint, associations bool) (*model.Status, e.Status) { query := s.db.Get() if associations { - query = query.Preload(clause.Associations) + query = query. + Preload("Submission.Problem"). + Preload("Submission.User"). + Preload(clause.Associations) } err := query. @@ -36,21 +39,23 @@ func (s *service) Query(sid uint, associations bool) (*model.Status, e.Status) { return status, e.Success } -func (s *service) QueryByVersion(pvid uint, offset int, limit int) ([]*model.Status, e.Status) { - var statuses []*model.Status +func (s *service) QueryByVersion(pvid uint, offset int, limit int, count *int64) ([]*model.Status, e.Status) { + var ret []*model.Status status := &model.Status{ ProblemVersionID: pvid, IsEnabled: true, } - err := s.db.Get().Preload(clause.Associations). + err := s.db.Get(). + Preload("Submission.Problem").Preload("Submission.User").Preload(clause.Associations). Where(status). - Limit(limit). - Offset(offset). - Find(&statuses).Error + Order("created_at DESC"). + Offset(offset).Limit(limit).Find(&ret). + Offset(-1).Limit(-1).Count(count). + Error if err != nil { s.log.Warn("DatabaseError", zap.Error(err), zap.Any("status", status)) return nil, e.DatabaseError } - return statuses, e.Success + return ret, e.Success } diff --git a/internal/service/status/service.go b/internal/service/status/service.go index b255d98..9683389 100644 --- a/internal/service/status/service.go +++ b/internal/service/status/service.go @@ -14,7 +14,7 @@ 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) + QueryByVersion(pvid uint, offset int, limit int, count *int64) ([]*model.Status, e.Status) HealthCheck() error } diff --git a/internal/service/submission/query.go b/internal/service/submission/query.go index 391b67c..2d07220 100644 --- a/internal/service/submission/query.go +++ b/internal/service/submission/query.go @@ -9,7 +9,7 @@ import ( "gorm.io/gorm/clause" ) -func (s *service) Query(pid uint, uid uint, offset int, limit int) ([]*model.Submission, e.Status) { +func (s *service) Query(pid uint, uid uint, offset int, limit int, count *int64) ([]*model.Submission, e.Status) { submissions := make([]*model.Submission, 0) submission := &model.Submission{ @@ -19,9 +19,10 @@ func (s *service) Query(pid uint, uid uint, offset int, limit int) ([]*model.Sub err := s.db.Get().Preload(clause.Associations). Where(submission). - Limit(limit). - Offset(offset). - Find(&submissions).Error + Order("created_at DESC"). + Offset(offset).Limit(limit).Find(&submissions). + Offset(-1).Limit(-1).Count(count). + Error //if errors.Is(err, gorm.ErrRecordNotFound) { // return nil, e.ProblemNotFound diff --git a/internal/service/submission/service.go b/internal/service/submission/service.go index 3ea8043..1eaa2dc 100644 --- a/internal/service/submission/service.go +++ b/internal/service/submission/service.go @@ -13,7 +13,7 @@ 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) + Query(pid uint, uid uint, offset int, limit int, count *int64) ([]*model.Submission, e.Status) QueryBySid(sid uint, associations bool) (*model.Submission, e.Status) HealthCheck() error diff --git a/internal/web/router/router.go b/internal/web/router/router.go index 691ad9e..146f92c 100644 --- a/internal/web/router/router.go +++ b/internal/web/router/router.go @@ -11,6 +11,7 @@ import ( "github.com/gin-contrib/cors" "github.com/gin-contrib/pprof" ginZap "github.com/gin-contrib/zap" + "github.com/gin-gonic/contrib/static" "github.com/gin-gonic/gin" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/samber/do" @@ -57,7 +58,14 @@ func (s *service) initRouters(conf *model.Config, injector *do.Injector) *gin.En gin.SetMode(utils.If[string](conf.Development, gin.DebugMode, gin.ReleaseMode)) r := gin.New() - r.MaxMultipartMemory = 8 << 20 + r.MaxMultipartMemory = 8 << 20 // 8MB + + // +-----------+ + // |Middlewares| + // +-----------+ + + // static files - must before sentry + r.Use(static.Serve("/", static.LocalFile("./resource/frontend", true))) // Sentry middleware r.Use(sentrygin.New(sentrygin.Options{Repanic: true})) @@ -87,6 +95,10 @@ func (s *service) initRouters(conf *model.Config, injector *do.Injector) *gin.En s.metric.SetLogPaths([]string{"/api"}) r.Use(s.metric.Handler()) + // +------+ + // |Routes| + // +------+ + // metrics r.GET("/metrics", gin.WrapH(promhttp.Handler())) @@ -117,9 +129,7 @@ func (s *service) initRouters(conf *model.Config, injector *do.Injector) *gin.En api := r.Group("/api/") s.setupApi(api, injector) - // static files - r.Static("/static", "./resource/frontend/static") - r.StaticFile("/", "./resource/frontend/index.html") + // fallback to frontend r.NoRoute(func(c *gin.Context) { c.File("./resource/frontend/index.html") }) diff --git a/resource/deploy/runner.yaml b/resource/deploy/runner.yaml index 76feac8..e6efb98 100644 --- a/resource/deploy/runner.yaml +++ b/resource/deploy/runner.yaml @@ -35,7 +35,7 @@ spec: spec: containers: - name: runner - image: git.0x7f.app/woj/woj-runner:1.1.0 + image: git.0x7f.app/woj/woj-runner:1.2.1 imagePullPolicy: IfNotPresent args: - runner diff --git a/resource/deploy/server.yaml b/resource/deploy/server.yaml index 100f254..2ecee52 100644 --- a/resource/deploy/server.yaml +++ b/resource/deploy/server.yaml @@ -18,33 +18,9 @@ spec: labels: app: server spec: - initContainers: - - name: init-server - image: git.0x7f.app/woj/woj-server:1.1.0 - imagePullPolicy: IfNotPresent - args: - - init - env: - - name: DATABASE_HOST - value: "db-service.woj.svc.cluster.local" - - name: DATABASE_USER - valueFrom: - configMapKeyRef: - name: shared-config - key: POSTGRES_USER - - name: DATABASE_PASSWORD - valueFrom: - configMapKeyRef: - name: shared-config - key: POSTGRES_PASSWORD - - name: DATABASE_NAME - valueFrom: - configMapKeyRef: - name: shared-config - key: POSTGRES_DB containers: - name: server - image: git.0x7f.app/woj/woj-server:1.1.0 + image: git.0x7f.app/woj/woj-server:1.2.1 imagePullPolicy: IfNotPresent args: - server diff --git a/resource/frontend/.gitignore b/resource/frontend/.gitignore index e69de29..f59ec20 100644 --- a/resource/frontend/.gitignore +++ b/resource/frontend/.gitignore @@ -0,0 +1 @@ +* \ No newline at end of file diff --git a/resource/frontend/index.html b/resource/frontend/index.html deleted file mode 100644 index a4937e8..0000000 --- a/resource/frontend/index.html +++ /dev/null @@ -1,8 +0,0 @@ - - - Test - - -

Building

- - \ No newline at end of file diff --git a/resource/runner/problem/example/README.md b/resource/runner/problem/example/README.md index 39ffa40..d68454a 100644 --- a/resource/runner/problem/example/README.md +++ b/resource/runner/problem/example/README.md @@ -5,7 +5,7 @@ ``` . ├── config.json # 题目配置信息 -├── description.md # (not used) 题目描述必须通过 API 提交给系统 +├── description.md # (optional) 题目描述,必须通过 API 提交,使用 import 脚本时将读取该文件 ├── data # 数据目录 │ ├── input # 输入数据 │ │ ├── (x).input # 第 x 组输入数据 @@ -25,20 +25,21 @@ ```json5 { - "Runtime": { - // 运行时配置 + "Runtime": { + // 运行时配置 "TimeLimit": 1000, // 时间限制 (ms) "MemoryLimit": 16, // 内存限制 (MB) "NProcLimit": 1 // 进(线)程 限制 }, - "Languages": [ + "Languages": [ + // 支持的语言 // c 语言,使用自定义评测脚本,脚本为 ./judge/XYZ.Makefile {"Lang": "c", "Type": "custom", "Script": "XYZ.Makefile", "Cmp": ""}, // c++ 语言,使用默认评测脚本,答案比对方式为 NCMP(testlib) {"Lang": "cpp", "Type": "default", "Script": "", "Cmp": "NCMP"} - ], // 支持的语言 - "Tasks": [ - // 评测点信息 + ], + "Tasks": [ + // 评测点信息 {"Id": 1, "Points": 10}, // 第一个评测点,分值 25 分,使用 ./data/{input,output}/1.{input,output} 为测试数据 {"Id": 2, "Points": 20}, {"Id": 3, "Points": 30}, @@ -51,7 +52,7 @@ 1. 默认评测脚本目前只支持 `c` 语言和 `cpp`,参见 `../../framework/template/default/{c,cpp}.Makefile` 2. 自定义评测脚本参见 `./judge/XYZ.Makefile` -3. `prebuild.Makefile`: 题目初始化脚本,用于编译辅助程序、生成数据等。如果存在改文件,系统会在题目分发到判机后自动执行,否则跳过改步骤 +3. `prebuild.Makefile`: 题目初始化脚本,用于编译辅助程序、生成数据等。如果存在该文件,系统会在题目分发到判机后自动执行,否则跳过该步骤 ## 注意事项 diff --git a/resource/runner/problem/example/judge/XYZ.Makefile b/resource/runner/problem/example/judge/XYZ.Makefile index 1264083..6fcde11 100644 --- a/resource/runner/problem/example/judge/XYZ.Makefile +++ b/resource/runner/problem/example/judge/XYZ.Makefile @@ -2,16 +2,37 @@ include ${TEMPLATE}/c.mk ${TEMPLATE}/Judger.mk # 评测分四个阶段 -# 1. prebuild: 用于提前生成测试数据、评测器、spj等工具,runner 只执行一次 -# 只有 ./data, ./judge 目录可见 -# 2. compile: 用于编译用户提交的程序 -# 只有 ./user/$(USER_PROG).$(LANG) 和 ./judge 目录可见 -# 3. run: 运行用户程序 -# 只有 ./data/input/*.input 和 ./user/$(USER_PROG).out 可见 -# 用户输出存放于 ./user/?.out.usr -# 使用 woj-sandbox 运行,等效于 $(PREFIX)/user/$(USER_PROG).out < $(PREFIX)/data/input/$(TEST_NUM).input > $(PREFIX)/user/$(TEST_NUM).out.usr -# 4. judge: 用于判定输出结果 环境变量 TEST_NUM 表示当前测试点编号 -# 所有目录 ./data ./judge ./user 可见 +# 1. prebuild: 用于提前生成测试数据、评测器、spj等工具,runner 只执行一次 +# 详细信息见 XYZ.Makefile +# 2. compile: 用于编译用户提交的程序 +# 目录映射情况: +# /woj/problem +# ├── judge 映射到题目目录的 ./judge <-- Readonly +# └── user 映射到题目目录的 ./user <-- 用户提交的程序在这里,命名:$(USER_PROG).$(LANG) +# 环境变量: +# USER_PROG=... <-- 一段随机字符串 +# LANG=... <-- 用户提交的程序的语言,如 c, cpp +# 其余通用环境变量,详见 ubuntu-full.Dockerfile +# 执行限制: +# 目前版本硬编码限制:时间 60s,内存 256mb +# 3. run: 运行用户程序 +# 只有 ./data/input/*.input 和 ./user/$(USER_PROG).out 可见 +# 用户输出存放于 ./user/?.out.usr +# 使用 woj-sandbox 运行,等效于 $(PREFIX)/user/$(USER_PROG).out < $(PREFIX)/data/input/$(TEST_NUM).input > $(PREFIX)/user/$(TEST_NUM).out.usr +# 4. judge: 用于判定输出结果 环境变量 TEST_NUM 表示当前测试点编号,每个测试点都会执行一次 +# 目录映射情况: +# /woj/problem +# ├── data 映射到题目目录的 ./data <-- Readonly +# ├── judge 映射到题目目录的 ./judge <-- Readonly +# └── user +# ├── $(TEST_NUM).out.usr <-- 用户程序在 $(TEST_NUM) 上的输出 +# └── $(TEST_NUM).judge <-- 评测结果写在这里,格式要求为 testlib 的 XML 格式 +# 环境变量: +# TEST_NUM=... <-- 当前测试点编号 +# CMP=... <-- 在 config.json 中配置的比较器,如 NCMP +# 其余通用环境变量,详见 ubuntu-full.Dockerfile +# 执行限制: +# 目前版本硬编码限制:时间 60s,内存 256mb compile: $(CC) $(CFLAGS) -o $(PREFIX)/user/$(USER_PROG).out $(PREFIX)/user/$(USER_PROG).$(LANG) $(PREFIX)/judge/gadget.c diff --git a/resource/runner/problem/example/judge/prebuild.Makefile b/resource/runner/problem/example/judge/prebuild.Makefile index c07df39..b10e235 100644 --- a/resource/runner/problem/example/judge/prebuild.Makefile +++ b/resource/runner/problem/example/judge/prebuild.Makefile @@ -2,6 +2,17 @@ include ${TEMPLATE}/c.mk ${TEMPLATE}/Judger.mk # 当题目被下载到 runner 后,会自动执行 prebuild 阶段,判题时不会再次执行 +# prebuild 阶段环境信息 +# 目录映射情况: +# /woj/problem +# ├── data 映射到题目目录的 ./data +# └── judge 映射到题目目录的 ./judge +# 环境变量: +# PREFIX=/woj/problem +# 其余通用环境变量,详见 ubuntu-full.Dockerfile +# 执行限制: +# 目前版本硬编码限制:时间 300s,内存 1g + prebuild: # 生成测试数据生成工具 clang++ -I$(TESTLIB) -Ofast -o $(PREFIX)/judge/gen.out $(PREFIX)/judge/gen.cpp diff --git a/resource/runner/scripts/run_timeout.sh b/resource/runner/scripts/docker_run.sh similarity index 59% rename from resource/runner/scripts/run_timeout.sh rename to resource/runner/scripts/docker_run.sh index 20b64f2..560e573 100755 --- a/resource/runner/scripts/run_timeout.sh +++ b/resource/runner/scripts/docker_run.sh @@ -1,9 +1,12 @@ #!/usr/bin/env bash -. common.sh +SCRIPT_PATH=$(cd "$(dirname "$0")" && pwd) +. "$SCRIPT_PATH/common.sh" function docker_run() { local timeout=${TIMEOUT:-10} + local network=${NETWORK:-"none"} + local memory=${MEMORY:-"256m"} local log_file=${LOG_FILE:-"/dev/stderr"} local log_limit=${LOG_LIMIT:-4K} log_info "$DOCKER run with timeout $timeout" @@ -12,7 +15,7 @@ function docker_run() { sleep "$timeout" $DOCKER kill "$CONTAINER_NAME" ) & - $DOCKER run --rm --name "$CONTAINER_NAME" "$@" 2>&1 | head -c "$log_limit" >"$log_file" + $DOCKER run --rm --name "$CONTAINER_NAME" --network "$network" --memory "$memory" "$@" 2>&1 | head -c "$log_limit" >"$log_file" pkill -P $$ $DOCKER kill "$CONTAINER_NAME" >/dev/null 2>&1 return 0 diff --git a/resource/runner/scripts/import.sh b/resource/runner/scripts/import.sh new file mode 100755 index 0000000..bf256e4 --- /dev/null +++ b/resource/runner/scripts/import.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash + +WORKSPACE=$(cd "$(dirname "$0")"/.. && pwd) +. "$WORKSPACE"/scripts/common.sh + +read -p "Enter HTTP API Endpoint: " -r endpoint +if [ -z "$endpoint" ]; then + log_error "[-] HTTP API Endpoint cannot be empty" + exit 1 +fi + +read -p "Enter token: " -r token +if [ -z "$token" ]; then + log_error "[-] Token cannot be empty" + exit 1 +fi + +for problem in "$WORKSPACE/problem/"*; do + if [ -d "$problem" ]; then + dir_name=$(basename "$problem") + log_info "[+] Importing problem $dir_name" + + if [ ! -f "$problem/config.json" ]; then + log_warn "[-] Skipping: $dir_name/config.json not found" + continue + fi + + if [ ! -f "$problem/description.md" ]; then + log_warn "[-] Skipping: $dir_name/description.md not found" + continue + fi + + read -p "Are you sure you want to import $dir_name? [y/N] " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + log_warn "[-] Skipping: user cancelled" + continue + fi + + title=$(head -n 1 "$problem/description.md" | sed -e 's/^# //' | xargs) + description=$(cat "$problem/description.md") + # TODO: extract tags + log_info "[*] Title: $title" + + zip_file=$(mktemp -u --suffix .zip) + log_info "[*] Compressing $problem into $zip_file" >/dev/null + cd "$problem" && zip -9rq "$zip_file" . -x ".mark.prebuild" && cd .. + + payload=$(jq -nc \ + --arg title "$title" \ + --arg description "$description" \ + '{ pid: 0, title: $title, statement: $description, is_enable: false }') + + log_info "[*] Creating problem statement" + response=$(curl -s \ + -H "Content-Type: application/json" \ + -H "Authorization: $token" \ + -X POST \ + -d "$payload" \ + "$endpoint/api/v1/problem/update") + + code=$(echo "$response" | jq -r '.code') + if [ "$code" != "0" ]; then + log_error "[-] Failed to create problem statement" + log_error "[-] Response: $response" + continue + fi + + id=$(echo "$response" | jq -r '.body.meta.ID') + log_info "[*] Problem statement created with id: $id" + + log_info "[*] Uploading problem package" + response=$(curl -s \ + -H "Authorization: $token" \ + -X POST \ + "$endpoint/api/v1/problem/upload") + + code=$(echo "$response" | jq -r '.code') + if [ "$code" != "0" ]; then + log_error "[-] Failed to get upload url" + log_error "[-] Response: $response" + continue + fi + + upload_url=$(echo "$response" | jq -r '.body.url') + storage_key=$(echo "$response" | jq -r '.body.key') + curl -s -X PUT -T "$zip_file" "$upload_url" + # shellcheck disable=SC2181 + if [ $? -ne 0 ]; then + log_error "[-] Failed to upload problem package" + echo curl -s -X PUT -T "$zip_file" "$upload_url" + continue + fi + + payload=$(jq -nc \ + --argjson pid "$id" \ + --arg storage_key "$storage_key" \ + '{ pid: $pid, storage_key: $storage_key }') + + log_info "[*] Creating problem version" + response=$(curl -s \ + -H "Content-Type: application/json" \ + -H "Authorization: $token" \ + -X POST \ + -d "$payload" \ + "$endpoint/api/v1/problem/create_version") + + code=$(echo "$response" | jq -r '.code') + if [ "$code" != "0" ]; then + log_error "[-] Failed to create problem version" + log_error "[-] Payload: $payload" + log_error "[-] Response: $response" + continue + fi + + log_info "[*] Problem version created" + log_info "[+] Problem $dir_name imported successfully" + fi + +done diff --git a/resource/runner/scripts/prepare_images.sh b/resource/runner/scripts/prepare_images.sh index f03c696..2db7ae4 100755 --- a/resource/runner/scripts/prepare_images.sh +++ b/resource/runner/scripts/prepare_images.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash -. common.sh +SCRIPT_PATH=$(cd "$(dirname "$0")" && pwd) +. "$SCRIPT_PATH/common.sh" cd "$(dirname "$0")"/../ || exit 1 diff --git a/resource/runner/scripts/problem.sh b/resource/runner/scripts/problem.sh index a9f2754..835499c 100755 --- a/resource/runner/scripts/problem.sh +++ b/resource/runner/scripts/problem.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash -. common.sh +SCRIPT_PATH=$(cd "$(dirname "$0")" && pwd) +. "$SCRIPT_PATH/common.sh" # get_problem_info # extract language info and limits diff --git a/resource/runner/scripts/problem_compile.sh b/resource/runner/scripts/problem_compile.sh index a25ccfc..ab06937 100755 --- a/resource/runner/scripts/problem_compile.sh +++ b/resource/runner/scripts/problem_compile.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash WORKSPACE=$(cd "$(dirname "$0")"/.. && pwd) -. "$WORKSPACE"/scripts/run_timeout.sh . "$WORKSPACE"/scripts/common.sh +. "$WORKSPACE"/scripts/docker_run.sh . "$WORKSPACE"/scripts/problem.sh if [ "$1" == "" ] || [ ! -d "$WORKSPACE/problem/$1" ] || [ "$2" == "" ] || [ ! -d "$WORKSPACE/user/$2" ] || [ -z "$3" ]; then diff --git a/resource/runner/scripts/problem_judge.sh b/resource/runner/scripts/problem_judge.sh index b262d6f..c94f4bb 100755 --- a/resource/runner/scripts/problem_judge.sh +++ b/resource/runner/scripts/problem_judge.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash WORKSPACE=$(cd "$(dirname "$0")"/.. && pwd) -. "$WORKSPACE"/scripts/run_timeout.sh . "$WORKSPACE"/scripts/common.sh +. "$WORKSPACE"/scripts/docker_run.sh . "$WORKSPACE"/scripts/problem.sh if [ "$1" == "" ] || [ ! -d "$WORKSPACE/problem/$1" ] || [ "$2" == "" ] || [ ! -d "$WORKSPACE/user/$2" ] || [ -z "$3" ]; then diff --git a/resource/runner/scripts/problem_prebuild.sh b/resource/runner/scripts/problem_prebuild.sh index e054688..b79672e 100755 --- a/resource/runner/scripts/problem_prebuild.sh +++ b/resource/runner/scripts/problem_prebuild.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash WORKSPACE=$(cd "$(dirname "$0")"/.. && pwd) -. "$WORKSPACE"/scripts/run_timeout.sh . "$WORKSPACE"/scripts/common.sh +. "$WORKSPACE"/scripts/docker_run.sh if [ "$1" == "" ] || [ ! -d "$WORKSPACE/problem/$1" ]; then log_warn "Usage: $0 " @@ -22,6 +22,7 @@ if [ ! -f "$WORKSPACE/problem/$1/judge/prebuild.Makefile" ]; then fi export TIMEOUT=${2:-300} +export MEMORY="1g" docker_run \ -v "$WORKSPACE/problem/$1/data":/woj/problem/data \ -v "$WORKSPACE/problem/$1/judge":/woj/problem/judge \ diff --git a/resource/runner/scripts/problem_run.sh b/resource/runner/scripts/problem_run.sh index 03da64d..b23ba71 100755 --- a/resource/runner/scripts/problem_run.sh +++ b/resource/runner/scripts/problem_run.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash WORKSPACE=$(cd "$(dirname "$0")"/.. && pwd) -. "$WORKSPACE"/scripts/run_timeout.sh . "$WORKSPACE"/scripts/common.sh +. "$WORKSPACE"/scripts/docker_run.sh . "$WORKSPACE"/scripts/problem.sh if [ "$1" == "" ] || [ ! -d "$WORKSPACE/problem/$1" ] || [ "$2" == "" ] || [ ! -d "$WORKSPACE/user/$2" ] || [ -z "$3" ]; then