Merge branch 'develop'

version 1.2.0
This commit is contained in:
Paul Pan 2023-12-22 15:51:31 +08:00
commit 8a22bb3cb5
Signed by: Paul
GPG Key ID: D639BDF5BA578AF4
148 changed files with 3016 additions and 2247 deletions

8
.dockerignore Normal file
View File

@ -0,0 +1,8 @@
# top
/woj
# runner
resource/runner/.mark.image
resource/runner/problem/*
resource/runner/tmp/*
resource/runner/user/*

View File

@ -1,32 +0,0 @@
name: Docker Image
on:
push:
branches: [ "master", "develop" ]
tags: [ "v*" ]
pull_request:
branches: [ "master", "develop" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Server Meta
id: server_meta
uses: docker/metadata-action@v4
with:
images: panpaul/woj-server
- name: Build and Push the Server Image
uses: docker/build-push-action@v3
with:
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.server_meta.outputs.tags }}
file: ./Dockerfile.server
labels: ${{ steps.server_meta.outputs.labels }}

View File

@ -1,29 +0,0 @@
name: Lint
on:
push:
branches: [ "master", "develop" ]
tags: [ "v*" ]
pull_request:
branches: [ "master", "develop" ]
permissions:
contents: read
pull-requests: read
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v3
with:
go-version: 1.19
- uses: actions/checkout@v3
- name: Generate Swagger Docs
run: make swagger
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: latest
only-new-issues: true

99
.gitignore vendored
View File

@ -1,96 +1,3 @@
### Project
/tmp
/server
/runner
my.secrets
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### Go template
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
/woj
/config.yaml
/dsn.txt

25
.idea/jsonSchemas.xml Normal file
View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JsonSchemaMappingsProjectConfiguration">
<state>
<map>
<entry key="docker-compose.yml">
<value>
<SchemaInfo>
<option name="name" value="docker-compose.yml" />
<option name="relativePathToSchema" value="https://raw.githubusercontent.com/compose-spec/compose-spec/master/schema/compose-spec.json" />
<option name="applicationDefined" value="true" />
<option name="patterns">
<list>
<Item>
<option name="path" value="docker-compose.yml" />
</Item>
</list>
</option>
</SchemaInfo>
</value>
</entry>
</map>
</state>
</component>
</project>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SwaggerSettings">
<option name="defaultPreviewType" value="SWAGGER_UI" />
</component>
</project>

View File

@ -1,11 +0,0 @@
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 +0,0 @@
FROM golang:latest AS builder
WORKDIR /builder
COPY . /builder
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/frontend /app/resource/frontend
EXPOSE 8000
ENTRYPOINT ["/app/server"]

View File

@ -1,36 +1,37 @@
GO := go
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)
PKG_BASE := $(shell head -n 1 go.mod | awk '{print $$2}')
BUILD_TIME := $(shell date -u '+%Y%m%d-%I%M%S')
VERSION := $(shell cat VERSION)
GIT_COMMIT := $(shell git rev-parse HEAD)
LDFLAGS += -X $(PKG_BASE)/cmd.BuildTime=$(BUILD_TIME)
LDFLAGS += -X $(PKG_BASE)/cmd.Version=$(VERSION)
LDFLAGS += -X $(PKG_BASE)/cmd.GitCommit=$(GIT_COMMIT)
LDFLAGS += -X $(PKG_BASE)/cmd.SentryDSN=$(shell cat dsn.txt)
LDFLAGS += -s -w
GOBUILD := $(GO) build -ldflags '$(LDFLAGS)'
GOBIN := $(shell go env GOPATH)/bin
.PHONY: all server runner build clean dep swagger fmt
.PHONY: all build clean dep swagger fmt
default: all
all: clean build
server: swagger dep
$(GOBUILD) -o server ./cmd/server
runner: dep
$(GOBUILD) -o runner ./cmd/runner
build: runner server
build: swagger dep
$(GOBUILD) -o woj ./cmd/woj
clean:
rm -f runner
rm -f server
rm -f woj
dep:
go mod tidy && go mod download
go mod download
swagger:
go install github.com/swaggo/swag/cmd/swag@latest
$(GOBIN)/swag init -g internal/router/api.go -o internal/router/docs
$(GOBIN)/swag init -g internal/web/router/api.go -d .,./internal/e,./internal/model --pdl 1 -o internal/web/router/docs
fmt:
go fmt ./...

35
Runner.Dockerfile Normal file
View File

@ -0,0 +1,35 @@
# builder
FROM docker.io/library/golang:alpine AS builder
ENV GOPROXY=https://goproxy.cn
WORKDIR /builder
RUN apk add --no-cache git make
RUN 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
COPY . /builder
RUN make build
# main image
FROM quay.io/podman/stable
# pkill
RUN yum -y install jq procps-ng && yum -y clean all && rm -rf /var/cache
WORKDIR /app
# prepare images
COPY --from=builder /builder/resource/runner /app/resource/runner
RUN bash -c "cd /app/resource/runner/scripts && ./prepare_images.sh save"
# sources
COPY --from=builder /builder/config.docker.yaml /app
COPY --from=builder /builder/docker-entrypoint.sh /app
COPY --from=builder /builder/woj /app
ENTRYPOINT ["/app/docker-entrypoint.sh"]

29
Server.Dockerfile Normal file
View File

@ -0,0 +1,29 @@
# builder
FROM docker.io/library/golang:alpine AS builder
ENV GOPROXY=https://goproxy.cn
WORKDIR /builder
RUN apk add --no-cache git make
RUN 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
COPY . /builder
RUN make build
# main image
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
ENTRYPOINT ["/app/docker-entrypoint.sh"]

View File

@ -1 +1 @@
1.0.0
1.2.0

61
build_image.sh Executable file
View File

@ -0,0 +1,61 @@
#!/usr/bin/env bash
. resource/runner/scripts/common.sh
# version
VERSION="$(cat VERSION)"
log_info "VERSION: $VERSION"
function build_base() {
log_info "[+] Building Base Images"
pushd resource/runner || exit 1
$DOCKER build -t git.0x7f.app/woj/ubuntu-full:latest -f scripts/ubuntu-full.Dockerfile . ||
(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
}
function push_base() {
log_info "[+] Pushing Base Images"
$DOCKER push "git.0x7f.app/woj/ubuntu-full:latest"
$DOCKER push "git.0x7f.app/woj/ubuntu-run:latest"
}
function build_server() {
log_info "[+] Building Server"
$DOCKER build -t "git.0x7f.app/woj/woj-server:latest" -f Server.Dockerfile . ||
(log_error "[!] Failed to build Server" && exit 1)
}
function build_runner() {
log_info "[+] Building Runner"
$DOCKER build \
--cap-add=sys_admin,mknod \
--device=/dev/fuse \
--security-opt label=disable \
-t "git.0x7f.app/woj/woj-runner:latest" \
-f Runner.Dockerfile . ||
(log_error "[!] Failed to build Runner" && exit 1)
}
function push_server() {
log_info "[+] Pushing Server Images"
$DOCKER push "git.0x7f.app/woj/woj-server:latest"
$DOCKER tag "git.0x7f.app/woj/woj-server:latest" "git.0x7f.app/woj/woj-server:$VERSION"
$DOCKER push "git.0x7f.app/woj/woj-server:$VERSION"
}
function push_runner() {
log_info "[+] Pushing Runner Images"
$DOCKER push "git.0x7f.app/woj/woj-runner:latest"
$DOCKER tag "git.0x7f.app/woj/woj-runner:latest" "git.0x7f.app/woj/woj-runner:$VERSION"
$DOCKER push "git.0x7f.app/woj/woj-runner:$VERSION"
}
build_base
push_base
build_server
push_server
build_runner
push_runner

View File

@ -1,10 +1,9 @@
package cmd
import (
"github.com/WHUPRJ/woj-server/internal/global"
"github.com/getsentry/sentry-go"
"github.com/urfave/cli/v2"
"log"
"math/rand"
"time"
)
@ -34,27 +33,39 @@ var App = &cli.App{
EnvVars: []string{"APP_CONFIG"},
},
},
After: cleanupSentry,
}
var (
BuildTime string
Version string
GitCommit string
SentryDSN string
)
func init() {
if BuildTime == "" {
BuildTime = "2022-09-06-01-00-00"
// First Commit
BuildTime = "20220907-153437"
}
App.Compiled = getBuildTime()
if Version == "" {
Version = "0.0.0+None"
Version = "0.0.0"
}
App.Version = Version
if GitCommit == "" {
GitCommit = "out-of-tree"
}
if SentryDSN != "" {
setupSentry()
}
}
func getBuildTime() time.Time {
build, err := time.Parse("2006-01-02-15-04-05", BuildTime)
build, err := time.Parse("20060102-150405", BuildTime)
if err != nil {
log.Printf("failed to parse build time: %v", err)
build = time.Now()
@ -62,14 +73,22 @@ func getBuildTime() time.Time {
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
func setupSentry() {
err := sentry.Init(sentry.ClientOptions{
Dsn: SentryDSN,
EnableTracing: true,
TracesSampleRate: 1.0,
SendDefaultPII: true,
Release: GitCommit,
})
if err != nil {
log.Fatalf("sentry.Init: %s", err)
}
}
func cleanupSentry(*cli.Context) error {
if SentryDSN != "" {
sentry.Flush(time.Second * 2)
}
return nil
}

View File

@ -1,34 +0,0 @@
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)
}

View File

@ -1,34 +0,0 @@
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)
}

115
cmd/woj/woj.go Normal file
View File

@ -0,0 +1,115 @@
package main
import (
"git.0x7f.app/WOJ/woj-server/cmd"
appRunner "git.0x7f.app/WOJ/woj-server/internal/app/runner"
appServer "git.0x7f.app/WOJ/woj-server/internal/app/server"
"git.0x7f.app/WOJ/woj-server/internal/misc/config"
"git.0x7f.app/WOJ/woj-server/internal/misc/log"
"git.0x7f.app/WOJ/woj-server/internal/repo/cache"
"git.0x7f.app/WOJ/woj-server/internal/repo/db"
"git.0x7f.app/WOJ/woj-server/internal/service/problem"
"git.0x7f.app/WOJ/woj-server/internal/service/runner"
"git.0x7f.app/WOJ/woj-server/internal/service/status"
"git.0x7f.app/WOJ/woj-server/internal/service/storage"
"git.0x7f.app/WOJ/woj-server/internal/service/submission"
"git.0x7f.app/WOJ/woj-server/internal/service/task"
"git.0x7f.app/WOJ/woj-server/internal/service/user"
"git.0x7f.app/WOJ/woj-server/internal/web/jwt"
"git.0x7f.app/WOJ/woj-server/internal/web/metrics"
"git.0x7f.app/WOJ/woj-server/internal/web/router"
"github.com/getsentry/sentry-go"
"github.com/samber/do"
"github.com/urfave/cli/v2"
slog "log"
"os"
"time"
)
func main() {
a := cmd.App
a.Usage = "woj-server"
a.Commands = []*cli.Command{
{
Name: "server",
Aliases: []string{"s"},
Usage: "start web api server",
Action: wrap(appServer.RunServer),
},
{
Name: "migrate",
Aliases: []string{"m"},
Usage: "migrate database",
Action: wrap(appServer.RunServerMigrate),
},
{
Name: "runner",
Aliases: []string{"r"},
Usage: "start runner",
Action: wrap(appRunner.RunRunner),
},
}
err := a.Run(os.Args)
if err != nil {
slog.Fatal(err)
}
}
func prepareServices(c *cli.Context) *do.Injector {
injector := do.New()
// cli context
do.ProvideValue(injector, c)
{ // basic services
do.Provide(injector, config.NewService)
do.Provide(injector, log.NewService)
}
{ // repo services
do.Provide(injector, db.NewService)
do.Provide(injector, cache.NewService)
}
{ // web helper services
do.Provide(injector, metrics.NewService)
do.Provide(injector, jwt.NewService)
do.Provide(injector, router.NewService)
}
{ // core services
do.Provide(injector, problem.NewService)
do.Provide(injector, runner.NewService)
do.Provide(injector, status.NewService)
do.Provide(injector, storage.NewService)
do.Provide(injector, submission.NewService)
do.Provide(injector, task.NewService)
do.Provide(injector, user.NewService)
}
return injector
}
func wrap(f func(i *do.Injector) error) func(*cli.Context) error {
return func(c *cli.Context) error {
defer func() {
if cmd.SentryDSN != "" {
// only recover when sentry is enabled
if r := recover(); r != nil {
sentry.CaptureException(r.(error))
sentry.Flush(time.Second * 2)
slog.Printf("Panic Captured: %v", r)
}
}
}()
injector := prepareServices(c)
logger := do.MustInvoke[log.Service](injector)
defer func() { _ = logger.GetRawLogger().Sync() }()
logger.GetRawLogger().Info("starting...")
return f(injector)
}
}

37
config.docker.yaml Normal file
View File

@ -0,0 +1,37 @@
WebServer:
Address: ${WEB_SERVER_ADDRESS}
Port: ${WEB_SERVER_PORT}
JwtSigningKey: ${WEB_SERVER_JWT_SIGNING_KEY}
JwtExpireHour: ${WEB_SERVER_JWT_EXPIRE_HOUR}
Redis:
Db: ${REDIS_DB}
QueueDb: ${REDIS_QUEUE_DB}
Address: ${REDIS_ADDRESS}
Port: ${REDIS_PORT}
Password: ${REDIS_PASSWORD}
Database:
Host: ${DATABASE_HOST}
Port: ${DATABASE_PORT}
User: ${DATABASE_USER}
Password: ${DATABASE_PASSWORD}
Database: ${DATABASE_NAME}
Prefix: ${DATABASE_PREFIX}
MaxOpenConns: ${DATABASE_MAX_OPEN_CONNS}
MaxIdleConns: ${DATABASE_MAX_IDLE_CONNS}
ConnMaxLifetime: ${DATABASE_CONN_MAX_LIFETIME}
TimeZone: ${DATABASE_TIMEZONE}
Storage:
Endpoint: ${STORAGE_ENDPOINT}
UseSSL: ${STORAGE_USE_SSL}
AccessKey: ${STORAGE_ACCESS_KEY}
SecretKey: ${STORAGE_SECRET_KEY}
Bucket: ${STORAGE_BUCKET}
Metrics:
Namespace: ${METRICS_NAMESPACE}
Subsystem: ${METRICS_SUBSYSTEM}
Development: ${DEVELOPMENT}

View File

@ -1,35 +0,0 @@
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'
Development: true

103
docker-compose.yml Normal file
View File

@ -0,0 +1,103 @@
services:
server:
image: git.0x7f.app/woj/woj-server:1.1.0
restart: unless-stopped
healthcheck:
test: [ "CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8000/health" ]
interval: 5s
command: server
environment:
- REDIS_ADDRESS=cache
- DATABASE_HOST=db
- DATABASE_USER=dev
- DATABASE_PASSWORD=password
- DATABASE_NAME=dev
- STORAGE_ENDPOINT=storage:9000
- STORAGE_ACCESS_KEY=access_key
- STORAGE_SECRET_KEY=secret_key
- STORAGE_BUCKET=woj
- DEVELOPMENT=true
volumes:
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
depends_on:
runner:
condition: service_started
storage:
condition: service_healthy
cache:
condition: service_healthy
db:
condition: service_healthy
ports:
- "8000:8000"
runner:
image: git.0x7f.app/woj/woj-runner:1.1.0
restart: unless-stopped
command: runner
security_opt:
- "label=disable"
cap_add:
- SYS_ADMIN
- MKNOD
devices:
- "/dev/fuse"
environment:
- REDIS_ADDRESS=cache
- STORAGE_ENDPOINT=storage:9000
- STORAGE_ACCESS_KEY=access_key
- STORAGE_SECRET_KEY=secret_key
- STORAGE_BUCKET=woj
- DEVELOPMENT=true
volumes:
- runner:/app/resource/runner/user
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
depends_on:
storage:
condition: service_healthy
cache:
condition: service_healthy
storage:
image: quay.io/minio/minio:latest
restart: unless-stopped
healthcheck:
test: [ "CMD", "curl", "-f", "http://127.0.0.1:9000/minio/health/live" ]
interval: 5s
entrypoint: sh
command: -c 'mkdir -p /data/woj && minio server /data'
environment:
MINIO_ROOT_USER: "access_key"
MINIO_ROOT_PASSWORD: "secret_key"
volumes:
- storage:/data
cache:
image: docker.io/library/redis:alpine
restart: unless-stopped
healthcheck:
test: [ "CMD", "redis-cli", "ping" ]
interval: 5s
volumes:
- cache:/data
db:
image: docker.io/library/postgres:alpine
restart: unless-stopped
healthcheck:
test: [ "CMD", "pg_isready", "-U", "dev" ]
interval: 5s
environment:
- POSTGRES_USER=dev
- POSTGRES_PASSWORD=password
- POSTGRES_DB=dev
volumes:
- db:/var/lib/postgresql/data
volumes:
runner:
storage:
cache:
db:

86
docker-entrypoint.sh Executable file
View File

@ -0,0 +1,86 @@
#!/bin/bash
set -eo pipefail
COLOR_RED="\e[0;31m"
COLOR_GREEN="\e[0;32m"
COLOR_YELLOW="\e[0;33m"
COLOR_NONE="\e[0m"
function log_info() { echo -e "${COLOR_GREEN}$*${COLOR_NONE}" 1>&2; }
function log_warn() { echo -e "${COLOR_YELLOW}$*${COLOR_NONE}" 1>&2; }
function log_error() { echo -e "${COLOR_RED}$*${COLOR_NONE}" 1>&2; }
log_info "extracting env vars"
function check_env() {
# $1 -> var name
# $2 -> default value
# $3 -> quota or not
val=$(eval "echo -n \${$1}")
if test -z "$val"; then
log_warn "Environment variable $1 is not set, using default value \"$2\""
export "$1"="$2"
else
log_info "Using $1=$val"
fi
if "$3"; then
val=$(eval "echo -n \${$1}")
# shellcheck disable=SC2140
export "$1"="'$val'"
fi
# echo -n ">>>>> "
# eval "echo -n \${$1}"
# echo " <<<<<"
}
check_env "WEB_SERVER_ADDRESS" "0.0.0.0" true
check_env "WEB_SERVER_PORT" 8000 false
check_env "WEB_SERVER_JWT_SIGNING_KEY" "$(head -n 10 /dev/urandom | md5sum | cut -c 1-32)" true
check_env "WEB_SERVER_JWT_EXPIRE_HOUR" 12 false
check_env "REDIS_DB" 0 false
check_env "REDIS_QUEUE_DB" 1 false
check_env "REDIS_ADDRESS" "redis" true
check_env "REDIS_PORT" 6379 false
check_env "REDIS_PASSWORD" "" true
check_env "DATABASE_HOST" "postgres" true
check_env "DATABASE_PORT" 5432 false
check_env "DATABASE_USER" "dev" true
check_env "DATABASE_PASSWORD" "password" true
check_env "DATABASE_NAME" "dev" true
check_env "DATABASE_PREFIX" "oj_" true
check_env "DATABASE_MAX_OPEN_CONNS" 100 false
check_env "DATABASE_MAX_IDLE_CONNS" 60 false
check_env "DATABASE_CONN_MAX_LIFETIME" 60 false
check_env "DATABASE_TIMEZONE" "Asia/Shanghai" true
check_env "STORAGE_ENDPOINT" "minio:9000" true
check_env "STORAGE_USE_SSL" "false" false
check_env "STORAGE_ACCESS_KEY" "access_key" true
check_env "STORAGE_SECRET_KEY" "secret_key" true
check_env "STORAGE_BUCKET" "woj" true
check_env "METRICS_NAMESPACE" "woj" true
check_env "METRICS_SUBSYSTEM" "server" true
check_env "DEVELOPMENT" false false
rm -f /tmp/tmp.yaml
(
echo "cat <<EOF >/app/config.yaml"
cat /app/config.docker.yaml
echo "EOF"
) >/tmp/tmp.yaml
if [ -f '/app/config.yaml' ]; then
log_info "config.yaml already exists, skip"
else
log_info "creating config.yaml"
. /tmp/tmp.yaml || (log_error "failed to create config.yaml" && exit 1)
fi
log_info "starting woj"
#cat /app/config.yaml
exec /app/woj "$@"

140
go.mod
View File

@ -1,90 +1,96 @@
module github.com/WHUPRJ/woj-server
module git.0x7f.app/WOJ/woj-server
go 1.19
go 1.20
require (
github.com/gin-contrib/cors v1.4.0
github.com/TheZeroSlave/zapsentry v1.20.0
github.com/getsentry/sentry-go v0.25.0
github.com/gin-contrib/cors v1.5.0
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
github.com/gin-contrib/zap v0.2.0
github.com/gin-gonic/gin v1.9.1
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/hibiken/asynq v0.24.1
github.com/jackc/pgtype v1.14.0
github.com/minio/minio-go/v7 v7.0.66
github.com/prometheus/client_golang v1.17.0
github.com/redis/go-redis/v9 v9.3.1
github.com/samber/do v1.6.0
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.0
github.com/swaggo/swag v1.16.2
github.com/urfave/cli/v2 v2.26.0
go.uber.org/zap v1.26.0
golang.org/x/crypto v0.17.0
golang.org/x/text v0.14.0
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
gorm.io/driver/postgres v1.5.4
gorm.io/gorm v1.25.5
moul.io/zapgorm2 v1.3.0
)
require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
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/bytedance/sonic v1.10.2 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // 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
github.com/go-openapi/spec v0.20.4 // indirect
github.com/go-openapi/swag v0.19.15 // indirect
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
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/go-openapi/jsonpointer v0.20.2 // indirect
github.com/go-openapi/jsonreference v0.20.3 // indirect
github.com/go-openapi/spec v0.20.12 // indirect
github.com/go-openapi/swag v0.22.5 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.16.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/uuid v1.5.0 // 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/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
github.com/jackc/pgx/v4 v4.18.1 // indirect
github.com/jackc/pgx/v5 v5.5.1 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.4 // indirect
github.com/jinzhu/now v1.1.5 // 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/klauspost/compress v1.17.4 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/minio/sha256-simd v1.0.0 // indirect
github.com/minio/sha256-simd v1.0.1 // 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/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.45.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/rs/xid v1.4.0 // indirect
github.com/rs/xid v1.5.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.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
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/arch v0.6.0 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/sync v0.5.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.16.1 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
)

790
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -2,11 +2,12 @@ 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"
"git.0x7f.app/WOJ/woj-server/internal/misc/log"
"git.0x7f.app/WOJ/woj-server/internal/service/problem"
"git.0x7f.app/WOJ/woj-server/internal/service/status"
"git.0x7f.app/WOJ/woj-server/internal/service/task"
"github.com/hibiken/asynq"
"github.com/samber/do"
"go.uber.org/zap"
)
@ -24,12 +25,12 @@ type handler struct {
taskService task.Service
}
func NewConsumer(g *global.Global) Handler {
func NewConsumer(i *do.Injector) Handler {
hnd := &handler{
log: g.Log,
problemService: problem.NewService(g),
statusService: status.NewService(g),
taskService: task.NewService(g),
log: do.MustInvoke[log.Service](i).GetLogger("api.consumer"),
problemService: do.MustInvoke[problem.Service](i),
statusService: do.MustInvoke[status.Service](i),
taskService: do.MustInvoke[task.Service](i),
}
return hnd

View File

@ -4,8 +4,8 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/WHUPRJ/woj-server/internal/e"
"github.com/WHUPRJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/model"
"github.com/hibiken/asynq"
"github.com/jackc/pgtype"
"go.uber.org/zap"

View File

@ -4,9 +4,9 @@ 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"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/internal/service/status"
"github.com/hibiken/asynq"
"go.uber.org/zap"
)

View File

@ -1,22 +1,25 @@
package debug
import (
"github.com/WHUPRJ/woj-server/internal/global"
"git.0x7f.app/WOJ/woj-server/internal/misc/log"
"github.com/gin-gonic/gin"
"github.com/samber/do"
"go.uber.org/zap"
)
var _ Handler = (*handler)(nil)
type Handler interface {
randomString(c *gin.Context)
RandomString(c *gin.Context)
}
type handler struct {
log *zap.Logger
}
func RouteRegister(g *global.Global, group *gin.RouterGroup) {
app := &handler{g.Log}
group.GET("/random", app.randomString)
func RouteRegister(rg *gin.RouterGroup, i *do.Injector) {
app := &handler{}
app.log = do.MustInvoke[log.Service](i).GetLogger("api.debug")
rg.GET("/random", app.RandomString)
}

View File

@ -1,20 +1,20 @@
package debug
import (
"github.com/WHUPRJ/woj-server/internal/e"
"github.com/WHUPRJ/woj-server/pkg/utils"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/pkg/utils"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// randomString
// @Summary random string
// @Description generate random string with length = 32
// RandomString
// @Summary generate random string
// @Description Generate random string with length = 32.
// @Tags debug
// @Produce json
// @Response 200 {object} e.Response "random string"
// @Response 200 {object} e.Response[string] "random string"
// @Router /debug/random [get]
func (h *handler) randomString(c *gin.Context) {
func (h *handler) RandomString(c *gin.Context) {
str := utils.RandomString(32)
h.log.Info("random string", zap.String("str", str))
e.Pong(c, e.Success, str)

View File

@ -1,10 +1,9 @@
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"
"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"
)
@ -14,54 +13,61 @@ type createVersionRequest struct {
}
// CreateVersion
// @Summary create a problem version
// @Description create a problem version
// @Summary [admin] create a problem version
// @Description Create a problem version associated with `pid`.
// @Tags problem,admin
// @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 ""
// @Param storage_key formData string true "storage key, zip file containing problem data"
// @Response 200 {object} e.Response[any] "nothing"
// @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)
e.Pong[any](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
// only admin can create problem version
role := claim.(*model.Claim).Role
if role < model.RoleAdmin {
e.Pong(c, e.UserUnauthorized, nil)
e.Pong[any](c, e.UserUnauthorized, nil)
return
}
// TODO: check pid exist
// make sure problem exists
_, status := h.problemService.Query(req.ProblemID, false, false)
if status != e.Success {
e.Pong[any](c, status, nil)
return
}
// create problem version
createVersionData := &problem.CreateVersionData{
ProblemID: req.ProblemID,
StorageKey: req.StorageKey,
}
pv, status := h.problemService.CreateVersion(createVersionData)
if status != e.Success {
e.Pong(c, status, nil)
e.Pong[any](c, status, nil)
return
}
// enqueue task: runner build problem
payload := &model.ProblemBuildPayload{
ProblemVersionID: pv.ID,
StorageKey: pv.StorageKey,
}
_, status = h.taskService.ProblemBuild(payload)
e.Pong(c, status, nil)
e.Pong[any](c, status, nil)
// TODO: if failed, delete problem version
}

View File

@ -1,9 +1,8 @@
package problem
import (
"github.com/WHUPRJ/woj-server/internal/e"
"github.com/WHUPRJ/woj-server/internal/global"
"github.com/WHUPRJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/model"
"github.com/gin-gonic/gin"
)
@ -11,37 +10,44 @@ type detailsRequest struct {
Pid uint `form:"pid"`
}
type problemDetailsResponse struct {
Problem *model.Problem `json:"problem"`
Context interface{} `json:"context"`
}
// Details
// @Summary get details of a problem
// @Description get details of a problem
// @Description Get details of a problem.
// @Tags problem
// @Accept application/x-www-form-urlencoded
// @Produce json
// @Param pid formData int true "problem id"
// @Response 200 {object} e.Response "problem details"
// @Response 200 {object} e.Response[problemDetailsResponse] "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
shouldEnable := !exist || claim.(*model.Claim).Role < model.RoleAdmin
p, status := h.problemService.Query(req.Pid, true, shouldEnable)
if status != e.Success {
e.Pong(c, status, nil)
e.Pong[any](c, status, nil)
return
}
pv, status := h.problemService.QueryLatestVersion(req.Pid)
if status != e.Success {
e.Pong(c, status, nil)
e.Pong[any](c, status, nil)
return
}
e.Pong(c, e.Success, gin.H{
"problem": p,
"context": pv.Context.Get(),
e.Pong(c, e.Success, problemDetailsResponse{
Problem: p,
Context: pv.Context.Get(),
})
}

View File

@ -1,11 +1,13 @@
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"
"git.0x7f.app/WOJ/woj-server/internal/misc/log"
"git.0x7f.app/WOJ/woj-server/internal/service/problem"
"git.0x7f.app/WOJ/woj-server/internal/service/storage"
"git.0x7f.app/WOJ/woj-server/internal/service/task"
"git.0x7f.app/WOJ/woj-server/internal/web/jwt"
"github.com/gin-gonic/gin"
"github.com/samber/do"
"go.uber.org/zap"
)
@ -16,28 +18,29 @@ type Handler interface {
Search(c *gin.Context)
Update(c *gin.Context)
Upload(c *gin.Context)
CreateVersion(c *gin.Context)
}
type handler struct {
log *zap.Logger
jwtService global.JwtService
jwtService jwt.Service
problemService problem.Service
taskService task.Service
storageService storage.Service
}
func RouteRegister(g *global.Global, group *gin.RouterGroup) {
func RouteRegister(rg *gin.RouterGroup, i *do.Injector) {
app := &handler{
log: g.Log,
jwtService: g.Jwt,
problemService: problem.NewService(g),
taskService: task.NewService(g),
storageService: storage.NewService(g),
log: do.MustInvoke[log.Service](i).GetLogger("api.problem"),
jwtService: do.MustInvoke[jwt.Service](i),
problemService: do.MustInvoke[problem.Service](i),
taskService: do.MustInvoke[task.Service](i),
storageService: do.MustInvoke[storage.Service](i),
}
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)
rg.POST("/search", app.Search)
rg.POST("/details", app.jwtService.Handler(false), app.Details)
rg.POST("/update", app.jwtService.Handler(true), app.Update)
rg.POST("/upload", app.jwtService.Handler(true), app.Upload)
rg.POST("/create_version", app.jwtService.Handler(true), app.CreateVersion)
}

View File

@ -1,7 +1,8 @@
package problem
import (
"github.com/WHUPRJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/e"
_ "git.0x7f.app/WOJ/woj-server/internal/model" // swag requires this
"github.com/gin-gonic/gin"
)
@ -10,16 +11,16 @@ type searchRequest struct {
}
// Search
// @Summary get detail of a problem
// @Description get detail of a problem
// @Summary search for problems
// @Description Search for problems based on keywords. If the keyword is empty, return all problems.
// @Tags problem
// @Accept application/x-www-form-urlencoded
// @Produce json
// @Param search formData string false "word search"
// @Response 200 {object} e.Response "problemset"
// @Param search formData string false "keyword"
// @Response 200 {object} e.Response[[]model.Problem] "problems found"
// @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

View File

@ -1,10 +1,10 @@
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"
"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"
"git.0x7f.app/WOJ/woj-server/pkg/utils"
"github.com/gin-gonic/gin"
)
@ -16,28 +16,31 @@ type updateRequest struct {
}
// Update
// @Summary create or update a problem
// @Description create or update a problem
// @Summary [admin] create or update a problem
// @Description Create or update a problem.
// @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"
// @Response 200 {object} e.Response "problem info without provider information"
// @Response 200 {object} e.Response[model.Problem] "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)
e.Pong[any](c, e.UserUnauthenticated, nil)
return
}
uid := claim.(*global.Claim).UID
role := claim.(*global.Claim).Role
uid := claim.(*model.Claim).UID
role := claim.(*model.Claim).Role
// only admin can modify problem
if role < model.RoleAdmin {
e.Pong(c, e.UserUnauthorized, nil)
e.Pong[any](c, e.UserUnauthorized, nil)
return
}
@ -48,6 +51,7 @@ func (h *handler) Update(c *gin.Context) {
}
if req.Pid == 0 {
// create problem
createData := &problem.CreateData{
Title: req.Title,
Statement: req.Statement,
@ -58,18 +62,24 @@ func (h *handler) Update(c *gin.Context) {
e.Pong(c, status, p)
return
} else {
// update problem
// check if problem exists
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)
e.Pong[any](c, status, nil)
return
}
p.Title = req.Title
p.Statement = req.Statement
// check if user is the provider of the problem
if p.ProviderID != uid {
e.Pong[any](c, e.UserUnauthorized, nil)
return
}
// 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
p, status = h.problemService.Update(p)

View File

@ -1,44 +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/WHUPRJ/woj-server/pkg/utils"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/pkg/utils"
"github.com/gin-gonic/gin"
"time"
)
type uploadResponse struct {
Key string `json:"key"`
URL string `json:"url"`
}
// Upload
// @Summary get upload url
// @Description get upload url
// @Summary [admin] get upload url
// @Description Retrieve a pre-signed upload URL from the object storage
// @Tags problem,admin
// @Produce json
// @Response 200 {object} e.Response "upload url and key"
// @Response 200 {object} e.Response[uploadResponse] "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)
e.Pong[any](c, e.UserUnauthenticated, nil)
return
}
role := claim.(*global.Claim).Role
// only admin can upload
role := claim.(*model.Claim).Role
if role < model.RoleAdmin {
e.Pong(c, e.UserUnauthorized, nil)
e.Pong[any](c, e.UserUnauthorized, nil)
return
}
// generate random key
key := utils.RandomString(16)
url, status := h.storageService.Upload(key, time.Second*60*60)
if status != e.Success {
e.Pong(c, status, nil)
e.Pong[any](c, status, nil)
return
}
e.Pong(c, e.Success, gin.H{
"key": key,
"url": url,
})
e.Pong(c, e.Success, uploadResponse{Key: key, URL: url})
}

View File

@ -4,8 +4,8 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/WHUPRJ/woj-server/internal/e"
"github.com/WHUPRJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/model"
"github.com/hibiken/asynq"
"go.uber.org/zap"
"time"
@ -31,6 +31,7 @@ func (h *handler) Build(_ context.Context, t *asynq.Task) error {
}
for i := range config.Languages {
// do not store in db
config.Languages[i].Type = ""
config.Languages[i].Script = ""
config.Languages[i].Cmp = ""

View File

@ -3,12 +3,13 @@ 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"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/misc/log"
"git.0x7f.app/WOJ/woj-server/internal/service/runner"
"git.0x7f.app/WOJ/woj-server/internal/service/storage"
"git.0x7f.app/WOJ/woj-server/internal/service/task"
"github.com/hibiken/asynq"
"github.com/samber/do"
"go.uber.org/zap"
)
@ -26,17 +27,17 @@ type handler struct {
storageService storage.Service
}
func NewRunner(g *global.Global) (Handler, error) {
func NewRunner(i *do.Injector) (Handler, error) {
hnd := &handler{
log: g.Log,
runnerService: runner.NewService(g),
taskService: task.NewService(g),
storageService: storage.NewService(g),
log: do.MustInvoke[log.Service](i).GetLogger("api.runner"),
runnerService: do.MustInvoke[runner.Service](i),
taskService: do.MustInvoke[task.Service](i),
storageService: do.MustInvoke[storage.Service](i),
}
status := hnd.runnerService.EnsureDeps(false)
if status != e.Success {
g.Log.Error("failed to ensure runner dependencies", zap.String("status", status.String()))
hnd.log.Error("failed to ensure runner dependencies", zap.String("status", status.String()))
return nil, errors.New("failed to ensure dependencies")
}

View File

@ -4,10 +4,10 @@ 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"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/internal/service/runner"
"git.0x7f.app/WOJ/woj-server/pkg/utils"
"github.com/hibiken/asynq"
"go.uber.org/zap"
"path/filepath"
@ -63,7 +63,7 @@ func (h *handler) Judge(_ context.Context, t *asynq.Task) error {
// 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
return utils.If(status != e.Success, e.InternalError, e.Success), point, result
}()
h.taskService.SubmitUpdate(&model.SubmitUpdatePayload{

View File

@ -1,9 +1,12 @@
package status
import (
"github.com/WHUPRJ/woj-server/internal/global"
"github.com/WHUPRJ/woj-server/internal/service/status"
"git.0x7f.app/WOJ/woj-server/internal/misc/log"
"git.0x7f.app/WOJ/woj-server/internal/service/status"
"git.0x7f.app/WOJ/woj-server/internal/service/submission"
"git.0x7f.app/WOJ/woj-server/internal/web/jwt"
"github.com/gin-gonic/gin"
"github.com/samber/do"
"go.uber.org/zap"
)
@ -11,22 +14,26 @@ var _ Handler = (*handler)(nil)
type Handler interface {
Query(c *gin.Context)
QueryBySubmissionID(c *gin.Context)
QueryByProblemVersion(c *gin.Context)
}
type handler struct {
log *zap.Logger
statusService status.Service
jwtService global.JwtService
submissionService submission.Service
jwtService jwt.Service
}
func RouteRegister(g *global.Global, group *gin.RouterGroup) {
func RouteRegister(rg *gin.RouterGroup, i *do.Injector) {
app := &handler{
log: g.Log,
statusService: status.NewService(g),
jwtService: g.Jwt,
log: do.MustInvoke[log.Service](i).GetLogger("api.status"),
submissionService: do.MustInvoke[submission.Service](i),
statusService: do.MustInvoke[status.Service](i),
jwtService: do.MustInvoke[jwt.Service](i),
}
group.POST("/query", app.Query)
group.POST("/query/problem_version", app.jwtService.Handler(true), app.QueryByProblemVersion)
rg.POST("/query", app.jwtService.Handler(true), app.Query)
rg.POST("/query/submission", app.jwtService.Handler(true), app.QueryBySubmissionID)
rg.POST("/query/version", app.jwtService.Handler(true), app.QueryByProblemVersion)
}

View File

@ -1,31 +1,75 @@
package status
import (
"github.com/WHUPRJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/pkg/utils"
"github.com/gin-gonic/gin"
)
type queryRequest struct {
SubmissionID uint `form:"sid" binding:"required"`
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 by via submission id
// @Description query submissions by via submission id
// @Summary query status via problem id or user id
// @Description Batch query judgement status based on either the question or user.
// @Tags status
// @Accept application/x-www-form-urlencoded
// @Produce json
// @Param sid formData uint true "submission id"
// @Response 200 {object} e.Response "model.status"
// @Param pid formData uint false "problem id"
// @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"
// @Router /v1/status/query [post]
func (h *handler) Query(c *gin.Context) {
req := new(queryRequest)
claim, exist := c.Get("claim")
if !exist {
e.Pong[any](c, e.UserUnauthenticated, nil)
return
}
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)
if req.Pid == 0 && req.Uid == 0 {
e.Pong[any](c, e.InvalidParameter, nil)
return
}
e.Pong(c, eStatus, status)
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
for _, submission := range submissions {
cur, _ := h.statusService.Query(submission.ID, false)
point := utils.If(cur == nil, -1, cur.Point)
resp := &queryResponse{
Submission: *submission,
Point: point,
}
if role < model.RoleAdmin || uid != submission.UserID {
// strip out code
resp.Submission.Code = ""
}
response = append(response, resp)
}
e.Pong(c, status, response)
}

View File

@ -1,51 +0,0 @@
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,50 @@
package status
import (
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/model"
"github.com/gin-gonic/gin"
)
type queryOneRequest struct {
SubmissionID uint `form:"sid" binding:"required"`
}
// QueryBySubmissionID
// @Summary query status via submission id
// @Description Query the detailed results of the judgement based on the submission ID.
// @Tags status
// @Accept application/x-www-form-urlencoded
// @Produce json
// @Param sid formData uint true "submission id"
// @Response 200 {object} e.Response[model.Status] "submission status"
// @Router /v1/status/query/submission [post]
func (h *handler) QueryBySubmissionID(c *gin.Context) {
claim, exist := c.Get("claim")
if !exist {
e.Pong[any](c, e.UserUnauthenticated, nil)
return
}
req := new(queryOneRequest)
if err := c.ShouldBind(req); err != nil {
e.Pong(c, e.InvalidParameter, err.Error())
return
}
// query status
submitStatus, status := h.statusService.Query(req.SubmissionID, true)
// check permission
role := claim.(*model.Claim).Role
if role >= model.RoleAdmin || submitStatus.Submission.UserID == claim.(*model.Claim).UID {
// full status
e.Pong(c, status, submitStatus)
return
} else {
// strip out code
submitStatus.Submission.Code = ""
e.Pong(c, status, submitStatus)
return
}
}

View File

@ -0,0 +1,49 @@
package status
import (
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/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 [admin] query status by problem version
// @Description Retrieve all judgement results corresponding to the problem version.
// @Tags status,admin
// @Accept application/x-www-form-urlencoded
// @Produce json
// @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"
// @Security Authentication
// @Router /v1/status/query/version [post]
func (h *handler) QueryByProblemVersion(c *gin.Context) {
claim, exist := c.Get("claim")
if !exist {
e.Pong[any](c, e.UserUnauthenticated, nil)
return
}
req := new(queryByVersionRequest)
if err := c.ShouldBind(req); err != nil {
e.Pong(c, e.InvalidParameter, err.Error())
return
}
// check permission
role := claim.(*model.Claim).Role
if role < model.RoleAdmin {
e.Pong[any](c, e.UserUnauthorized, nil)
return
}
submitStatus, status := h.statusService.QueryByVersion(req.ProblemVersionID, req.Offset, req.Limit)
e.Pong(c, status, submitStatus)
}

View File

@ -1,10 +1,9 @@
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"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/internal/service/submission"
"github.com/gin-gonic/gin"
)
@ -15,63 +14,66 @@ type createRequest struct {
}
// Create
// @Summary create a submission
// @Description create a submission
// @Summary submit for judgement
// @Description Submit the code for judgement.
// @Tags 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 ""
// @Response 200 {object} e.Response[uint] "submission id"
// @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)
e.Pong[any](c, e.UserUnauthenticated, nil)
return
}
uid := claim.(*global.Claim).UID
uid := claim.(*model.Claim).UID
role := claim.(*model.Claim).Role
// guest can not submit
if role < model.RoleGeneral {
e.Pong[any](c, e.UserUnauthorized, nil)
return
}
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
}
// create submission
createData := &submission.CreateData{
ProblemID: req.Pid,
UserID: uid,
Language: req.Language,
Code: req.Code,
}
s, status := h.submissionService.Create(createData)
res, status := h.submissionService.Create(createData)
if status != e.Success {
e.Pong(c, status, nil)
e.Pong[any](c, status, nil)
return
}
// query latest version
pv, status := h.problemService.QueryLatestVersion(req.Pid)
if status != e.Success {
e.Pong(c, status, nil)
e.Pong[any](c, status, nil)
return
}
// submit judge
payload := &model.SubmitJudgePayload{
ProblemVersionID: pv.ID,
StorageKey: pv.StorageKey,
Submission: *s,
Submission: *res,
}
_, status = h.taskService.SubmitJudge(payload)
e.Pong(c, status, nil)
e.Pong[any](c, status, res.ID)
}

View File

@ -1,12 +1,14 @@
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"
"git.0x7f.app/WOJ/woj-server/internal/misc/log"
"git.0x7f.app/WOJ/woj-server/internal/service/problem"
"git.0x7f.app/WOJ/woj-server/internal/service/status"
"git.0x7f.app/WOJ/woj-server/internal/service/submission"
"git.0x7f.app/WOJ/woj-server/internal/service/task"
"git.0x7f.app/WOJ/woj-server/internal/web/jwt"
"github.com/gin-gonic/gin"
"github.com/samber/do"
"go.uber.org/zap"
)
@ -14,30 +16,28 @@ 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
jwtService jwt.Service
problemService problem.Service
statusService status.Service
submissionService submission.Service
taskService task.Service
}
func RouteRegister(g *global.Global, group *gin.RouterGroup) {
func RouteRegister(rg *gin.RouterGroup, i *do.Injector) {
app := &handler{
log: g.Log,
jwtService: g.Jwt,
problemService: problem.NewService(g),
statusService: status.NewService(g),
submissionService: submission.NewService(g),
taskService: task.NewService(g),
log: do.MustInvoke[log.Service](i).GetLogger("api.submission"),
jwtService: do.MustInvoke[jwt.Service](i),
problemService: do.MustInvoke[problem.Service](i),
statusService: do.MustInvoke[status.Service](i),
submissionService: do.MustInvoke[submission.Service](i),
taskService: do.MustInvoke[task.Service](i),
}
group.POST("/create", app.jwtService.Handler(true), app.Create)
group.POST("/query", app.Query)
group.POST("/rejudge", app.jwtService.Handler(true), app.Rejudge)
rg.POST("/create", app.jwtService.Handler(true), app.Create)
rg.POST("/rejudge", app.jwtService.Handler(true), app.Rejudge)
}

View File

@ -1,71 +0,0 @@
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

@ -1,9 +1,8 @@
package submission
import (
"github.com/WHUPRJ/woj-server/internal/e"
"github.com/WHUPRJ/woj-server/internal/global"
"github.com/WHUPRJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/model"
"github.com/gin-gonic/gin"
)
@ -12,52 +11,55 @@ type rejudgeRequest struct {
}
// Rejudge
// @Summary rejudge a submission
// @Description rejudge a submission
// @Summary [admin] rejudge a specific submission
// @Description rejudge a specific submission
// @Tags submission,admin
// @Accept application/x-www-form-urlencoded
// @Produce json
// @Param sid formData int true "submission id"
// @Response 200 {object} e.Response ""
// @Response 200 {object} e.Response[any] "nothing"
// @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)
e.Pong[any](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
role := claim.(*model.Claim).Role
if role < model.RoleAdmin {
e.Pong(c, e.UserUnauthorized, nil)
e.Pong[any](c, e.UserUnauthorized, nil)
return
}
// query submission
s, status := h.submissionService.QueryBySid(req.Sid, false)
if status != e.Success {
e.Pong(c, status, nil)
e.Pong[any](c, status, nil)
return
}
// query latest problem version
pv, status := h.problemService.QueryLatestVersion(s.ProblemID)
if status != e.Success {
e.Pong(c, status, nil)
e.Pong[any](c, status, nil)
return
}
// submit judge
_, status = h.taskService.SubmitJudge(&model.SubmitJudgePayload{
ProblemVersionID: pv.ID,
StorageKey: pv.StorageKey,
Submission: *s,
})
e.Pong(c, status, nil)
e.Pong[any](c, status, nil)
}

View File

@ -1,9 +1,9 @@
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"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/internal/service/user"
"github.com/gin-gonic/gin"
)
@ -16,21 +16,22 @@ type createRequest struct {
// Create
// @Summary create a new user
// @Description create a new user
// @Tags 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"
// @Response 200 {object} e.Response[string] "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
}
// create user
createData := &user.CreateData{
UserName: req.UserName,
Password: req.Password,
@ -38,17 +39,19 @@ func (h *handler) Create(c *gin.Context) {
}
u, status := h.userService.Create(createData)
if status != e.Success {
e.Pong(c, status, nil)
e.Pong[any](c, status, nil)
return
}
// update version in cache
version, status := h.userService.IncrVersion(u.ID)
if status != e.Success {
e.Pong(c, status, nil)
e.Pong[any](c, status, nil)
return
}
claim := &global.Claim{
// sign jwt token
claim := &model.Claim{
UID: u.ID,
Role: u.Role,
Version: version,

View File

@ -1,9 +1,11 @@
package user
import (
"github.com/WHUPRJ/woj-server/internal/global"
"github.com/WHUPRJ/woj-server/internal/service/user"
"git.0x7f.app/WOJ/woj-server/internal/misc/log"
"git.0x7f.app/WOJ/woj-server/internal/service/user"
"git.0x7f.app/WOJ/woj-server/internal/web/jwt"
"github.com/gin-gonic/gin"
"github.com/samber/do"
"go.uber.org/zap"
)
@ -18,19 +20,19 @@ type Handler interface {
type handler struct {
log *zap.Logger
jwtService global.JwtService
jwtService jwt.Service
userService user.Service
}
func RouteRegister(g *global.Global, group *gin.RouterGroup) {
func RouteRegister(rg *gin.RouterGroup, i *do.Injector) {
app := &handler{
log: g.Log,
jwtService: g.Jwt,
userService: user.NewService(g),
log: do.MustInvoke[log.Service](i).GetLogger("api.user"),
jwtService: do.MustInvoke[jwt.Service](i),
userService: do.MustInvoke[user.Service](i),
}
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)
rg.POST("/create", app.Create)
rg.POST("/login", app.Login)
rg.POST("/logout", app.jwtService.Handler(true), app.Logout)
rg.POST("/profile", app.jwtService.Handler(true), app.Profile)
}

View File

@ -1,9 +1,9 @@
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"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/internal/service/user"
"github.com/gin-gonic/gin"
)
@ -12,20 +12,25 @@ type loginRequest struct {
Password string `form:"password" binding:"required"`
}
type loginResponse struct {
Token string `json:"token"`
NickName string `json:"nickname"`
}
// Login
// @Summary login
// @Description login and return token
// @Tags user
// @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"
// @Response 200 {object} e.Response[loginResponse] "jwt token and user's 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)
e.Pong(c, e.InvalidParameter, err.Error())
return
}
@ -36,24 +41,21 @@ func (h *handler) Login(c *gin.Context) {
}
u, status := h.userService.Login(loginData)
if status != e.Success {
e.Pong(c, status, nil)
e.Pong[any](c, status, nil)
return
}
// sign and return token
version, status := h.userService.IncrVersion(u.ID)
if status != e.Success {
e.Pong(c, status, nil)
e.Pong[any](c, status, nil)
return
}
claim := &global.Claim{
claim := &model.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,
})
e.Pong(c, status, loginResponse{Token: token, NickName: u.NickName})
}

View File

@ -1,26 +1,27 @@
package user
import (
"github.com/WHUPRJ/woj-server/internal/e"
"github.com/WHUPRJ/woj-server/internal/global"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/model"
"github.com/gin-gonic/gin"
)
// Logout
// @Summary logout
// @Description logout
// @Tags user
// @Accept application/x-www-form-urlencoded
// @Produce json
// @Response 200 {object} e.Response "nil"
// @Response 200 {object} e.Response[any] "nothing"
// @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)
e.Pong[any](c, e.UserUnauthenticated, nil)
return
}
_, status := h.userService.IncrVersion(claim.(*global.Claim).UID)
e.Pong(c, status, nil)
_, status := h.userService.IncrVersion(claim.(*model.Claim).UID)
e.Pong[any](c, status, nil)
}

View File

@ -1,9 +1,9 @@
package user
import (
"github.com/WHUPRJ/woj-server/internal/e"
"github.com/WHUPRJ/woj-server/internal/global"
"github.com/WHUPRJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/pkg/utils"
"github.com/gin-gonic/gin"
)
@ -14,41 +14,39 @@ type profileRequest struct {
// Profile
// @Summary profile
// @Description fetch user profile
// @Tags user
// @Accept application/x-www-form-urlencoded
// @Produce json
// @Param uid formData int false "user id"
// @Response 200 {object} e.Response "user info"
// @Response 200 {object} e.Response[model.User] "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)
e.Pong[any](c, e.UserUnauthenticated, nil)
return
}
uid := claim.(*global.Claim).UID
role := claim.(*global.Claim).Role
uid := claim.(*model.Claim).UID
role := claim.(*model.Claim).Role
req := new(profileRequest)
if err := c.ShouldBind(req); err != nil {
e.Pong(c, e.InvalidParameter, nil)
e.Pong(c, e.InvalidParameter, err.Error())
return
}
if req.UID == 0 {
req.UID = uid
} else if req.UID != uid && role < model.RoleGeneral {
e.Pong(c, e.UserUnauthorized, nil)
user, status := h.userService.Profile(utils.If(req.UID == 0, uid, req.UID))
if status != e.Success {
e.Pong[any](c, status, nil)
return
}
user, status := h.userService.Profile(req.UID)
// TODO: >= admin can see is_enable
if role < model.RoleAdmin && user.ID != uid {
e.Pong[any](c, e.UserUnauthorized, nil)
return
}
e.Pong(c, status, user)
}

View File

@ -1,18 +1,24 @@
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"
"fmt"
"git.0x7f.app/WOJ/woj-server/internal/api/runner"
"git.0x7f.app/WOJ/woj-server/internal/misc/config"
"git.0x7f.app/WOJ/woj-server/internal/misc/log"
"git.0x7f.app/WOJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/pkg/utils"
"git.0x7f.app/WOJ/woj-server/pkg/zapasynq"
"github.com/hibiken/asynq"
"github.com/samber/do"
"go.uber.org/zap"
"runtime"
)
func RunRunner(g *global.Global) error {
hnd, err := runner.NewRunner(g)
func RunRunner(i *do.Injector) error {
conf := do.MustInvoke[config.Service](i).GetConfig()
rlog := do.MustInvoke[log.Service](i).GetLogger("app.runner")
hnd, err := runner.NewRunner(i)
if err != nil {
return err
}
@ -23,19 +29,19 @@ func RunRunner(g *global.Global) error {
srv := asynq.NewServer(
asynq.RedisClientOpt{
Addr: g.Conf.Redis.Address,
Password: g.Conf.Redis.Password,
DB: g.Conf.Redis.QueueDb,
Addr: fmt.Sprintf("%s:%d", conf.Redis.Address, conf.Redis.Port),
Password: conf.Redis.Password,
DB: conf.Redis.QueueDb,
},
asynq.Config{
Concurrency: utils.If(runtime.NumCPU() > 1, runtime.NumCPU()-1, 1).(int),
Logger: zapasynq.New(g.Log),
Concurrency: utils.If(runtime.NumCPU() > 1, runtime.NumCPU()-1, 1),
Logger: zapasynq.New(rlog),
Queues: map[string]int{model.QueueRunner: 1},
},
)
if err := srv.Run(mux); err != nil {
g.Log.Warn("could not run server", zap.Error(err))
rlog.Warn("could not run server", zap.Error(err))
return err
}

View File

@ -2,17 +2,18 @@ package server
import (
"context"
"errors"
"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"
"git.0x7f.app/WOJ/woj-server/internal/api/consumer"
"git.0x7f.app/WOJ/woj-server/internal/misc/config"
"git.0x7f.app/WOJ/woj-server/internal/misc/log"
"git.0x7f.app/WOJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/internal/repo/db"
"git.0x7f.app/WOJ/woj-server/internal/web/router"
"git.0x7f.app/WOJ/woj-server/pkg/utils"
"git.0x7f.app/WOJ/woj-server/pkg/zapasynq"
"github.com/hibiken/asynq"
"github.com/samber/do"
"go.uber.org/zap"
"net/http"
"os"
@ -22,23 +23,27 @@ import (
"time"
)
func RunServer(g *global.Global) error {
// Setup Database
g.Db = new(postgresql.Repo)
g.Db.Setup(g)
func RunServerMigrate(i *do.Injector) error {
slog := do.MustInvoke[log.Service](i).GetLogger("app.server")
// Setup Redis
g.Redis = new(redis.Repo)
g.Redis.Setup(g)
// Migrate and shutdown database
err := do.MustInvoke[db.Service](i).Close()
if err != nil {
slog.Warn("Database Close Failed", zap.Error(err))
}
// Setup JWT
g.Jwt = jwt.NewJwtService(g)
return err
}
func RunServer(i *do.Injector) error {
conf := do.MustInvoke[config.Service](i).GetConfig()
slog := do.MustInvoke[log.Service](i).GetLogger("app.server")
// Prepare Router
routers := router.InitRouters(g)
routers := do.MustInvoke[router.Service](i).GetRouter()
// Create Server
addr := fmt.Sprintf("%s:%d", g.Conf.WebServer.Address, g.Conf.WebServer.Port)
addr := fmt.Sprintf("%s:%d", conf.WebServer.Address, conf.WebServer.Port)
server := &http.Server{
Addr: addr,
Handler: routers,
@ -46,57 +51,57 @@ func RunServer(g *global.Global) error {
// Run Server
go func() {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
g.Log.Fatal("ListenAndServe Failed", zap.Error(err))
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
slog.Fatal("ListenAndServe Failed", zap.Error(err))
}
}()
// Create Queue
queueMux := asynq.NewServeMux()
{
handler := consumer.NewConsumer(g)
handler := consumer.NewConsumer(i)
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,
Addr: fmt.Sprintf("%s:%d", conf.Redis.Address, conf.Redis.Port),
Password: conf.Redis.Password,
DB: conf.Redis.QueueDb,
},
asynq.Config{
Concurrency: utils.If(runtime.NumCPU() > 1, runtime.NumCPU()-1, 1).(int),
Logger: zapasynq.New(g.Log),
Concurrency: utils.If(runtime.NumCPU() > 1, runtime.NumCPU()-1, 1),
Logger: zapasynq.New(slog),
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))
slog.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 ...")
slog.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))
slog.Warn("Server Shutdown Failed", zap.Error(err))
}
// Graceful Shutdown Queue
queueSrv.Shutdown()
// Graceful Shutdown Database
err = g.Db.Close()
err = do.MustInvoke[db.Service](i).Close()
if err != nil {
g.Log.Warn("Database Close Failed", zap.Error(err))
slog.Warn("Database Close Failed", zap.Error(err))
}
return err

View File

@ -1,35 +1,26 @@
package e
import (
"github.com/WHUPRJ/woj-server/pkg/utils"
"git.0x7f.app/WOJ/woj-server/pkg/utils"
"github.com/gin-gonic/gin"
"net/http"
)
type Response struct {
type Response[T any] struct {
Code int `json:"code"`
Msg string `json:"msg"`
Body interface{} `json:"body"`
Body T `json:"body"`
}
func Wrap(status Status, body interface{}) interface{} {
return Response{
func wrap[T any](status Status, body T) Response[interface{}] {
return Response[interface{}]{
Code: int(status),
Msg: status.String(),
Body: utils.If(status == Success, body, nil),
Body: utils.If[interface{}](status == Success, body, nil),
}
}
func Pong(c *gin.Context, status Status, body interface{}) {
func Pong[T any](c *gin.Context, status Status, body T) {
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)
}
c.JSON(http.StatusOK, wrap(status, body))
}

View File

@ -1,15 +0,0 @@
package global
import (
"github.com/WHUPRJ/woj-server/internal/pkg/metrics"
"go.uber.org/zap"
)
type Global struct {
Log *zap.Logger
Conf *Config
Stat *metrics.Metrics
Db Repo
Redis Repo
Jwt JwtService
}

View File

@ -1,23 +0,0 @@
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,7 +0,0 @@
package global
type Repo interface {
Setup(*Global)
Get() interface{}
Close() error
}

View File

@ -1,40 +0,0 @@
package global
import (
"github.com/WHUPRJ/woj-server/pkg/utils"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"gopkg.in/yaml.v3"
"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())
}
}
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

@ -0,0 +1,48 @@
package config
import (
"git.0x7f.app/WOJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/pkg/utils"
"github.com/samber/do"
"github.com/urfave/cli/v2"
"gopkg.in/yaml.v3"
"log"
)
var _ Service = (*service)(nil)
type Service interface {
GetConfig() *model.Config
HealthCheck() error
}
func NewService(i *do.Injector) (Service, error) {
cliCtx := do.MustInvoke[*cli.Context](i)
data, err := utils.FileRead(cliCtx.String("config"))
if err != nil {
log.Printf("Failed to setup config: %s\n", err.Error())
return nil, err
}
srv := &service{}
err = yaml.Unmarshal(data, &srv.conf)
if err != nil {
log.Printf("Failed to setup config: %s\n", err.Error())
return nil, err
}
return srv, nil
}
type service struct {
conf model.Config
}
func (s *service) GetConfig() *model.Config {
return &s.conf
}
func (s *service) HealthCheck() error {
return nil
}

91
internal/misc/log/zap.go Normal file
View File

@ -0,0 +1,91 @@
package log
import (
"git.0x7f.app/WOJ/woj-server/cmd"
"git.0x7f.app/WOJ/woj-server/internal/misc/config"
"git.0x7f.app/WOJ/woj-server/pkg/utils"
"github.com/TheZeroSlave/zapsentry"
"github.com/getsentry/sentry-go"
"github.com/samber/do"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"log"
)
var _ Service = (*service)(nil)
type Service interface {
GetRawLogger() *zap.Logger
GetLogger(domain string) *zap.Logger
HealthCheck() error
}
func NewService(i *do.Injector) (Service, error) {
srv := &service{}
srv.confService = do.MustInvoke[config.Service](i)
c := srv.confService.GetConfig()
cfg := zap.Config{
Level: zap.NewAtomicLevelAt(utils.If(
c.Development,
zapcore.DebugLevel,
zapcore.InfoLevel,
)),
Development: c.Development,
Encoding: "console", // or json
EncoderConfig: utils.If(
c.Development,
zap.NewDevelopmentEncoderConfig(),
zap.NewProductionEncoderConfig(),
),
OutputPaths: []string{"stdout"},
ErrorOutputPaths: []string{"stderr"},
}
var err error
srv.logger, err = cfg.Build()
if err != nil {
log.Printf("Failed to setup Zap: %s\n", err.Error())
return nil, err
}
if cmd.SentryDSN != "" {
srv.logger = attachSentry(srv.logger)
}
return srv, nil
}
func attachSentry(log *zap.Logger) *zap.Logger {
cfg := zapsentry.Configuration{
Level: zapcore.ErrorLevel,
EnableBreadcrumbs: true,
BreadcrumbLevel: zapcore.InfoLevel,
}
core, err := zapsentry.NewCore(cfg, zapsentry.NewSentryClientFromClient(sentry.CurrentHub().Client()))
if err != nil {
log.Warn("failed to init zap", zap.Error(err))
return log
}
log = zapsentry.AttachCoreToLogger(core, log)
return log.With(zapsentry.NewScope())
}
type service struct {
confService config.Service
logger *zap.Logger
}
func (s *service) GetRawLogger() *zap.Logger {
return s.logger
}
func (s *service) GetLogger(domain string) *zap.Logger {
return s.logger.Named(domain)
}
func (s *service) HealthCheck() error {
return nil
}

View File

@ -1,7 +1,7 @@
package model
import (
"github.com/WHUPRJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/e"
)
const (

View File

@ -1,4 +1,4 @@
package global
package model
type ConfigWebServer struct {
Address string `yaml:"Address"`
@ -11,6 +11,7 @@ type ConfigRedis struct {
Db int `yaml:"Db"`
QueueDb int `yaml:"QueueDb"`
Address string `yaml:"Address"`
Port int `yaml:"Port"`
Password string `yaml:"Password"`
}
@ -24,6 +25,7 @@ type ConfigDatabase struct {
MaxOpenConns int `yaml:"MaxOpenConns"`
MaxIdleConns int `yaml:"MaxIdleConns"`
ConnMaxLifetime int `yaml:"ConnMaxLifetime"`
TimeZone string `yaml:"TimeZone"`
}
type ConfigStorage struct {

12
internal/model/jwt.go Normal file
View File

@ -0,0 +1,12 @@
package model
import (
"github.com/golang-jwt/jwt/v4"
)
type Claim struct {
UID uint `json:"id"`
Role Role `json:"role"`
Version int64 `json:"version"`
jwt.RegisteredClaims
}

View File

@ -1,11 +1,12 @@
package global
package model
import (
"github.com/gin-gonic/gin"
"github.com/samber/do"
)
type EndpointInfo struct {
Version string
Path string
Register func(g *Global, group *gin.RouterGroup)
Register func(rg *gin.RouterGroup, i *do.Injector)
}

View File

@ -2,7 +2,7 @@ package cast
import (
"fmt"
"github.com/WHUPRJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/e"
"strconv"
)

View File

@ -1,60 +0,0 @@
package metrics
import (
"github.com/WHUPRJ/woj-server/internal/e"
"github.com/WHUPRJ/woj-server/internal/pkg/cast"
"github.com/prometheus/client_golang/prometheus"
)
type Metrics struct {
namespace string
subsystem string
counter *prometheus.CounterVec
hist *prometheus.HistogramVec
logPaths []string
}
func (m *Metrics) Setup(namespace string, subsystem string) {
m.namespace = namespace
m.subsystem = subsystem
m.counter = prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "requests_total",
Help: "Total number of requests",
},
[]string{"method", "url"},
)
m.hist = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "requests_details",
Help: "Details of each request",
Buckets: []float64{0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 25, 50, 100, 250, 500, 1000},
},
[]string{"method", "url", "success", "http_code", "err_code"},
)
prometheus.MustRegister(m.counter, m.hist)
}
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,
}).Inc()
m.hist.With(prometheus.Labels{
"method": method,
"url": url,
"success": cast.ToString(success),
"http_code": cast.ToString(httpCode),
"err_code": cast.ToString(errCode),
}).Observe(elapsed)
}

60
internal/repo/cache/redis.go vendored Normal file
View File

@ -0,0 +1,60 @@
package cache
import (
"context"
"fmt"
"git.0x7f.app/WOJ/woj-server/internal/misc/config"
"git.0x7f.app/WOJ/woj-server/internal/misc/log"
"github.com/redis/go-redis/v9"
"github.com/samber/do"
"go.uber.org/zap"
)
var _ Service = (*service)(nil)
type Service interface {
Get() *redis.Client
Close() error
HealthCheck() error
}
func NewService(i *do.Injector) (Service, error) {
srv := &service{}
srv.log = do.MustInvoke[log.Service](i).GetLogger("redis")
conf := do.MustInvoke[config.Service](i).GetConfig()
srv.setup(fmt.Sprintf("%s:%d", conf.Redis.Address, conf.Redis.Port), conf.Redis.Password, conf.Redis.Db)
return srv, srv.err
}
type service struct {
log *zap.Logger
client *redis.Client
err error
}
func (s *service) Get() *redis.Client {
return s.client
}
func (s *service) Close() error {
return s.client.Close()
}
func (s *service) HealthCheck() error {
return s.err
}
func (s *service) setup(addr string, password string, db int) {
s.client = redis.NewClient(&redis.Options{
Addr: addr,
Password: password,
DB: db,
})
_, s.err = s.client.Ping(context.Background()).Result()
if s.err != nil {
s.log.Error("Redis ping failed", zap.Error(s.err))
return
}
}

165
internal/repo/db/pg.go Normal file
View File

@ -0,0 +1,165 @@
package db
import (
"database/sql"
"errors"
"fmt"
"git.0x7f.app/WOJ/woj-server/internal/misc/config"
"git.0x7f.app/WOJ/woj-server/internal/misc/log"
"git.0x7f.app/WOJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/pkg/utils"
"github.com/samber/do"
"go.uber.org/zap"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/schema"
"hash/fnv"
"moul.io/zapgorm2"
"time"
)
var _ Service = (*service)(nil)
type Service interface {
Get() *gorm.DB
Close() error
HealthCheck() error
}
func NewService(i *do.Injector) (Service, error) {
srv := &service{}
srv.log = do.MustInvoke[log.Service](i).GetLogger("postgresql")
conf := do.MustInvoke[config.Service](i).GetConfig()
srv.setup(conf)
return srv, srv.err
}
type service struct {
log *zap.Logger
db *gorm.DB
err error
}
func (s *service) Get() *gorm.DB {
return s.db
}
func (s *service) Close() error {
var db *sql.DB
db, s.err = s.db.DB()
if s.err != nil {
return s.err
}
s.err = db.Close()
return s.err
}
func (s *service) HealthCheck() error {
return s.err
}
func (s *service) setup(conf *model.Config) {
s.log.Info("Connecting to database...")
logger := zapgorm2.New(s.log)
logger.IgnoreRecordNotFoundError = true
tz := utils.If(conf.Database.TimeZone == "", "Asia/Shanghai", conf.Database.TimeZone)
dsn := fmt.Sprintf(
"user=%s password=%s dbname=%s host=%s port=%d sslmode=disable TimeZone=%s",
conf.Database.User,
conf.Database.Password,
conf.Database.Database,
conf.Database.Host,
conf.Database.Port,
tz,
)
s.db, s.err = gorm.Open(
postgres.Open(dsn),
&gorm.Config{
NamingStrategy: schema.NamingStrategy{
SingularTable: true,
TablePrefix: conf.Database.Prefix,
},
PrepareStmt: true,
Logger: logger,
},
)
if s.err != nil {
s.log.Error("Failed to connect to database", zap.Error(s.err))
return
}
var db *sql.DB
db, s.err = s.checkAlive(3)
if s.err != nil {
s.log.Error("Database is not alive", zap.Error(s.err))
return
}
db.SetMaxOpenConns(conf.Database.MaxOpenConns)
db.SetMaxIdleConns(conf.Database.MaxIdleConns)
db.SetConnMaxLifetime(time.Duration(conf.Database.ConnMaxLifetime) * time.Minute)
s.migrateDatabase()
}
func (s *service) migrateDatabase() {
s.log.Info("Auto Migrating database...")
// Running AutoMigrate concurrently on the same model fails with various race conditions
// https://github.com/go-gorm/gorm/pull/6680
// https://github.com/go-gorm/postgres/pull/224
// Obtain a lock to prevent concurrent AutoMigrate
lockID := func(s string) int64 {
h := fnv.New64a()
_, err := h.Write([]byte(s))
return utils.If(err != nil, int64(0x4242AA55), int64(h.Sum64()))
}("gorm:migrator")
s.err = s.db.Exec("SELECT pg_advisory_lock(?)", lockID).Error
if s.err != nil {
s.log.Error("Failed to obtain lock", zap.Error(s.err))
return
}
_ = s.db.AutoMigrate(&model.User{})
_ = s.db.AutoMigrate(&model.Problem{})
_ = s.db.AutoMigrate(&model.ProblemVersion{})
_ = s.db.AutoMigrate(&model.Submission{})
_ = s.db.AutoMigrate(&model.Status{})
s.err = s.db.Exec("SELECT pg_advisory_unlock(?)", lockID).Error
if s.err != nil {
s.log.Error("Failed to release lock", zap.Error(s.err))
}
}
func (s *service) 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 := s.db.DB()
if err != nil {
s.log.Warn("failed to get sql.DB instance", zap.Error(err))
time.Sleep(5 * time.Second)
return s.checkAlive(retry - 1)
}
err = db.Ping()
if err != nil {
s.log.Warn("failed to ping database", zap.Error(err))
time.Sleep(5 * time.Second)
return s.checkAlive(retry - 1)
}
s.log.Info("database connect established")
return db, nil
}

View File

@ -1,113 +0,0 @@
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

@ -1,39 +0,0 @@
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,81 +0,0 @@
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"
"github.com/gin-contrib/pprof"
ginZap "github.com/gin-contrib/zap"
"github.com/gin-gonic/gin"
"github.com/prometheus/client_golang/prometheus/promhttp"
swaggerFiles "github.com/swaggo/files"
"github.com/swaggo/gin-swagger"
"net/http"
"time"
)
func InitRouters(g *global.Global) *gin.Engine {
gin.SetMode(utils.If(g.Conf.Development, gin.DebugMode, gin.ReleaseMode).(string))
r := gin.New()
r.MaxMultipartMemory = 8 << 20
// Logger middleware and debug
if g.Conf.Development {
// Gin's default logger is pretty enough
r.Use(gin.Logger())
r.Use(gin.Recovery())
// add prof
pprof.Register(r)
} else {
r.Use(ginZap.Ginzap(g.Log, time.RFC3339, false))
r.Use(ginZap.RecoveryWithZap(g.Log, true))
}
// CORS middleware
r.Use(cors.New(cors.Config{
AllowAllOrigins: true,
AllowMethods: []string{"GET", "POST", "PUT", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Length", "Content-Type"},
AllowCredentials: true,
}))
// 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
r.GET("/metrics", gin.WrapH(promhttp.Handler()))
// swagger
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
// health
r.GET("/health", func(c *gin.Context) {
resp := &struct {
Timestamp time.Time `json:"timestamp"`
Status string `json:"status"`
}{
Timestamp: time.Now(),
Status: "ok",
}
c.JSON(http.StatusOK, resp)
})
// api
api := r.Group("/api/")
setupApi(g, api)
// static files
r.Static("/static", "./resource/frontend/static")
r.StaticFile("/", "./resource/frontend/index.html")
r.NoRoute(func(c *gin.Context) {
c.File("./resource/frontend/index.html")
})
return r
}

View File

@ -1,25 +0,0 @@
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

@ -1,8 +1,8 @@
package problem
import (
"github.com/WHUPRJ/woj-server/internal/e"
"github.com/WHUPRJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/model"
"go.uber.org/zap"
)
@ -21,7 +21,7 @@ func (s *service) Create(data *CreateData) (*model.Problem, e.Status) {
IsEnabled: data.IsEnabled,
}
err := s.db.Create(problem).Error
err := s.db.Get().Create(problem).Error
if err != nil {
s.log.Warn("DatabaseError", zap.Error(err), zap.Any("problem", problem))
return nil, e.DatabaseError

View File

@ -1,8 +1,8 @@
package problem
import (
"github.com/WHUPRJ/woj-server/internal/e"
"github.com/WHUPRJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/model"
"github.com/jackc/pgtype"
"go.uber.org/zap"
)
@ -19,7 +19,7 @@ func (s *service) CreateVersion(data *CreateVersionData) (*model.ProblemVersion,
StorageKey: data.StorageKey,
}
err := s.db.Create(problemVersion).Error
err := s.db.Get().Create(problemVersion).Error
if err != nil {
s.log.Warn("DatabaseError", zap.Error(err), zap.Any("problemVersion", problemVersion))
return nil, e.DatabaseError

View File

@ -2,8 +2,8 @@ package problem
import (
"errors"
"github.com/WHUPRJ/woj-server/internal/e"
"github.com/WHUPRJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/model"
"go.uber.org/zap"
"gorm.io/gorm"
"gorm.io/gorm/clause"
@ -12,7 +12,7 @@ import (
func (s *service) Query(pid uint, associations bool, shouldEnable bool) (*model.Problem, e.Status) {
problem := new(model.Problem)
query := s.db
query := s.db.Get()
if associations {
query = query.Preload(clause.Associations)
}

View File

@ -1,8 +1,8 @@
package problem
import (
"github.com/WHUPRJ/woj-server/internal/e"
"github.com/WHUPRJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/model"
"go.uber.org/zap"
"gorm.io/gorm/clause"
)
@ -10,7 +10,7 @@ import (
func (s *service) QueryFuzz(search string, associations bool, shouldEnable bool) ([]*model.Problem, e.Status) {
problems := make([]*model.Problem, 0)
query := s.db
query := s.db.Get()
if associations {
query = query.Preload(clause.Associations)
}
@ -18,7 +18,7 @@ func (s *service) QueryFuzz(search string, associations bool, shouldEnable bool)
query = query.Where("is_enabled = true")
}
query = query.
Where(s.db.Where("title LIKE ?", "%"+search+"%").
Where(s.db.Get().Where("title LIKE ?", "%"+search+"%").
Or("statement LIKE ?", "%"+search+"%"))
err := query.Find(&problems).Error
if err != nil {

View File

@ -2,8 +2,8 @@ package problem
import (
"errors"
"github.com/WHUPRJ/woj-server/internal/e"
"github.com/WHUPRJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/model"
"go.uber.org/zap"
"gorm.io/gorm"
)
@ -14,7 +14,7 @@ func (s *service) QueryLatestVersion(pid uint) (*model.ProblemVersion, e.Status)
IsEnabled: true,
}
err := s.db.
err := s.db.Get().
Where(problemVersion).
Last(&problemVersion).Error
if errors.Is(err, gorm.ErrRecordNotFound) {

View File

@ -2,8 +2,8 @@ package problem
import (
"errors"
"github.com/WHUPRJ/woj-server/internal/e"
"github.com/WHUPRJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/model"
"go.uber.org/zap"
"gorm.io/gorm"
)
@ -11,7 +11,7 @@ import (
func (s *service) QueryVersion(pvid uint, shouldEnable bool) (*model.ProblemVersion, e.Status) {
problemVersion := new(model.ProblemVersion)
err := s.db.First(&problemVersion, pvid).Error
err := s.db.Get().First(&problemVersion, pvid).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, e.ProblemVersionNotFound
}

View File

@ -1,11 +1,12 @@
package problem
import (
"github.com/WHUPRJ/woj-server/internal/e"
"github.com/WHUPRJ/woj-server/internal/global"
"github.com/WHUPRJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/misc/log"
"git.0x7f.app/WOJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/internal/repo/db"
"github.com/samber/do"
"go.uber.org/zap"
"gorm.io/gorm"
)
var _ Service = (*service)(nil)
@ -20,16 +21,22 @@ type Service interface {
UpdateVersion(pvid uint, values interface{}) e.Status
QueryVersion(pvid uint, shouldEnable bool) (*model.ProblemVersion, e.Status)
QueryLatestVersion(pid uint) (*model.ProblemVersion, e.Status)
HealthCheck() error
}
func NewService(i *do.Injector) (Service, error) {
return &service{
log: do.MustInvoke[log.Service](i).GetLogger("problem"),
db: do.MustInvoke[db.Service](i),
}, nil
}
type service struct {
log *zap.Logger
db *gorm.DB
db db.Service
}
func NewService(g *global.Global) Service {
return &service{
log: g.Log,
db: g.Db.Get().(*gorm.DB),
}
func (s *service) HealthCheck() error {
return nil
}

View File

@ -1,13 +1,13 @@
package problem
import (
"github.com/WHUPRJ/woj-server/internal/e"
"github.com/WHUPRJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/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
err := s.db.Get().Save(problem).Error
if err != nil {
s.log.Warn("DatabaseError", zap.Error(err), zap.Any("problem", problem))
return nil, e.DatabaseError

View File

@ -1,13 +1,13 @@
package problem
import (
"github.com/WHUPRJ/woj-server/internal/e"
"github.com/WHUPRJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/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
err := s.db.Get().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

View File

@ -2,8 +2,8 @@ package runner
import (
"fmt"
"github.com/WHUPRJ/woj-server/internal/e"
"github.com/WHUPRJ/woj-server/pkg/utils"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/pkg/utils"
"go.uber.org/zap"
"os"
"os/exec"
@ -36,6 +36,10 @@ func (s *service) execute(script string, args ...string) error {
p := filepath.Join(ScriptsDir, script)
cmd := exec.Command(p, args...)
cmd.Dir = ScriptsDir
if s.verbose {
cmd.Stdout = os.Stderr
cmd.Stderr = os.Stderr
}
return cmd.Run()
}

View File

@ -2,8 +2,8 @@ package runner
import (
"fmt"
"github.com/WHUPRJ/woj-server/internal/e"
"github.com/WHUPRJ/woj-server/pkg/utils"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/pkg/utils"
"os"
"path/filepath"
)
@ -16,14 +16,14 @@ func (s *service) Compile(version uint, user string, lang string) (JudgeStatus,
log := filepath.Join(UserDir, user, fmt.Sprintf("%s.compile.log", user))
msg, err := utils.FileRead(log)
msg = utils.If(err == nil, msg, nil).([]byte)
msg = utils.If(err == nil, msg, nil)
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)
utils.If(status == e.Success, e.RunnerUserCompileFailed, status)
}
return JudgeStatus{}, e.Success

View File

@ -16,9 +16,9 @@ type Config struct {
} `json:"Runtime"`
Languages []struct {
Lang string `json:"Lang"`
Type string `json:"Type"`
Script string `json:"Script"`
Cmp string `json:"Cmp"`
Type string `json:"Type,omitempty"`
Script string `json:"Script,omitempty"`
Cmp string `json:"Cmp,omitempty"`
} `json:"Languages"`
Tasks []struct {
Id int `json:"Id"`

View File

@ -1,8 +1,8 @@
package runner
import (
"github.com/WHUPRJ/woj-server/internal/e"
"github.com/WHUPRJ/woj-server/pkg/utils"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/pkg/utils"
"go.uber.org/zap"
"os"
"os/exec"
@ -10,7 +10,7 @@ import (
)
func (s *service) EnsureDeps(force bool) e.Status {
mark := filepath.Join(Prefix, ".mark.docker")
mark := filepath.Join(Prefix, ".mark.image")
if force {
_ = os.Remove(mark)
@ -18,9 +18,13 @@ func (s *service) EnsureDeps(force bool) e.Status {
return e.Success
}
script := filepath.Join(ScriptsDir, "prepare_container.sh")
script := filepath.Join(ScriptsDir, "prepare_images.sh")
cmd := exec.Command(script)
cmd.Dir = ScriptsDir
if s.verbose {
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
}
err := cmd.Run()
if err != nil {
s.log.Warn("prebuild docker images failed", zap.Error(err))

View File

@ -2,10 +2,10 @@ 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"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/pkg/down"
"git.0x7f.app/WOJ/woj-server/pkg/unzip"
"git.0x7f.app/WOJ/woj-server/pkg/utils"
"go.uber.org/zap"
"os"
"path/filepath"

View File

@ -1,6 +1,6 @@
package runner
import "github.com/WHUPRJ/woj-server/internal/e"
import "git.0x7f.app/WOJ/woj-server/internal/e"
func (s *service) RunAndJudge(version uint, user string, lang string, config *Config) (JudgeStatus, int32, e.Status) {
// run user program

View File

@ -1,8 +1,10 @@
package runner
import (
"github.com/WHUPRJ/woj-server/internal/e"
"github.com/WHUPRJ/woj-server/internal/global"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/misc/config"
"git.0x7f.app/WOJ/woj-server/internal/misc/log"
"github.com/samber/do"
"go.uber.org/zap"
)
@ -23,14 +25,22 @@ type Service interface {
ParseConfig(version uint, skipCheck bool) (Config, error)
// ProblemExists check if problem exists
ProblemExists(version uint) bool
HealthCheck() error
}
func NewService(i *do.Injector) (Service, error) {
return &service{
log: do.MustInvoke[log.Service](i).GetLogger("runner"),
verbose: do.MustInvoke[config.Service](i).GetConfig().Development,
}, nil
}
type service struct {
log *zap.Logger
verbose bool
}
func NewService(g *global.Global) Service {
return &service{
log: g.Log,
}
func (s *service) HealthCheck() error {
return nil
}

View File

@ -5,7 +5,7 @@ import (
"encoding/json"
"encoding/xml"
"fmt"
"github.com/WHUPRJ/woj-server/pkg/utils"
"git.0x7f.app/WOJ/woj-server/pkg/utils"
"golang.org/x/text/encoding/charmap"
"io"
"path/filepath"

View File

@ -1,8 +1,8 @@
package status
import (
"github.com/WHUPRJ/woj-server/internal/e"
"github.com/WHUPRJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/model"
"github.com/jackc/pgtype"
"go.uber.org/zap"
)
@ -14,7 +14,7 @@ type CreateData struct {
Point int32
}
func (s service) Create(data *CreateData) (*model.Status, e.Status) {
func (s *service) Create(data *CreateData) (*model.Status, e.Status) {
status := &model.Status{
SubmissionID: data.SubmissionID,
ProblemVersionID: data.ProblemVersionID,
@ -26,7 +26,7 @@ func (s service) Create(data *CreateData) (*model.Status, e.Status) {
IsEnabled: true,
}
err := s.db.Create(status).Error
err := s.db.Get().Create(status).Error
if err != nil {
s.log.Warn("DatabaseError", zap.Error(err), zap.Any("status", status))
return nil, e.DatabaseError

View File

@ -2,21 +2,21 @@ package status
import (
"errors"
"github.com/WHUPRJ/woj-server/internal/e"
"github.com/WHUPRJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/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) {
func (s *service) Query(sid uint, associations bool) (*model.Status, e.Status) {
status := &model.Status{
SubmissionID: sid,
IsEnabled: true,
}
query := s.db
query := s.db.Get()
if associations {
query = query.Preload(clause.Associations)
}
@ -36,14 +36,14 @@ 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) {
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).
err := s.db.Get().Preload(clause.Associations).
Where(status).
Limit(limit).
Offset(offset).

View File

@ -1,11 +1,12 @@
package status
import (
"github.com/WHUPRJ/woj-server/internal/e"
"github.com/WHUPRJ/woj-server/internal/global"
"github.com/WHUPRJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/misc/log"
"git.0x7f.app/WOJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/internal/repo/db"
"github.com/samber/do"
"go.uber.org/zap"
"gorm.io/gorm"
)
var _ Service = (*service)(nil)
@ -14,16 +15,22 @@ 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)
HealthCheck() error
}
func NewService(i *do.Injector) (Service, error) {
return &service{
log: do.MustInvoke[log.Service](i).GetLogger("status"),
db: do.MustInvoke[db.Service](i),
}, nil
}
type service struct {
log *zap.Logger
db *gorm.DB
db db.Service
}
func NewService(g *global.Global) Service {
return &service{
log: g.Log,
db: g.Db.Get().(*gorm.DB),
}
func (s *service) HealthCheck() error {
return nil
}

View File

@ -2,7 +2,7 @@ package storage
import (
"context"
"github.com/WHUPRJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/e"
"go.uber.org/zap"
"net/url"
"time"

View File

@ -1,10 +1,12 @@
package storage
import (
"github.com/WHUPRJ/woj-server/internal/e"
"github.com/WHUPRJ/woj-server/internal/global"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/misc/config"
"git.0x7f.app/WOJ/woj-server/internal/misc/log"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"github.com/samber/do"
"go.uber.org/zap"
"time"
)
@ -14,6 +16,33 @@ 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)
HealthCheck() error
}
func NewService(i *do.Injector) (Service, error) {
conf := do.MustInvoke[config.Service](i).GetConfig()
srv := &service{
log: do.MustInvoke[log.Service](i).GetLogger("storage"),
bucket: conf.Storage.Bucket,
}
var err error
srv.client, err = minio.New(
conf.Storage.Endpoint,
&minio.Options{
Creds: credentials.NewStaticV4(conf.Storage.AccessKey, conf.Storage.SecretKey, ""),
Secure: conf.Storage.UseSSL,
},
)
if err != nil {
srv.log.Error("failed to create minio client", zap.Error(err))
return nil, err
}
return srv, nil
}
type service struct {
@ -22,20 +51,6 @@ type service struct {
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))
func (s *service) HealthCheck() error {
return nil
}
return &service{
log: g.Log,
client: minioClient,
bucket: g.Conf.Storage.Bucket,
}
}

View File

@ -2,7 +2,7 @@ package storage
import (
"context"
"github.com/WHUPRJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/e"
"go.uber.org/zap"
"time"
)

View File

@ -1,8 +1,8 @@
package submission
import (
"github.com/WHUPRJ/woj-server/internal/e"
"github.com/WHUPRJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/model"
"go.uber.org/zap"
)
@ -21,7 +21,7 @@ func (s *service) Create(data *CreateData) (*model.Submission, e.Status) {
Code: data.Code,
}
err := s.db.Create(submission).Error
err := s.db.Get().Create(submission).Error
if err != nil {
s.log.Warn("DatabaseError", zap.Error(err), zap.Any("submission", submission))
return nil, e.DatabaseError

View File

@ -2,8 +2,8 @@ package submission
import (
"errors"
"github.com/WHUPRJ/woj-server/internal/e"
"github.com/WHUPRJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/model"
"go.uber.org/zap"
"gorm.io/gorm"
"gorm.io/gorm/clause"
@ -17,7 +17,7 @@ func (s *service) Query(pid uint, uid uint, offset int, limit int) ([]*model.Sub
UserID: uid,
}
err := s.db.Preload(clause.Associations).
err := s.db.Get().Preload(clause.Associations).
Where(submission).
Limit(limit).
Offset(offset).
@ -37,7 +37,7 @@ func (s *service) Query(pid uint, uid uint, offset int, limit int) ([]*model.Sub
func (s *service) QueryBySid(sid uint, associations bool) (*model.Submission, e.Status) {
submission := new(model.Submission)
query := s.db
query := s.db.Get()
if associations {
query = query.Preload(clause.Associations)
}

View File

@ -1,11 +1,12 @@
package submission
import (
"github.com/WHUPRJ/woj-server/internal/e"
"github.com/WHUPRJ/woj-server/internal/global"
"github.com/WHUPRJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/misc/log"
"git.0x7f.app/WOJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/internal/repo/db"
"github.com/samber/do"
"go.uber.org/zap"
"gorm.io/gorm"
)
var _ Service = (*service)(nil)
@ -14,16 +15,22 @@ 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)
HealthCheck() error
}
func NewService(i *do.Injector) (Service, error) {
return &service{
log: do.MustInvoke[log.Service](i).GetLogger("submission"),
db: do.MustInvoke[db.Service](i),
}, nil
}
type service struct {
log *zap.Logger
db *gorm.DB
db db.Service
}
func NewService(g *global.Global) Service {
return &service{
log: g.Log,
db: g.Db.Get().(*gorm.DB),
}
func (s *service) HealthCheck() error {
return nil
}

View File

@ -1,7 +1,7 @@
package task
import (
"github.com/WHUPRJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/e"
"github.com/hibiken/asynq"
"go.uber.org/zap"
)

View File

@ -2,8 +2,8 @@ package task
import (
"encoding/json"
"github.com/WHUPRJ/woj-server/internal/e"
"github.com/WHUPRJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/model"
"go.uber.org/zap"
)

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