diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml new file mode 100644 index 0000000..9a2806e --- /dev/null +++ b/.gitea/workflows/build.yaml @@ -0,0 +1,31 @@ +name: Build + +on: + pull_request: + +jobs: + build-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: docker/setup-buildx-action@v3 + - name: Set build time + id: build-time + run: echo "time=$(date +%s)" >> $GITHUB_OUTPUT + - name: build-app + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + push: false + tags: 015-app:test + build-args: | + VERSION=${{ gitea.ref_name || github.ref_name }} + BUILD_TIME=${{ steps.build-time.outputs.time }} + - name: build-worker + uses: docker/build-push-action@v5 + with: + context: . + file: ./worker/Dockerfile + push: false + tags: 015-worker:test \ No newline at end of file diff --git a/.gitea/workflows/lint.yaml b/.gitea/workflows/lint.yaml new file mode 100644 index 0000000..889c9a0 --- /dev/null +++ b/.gitea/workflows/lint.yaml @@ -0,0 +1,47 @@ +name: Lint + +on: + push: + +jobs: + lint-front: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: '24' + - uses: pnpm/action-setup@v4 + with: + cache: true + - name: Install dependencies + run: | + pnpm install --frozen-lockfile + - name: Run frontend lint + run: pnpm lint:front + + lint-backend: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-go@v6 + with: + go-version-file: 'go.work' + - name: Run golangci-lint + uses: golangci/golangci-lint-action@v9 + with: + skip-cache: true + working-directory: backend + + lint-worker: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-go@v6 + with: + go-version-file: 'go.work' + - name: Run golangci-lint + uses: golangci/golangci-lint-action@v9 + with: + skip-cache: true + working-directory: worker diff --git a/.gitea/workflows/publish.yaml b/.gitea/workflows/publish.yaml new file mode 100644 index 0000000..3fe33ee --- /dev/null +++ b/.gitea/workflows/publish.yaml @@ -0,0 +1,52 @@ +name: Publish + +on: + push: + tags: + - '*' + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: docker/setup-buildx-action@v3 + - uses: docker/login-action@v3 + with: + username: fudaoyuanicu + password: ${{ secrets.DOCKER_PASSWORD }} + - uses: docker/metadata-action@v5 + with: + images: fudaoyuanicu/015-app + tags: | + type=ref,event=tag + type=raw,value=latest + - name: Set build time + id: build-time + run: | + echo "time=$(date +%s)" >> $GITHUB_OUTPUT + shell: bash + - name: build-app + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: | + VERSION=${{ gitea.ref_name || github.ref_name }} + BUILD_TIME=${{ steps.build-time.outputs.time }} + - name: build-worker + uses: docker/build-push-action@v5 + with: + context: . + file: ./worker/Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + - name: Send deployment webhook + run: | + curl -X POST http://192.168.100.5:8364/v1/update \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer helloworld" diff --git a/.gitignore b/.gitignore index 3f813a5..f4907fb 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ logs .env .env.* !.env.example +config.yaml # Serwist /front/public/sw* diff --git a/Dockerfile b/Dockerfile index 2ff7db6..e3a8433 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ FROM front-base AS front-deps RUN apk add --no-cache gcompat WORKDIR /app COPY . . -RUN corepack enable pnpm && pnpm i && pnpm --filter=015-front deploy dist +RUN corepack enable pnpm && pnpm i && pnpm --filter=015-front deploy dist --legacy FROM front-base AS front-builder @@ -13,16 +13,17 @@ WORKDIR /app COPY --from=front-deps /app/dist/ . RUN corepack enable pnpm && pnpm build -FROM golang:1.24.3 AS backend-builder +FROM golang:1.25.5 AS backend-builder WORKDIR /app -# Download Go modules -COPY backend/go.mod backend/go.sum ./ -RUN go env -w GO111MODULE=on && go env -w GOPROXY=https://goproxy.cn,direct && go mod download -# Copy the source code. Note the slash at the end, as explained in -# https://docs.docker.com/engine/reference/builder/#copy -COPY backend/ . -# Build -RUN CGO_ENABLED=0 GOOS=linux go build -o backend +# Workspace and module manifests for cache +COPY go.work go.work.sum ./ +COPY backend/ ./backend/ +COPY worker/ ./worker/ +COPY pkg/ ./pkg/ +RUN go env -w GO111MODULE=on && go env -w GOPROXY=https://goproxy.cn,direct && \ + go mod download +# Build from workspace root so pkg/utils, pkg/models, pkg/services resolve +RUN CGO_ENABLED=0 GOOS=linux go build -o backend ./backend FROM front-base AS runner diff --git a/README-zh.md b/README-zh.md index 28951f2..2ef09cd 100644 --- a/README-zh.md +++ b/README-zh.md @@ -70,6 +70,23 @@ docker compose up -d ``` +## 🚀 快速开始 + +### Docker + +1. 下载文件 + - config.example.yaml + - docker-compose.yml + +2. 把config.example.yaml配置完成后改为config.yaml + + +3. 启动 +```bash +docker compose up -d +``` + + ## 🏗️ 技术架构 ### 前端技术栈 diff --git a/backend/Dockerfile b/backend/Dockerfile index 52b549e..be0aaee 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1 -FROM golang:1.24.3 AS builder +FROM golang:1.25.5 AS builder # Set destination for COPY WORKDIR /app diff --git a/backend/go.mod b/backend/go.mod index 6239f18..5e2fc84 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -1,56 +1,36 @@ module backend -go 1.24.0 - -toolchain go1.24.3 +go 1.25.5 require ( - dario.cat/mergo v1.0.2 - github.com/dustin/go-humanize v1.0.1 - github.com/golang-jwt/jwt/v5 v5.3.0 + github.com/golang-jwt/jwt/v5 v5.3.1 github.com/gorilla/sessions v1.4.0 github.com/hibiken/asynq v0.25.1 - github.com/labstack/echo-contrib v0.17.4 - github.com/labstack/echo/v4 v4.13.4 + github.com/labstack/echo-contrib v0.50.0 + github.com/labstack/echo/v5 v5.0.1 github.com/matoous/go-nanoid/v2 v2.1.0 - github.com/redis/go-redis/v9 v9.14.0 - github.com/samber/lo v1.51.0 + github.com/samber/lo v1.52.0 github.com/spf13/cast v1.10.0 - github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 - go.uber.org/zap v1.27.0 - golang.org/x/crypto v0.42.0 - golang.org/x/time v0.13.0 + go.uber.org/zap v1.27.1 + golang.org/x/crypto v0.47.0 ) require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/go-viper/mapstructure/v2 v2.4.0 // indirect - github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/context v1.1.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect - github.com/labstack/gommon v0.4.2 // indirect - github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/redis/go-redis/v9 v9.17.3 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect - github.com/sagikazarmark/locafero v0.12.0 // indirect - github.com/spf13/afero v1.15.0 // indirect - github.com/spf13/pflag v1.0.10 // indirect - github.com/subosito/gotenv v1.6.0 // indirect - github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasttemplate v1.2.2 // indirect go.uber.org/multierr v1.11.0 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/net v0.44.0 // indirect - golang.org/x/sys v0.36.0 // indirect - golang.org/x/text v0.29.0 // indirect - google.golang.org/protobuf v1.36.9 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/text v0.33.0 // indirect + golang.org/x/time v0.14.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/backend/go.sum b/backend/go.sum index 59ab6ab..da92968 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,5 +1,3 @@ -dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= -dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= @@ -10,16 +8,10 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= -github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= @@ -38,69 +30,44 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/labstack/echo-contrib v0.17.4 h1:g5mfsrJfJTKv+F5uNKCyrjLK7js+ZW6HTjg4FnDxxgk= -github.com/labstack/echo-contrib v0.17.4/go.mod h1:9O7ZPAHUeMGTOAfg80YqQduHzt0CzLak36PZRldYrZ0= -github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA= -github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ= -github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= -github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/labstack/echo-contrib v0.50.0 h1:MLTQdqME3BEBczV2thYz9yPT5sBhzkoUEpwAOY9llds= +github.com/labstack/echo-contrib v0.50.0/go.mod h1:oftqJL4enNg9ao1VLpVZmisVE5/8uwHtIYE4zTpqyWU= +github.com/labstack/echo/v5 v5.0.1 h1:60L7x1KMWRIJuaFqvnEHH322g+YnsMWq5Rzaeo6lcP4= +github.com/labstack/echo/v5 v5.0.1/go.mod h1:SyvlSdObGjRXeQfCCXW/sybkZdOOQZBmpKF0bvALaeo= github.com/matoous/go-nanoid/v2 v2.1.0 h1:P64+dmq21hhWdtvZfEAofnvJULaRR1Yib0+PnU669bE= github.com/matoous/go-nanoid/v2 v2.1.0/go.mod h1:KlbGNQ+FhrUNIHUxZdL63t7tl4LaPkZNpUULS8H4uVM= -github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= -github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/redis/go-redis/v9 v9.14.0 h1:u4tNCjXOyzfgeLN+vAZaW1xUooqWDqVEsZN0U01jfAE= -github.com/redis/go-redis/v9 v9.14.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= +github.com/redis/go-redis/v9 v9.17.3 h1:fN29NdNrE17KttK5Ndf20buqfDZwGNgoUr9qjl1DQx4= +github.com/redis/go-redis/v9 v9.17.3/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= -github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= -github.com/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI= -github.com/samber/lo v1.51.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= -github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= -github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw= +github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= -github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= -github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= -github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= -github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= -go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= -golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= -golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= -golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= -golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= -golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= -google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= -google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/backend/internal/controllers/about.go b/backend/internal/controllers/about.go index a4ae534..a0ccb0b 100644 --- a/backend/internal/controllers/about.go +++ b/backend/internal/controllers/about.go @@ -1,16 +1,17 @@ package controllers import ( - "backend/internal/models" "backend/internal/utils" "encoding/json" + "pkg/models" + u "pkg/utils" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" "github.com/samber/lo" ) -func GetAbout(c echo.Context) error { - maxStorageSize, err := utils.GetFileSize(utils.GetEnv("upload.maximum")) +func GetAbout(c *echo.Context) error { + maxStorageSize, err := u.GetFileSize(u.GetEnv("upload.maximum")) if err != nil { return utils.HTTPErrorHandler(c, err) } @@ -30,12 +31,12 @@ func GetAbout(c echo.Context) error { }, 0) return utils.HTTPSuccessHandler(c, map[string]any{ - "bg_url": utils.GetEnv("about.bg_url"), - "content": utils.GetEnvMapString("about.content"), - "email": utils.GetEnv("about.email"), - "name": utils.GetEnv("about.name"), - "url": utils.GetEnv("about.url"), - "avatar": utils.GetEnv("about.avatar"), + "bg_url": u.GetEnv("about.bg_url"), + "content": u.GetEnvMapString("about.content"), + "email": u.GetEnv("about.email"), + "name": u.GetEnv("about.name"), + "url": u.GetEnv("about.url"), + "avatar": u.GetEnv("about.avatar"), "file": map[string]any{ "maximun": maxStorageSize, "current": currentFileSize, diff --git a/backend/internal/controllers/config.go b/backend/internal/controllers/config.go index 97cc1cf..d4dda68 100644 --- a/backend/internal/controllers/config.go +++ b/backend/internal/controllers/config.go @@ -2,20 +2,21 @@ package controllers import ( "backend/internal/utils" + u "pkg/utils" "time" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" "github.com/spf13/cast" ) -func GetConfig(c echo.Context) error { +func GetConfig(c *echo.Context) error { return utils.HTTPSuccessHandler(c, map[string]any{ - "site_title": utils.GetEnvMapString("site.title"), - "site_desc": utils.GetEnvMapString("site.desc"), - "site_url": utils.GetEnv("site.url"), - "site_icon": utils.GetEnvWithDefault("site.icon", "/logo.png"), - "site_bg_url": utils.GetEnvWithDefault("site.bg_url", "https://img.fudaoyuan.icu/api/1/random/?scale_min=1.5&webp=true&md=false&format=302"), - "version": utils.GetEnvWithDefault("VERSION", "dev"), - "build_time": cast.ToInt(utils.GetEnvWithDefault("BUILD_TIME", cast.ToString(time.Now().Unix()))), + "site_title": u.GetEnvMapString("site.title"), + "site_desc": u.GetEnvMapString("site.desc"), + "site_url": u.GetEnv("site.url"), + "site_icon": u.GetEnvWithDefault("site.icon", "/logo.png"), + "site_bg_url": u.GetEnvWithDefault("site.bg_url", "https://img.fudaoyuan.icu/api/1/random/?scale_min=1.5&webp=true&md=false&format=302"), + "version": u.GetEnvWithDefault("VERSION", "dev"), + "build_time": cast.ToInt(u.GetEnvWithDefault("BUILD_TIME", cast.ToString(time.Now().Unix()))), }) } diff --git a/backend/internal/controllers/download.go b/backend/internal/controllers/download.go index 4d42dbc..864f936 100644 --- a/backend/internal/controllers/download.go +++ b/backend/internal/controllers/download.go @@ -1,15 +1,14 @@ package controllers import ( - "backend/internal/models" "backend/internal/utils" - "backend/middleware" - "errors" "fmt" + "pkg/models" + u "pkg/utils" "time" "github.com/golang-jwt/jwt/v5" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" "github.com/spf13/cast" ) @@ -18,31 +17,30 @@ type DownloadShareClaims struct { jwt.RegisteredClaims } -func DownloadShare(c echo.Context) error { - cc := c.(*middleware.CustomContext) - token := cc.FormValue("token") +func DownloadShare(c *echo.Context) error { + token := c.FormValue("token") if token == "" { - return utils.HTTPErrorHandler(c, errors.New("缺少token")) + return utils.HTTPErrorHandler(c, ErrInvalidRequest) } claims := DownloadShareClaims{} t, err := jwt.ParseWithClaims(token, &claims, func(token *jwt.Token) (interface{}, error) { - return []byte(utils.GetEnv("share.download_secret")), nil + return []byte(u.GetEnv("share.download_secret")), nil }) if err != nil { return utils.HTTPErrorHandler(c, err) } if !t.Valid { - return utils.HTTPErrorHandler(c, errors.New("token格式错误")) + return utils.HTTPErrorHandler(c, ErrInvalidRequest) } shareInfo, _ := models.GetRedisShareInfo(claims.ShareId) if shareInfo.Type == models.ShareTypeFile { fileInfo, _ := models.GetRedisFileInfo(shareInfo.Data) - uploadPath, err := utils.GetUploadDirPath() + uploadPath, err := u.GetUploadDirPath() if err != nil { return err } - return cc.Attachment(fmt.Sprintf("%s/%s", uploadPath, utils.GetFileId(fileInfo.FileHash, fileInfo.FileSize)), shareInfo.FileName) + return c.Attachment(fmt.Sprintf("%s/%s", uploadPath, u.GetFileId(fileInfo.FileHash, fileInfo.FileSize)), shareInfo.FileName) } return utils.HTTPSuccessHandler(c, map[string]any{ "data": shareInfo.Data, @@ -54,16 +52,14 @@ type VaildateShareProps struct { Password string `json:"password"` } -func VaildateShare(c echo.Context) error { - cc := c.(*middleware.CustomContext) - +func VaildateShare(c *echo.Context) error { r := new(VaildateShareProps) - if err := cc.Bind(r); err != nil { + if err := c.Bind(r); err != nil { return utils.HTTPErrorHandler(c, err) } if r.ShareId == "" { - return utils.HTTPErrorHandler(c, errors.New("缺少分享ID")) + return utils.HTTPErrorHandler(c, ErrInvalidRequest) } shareInfo, err := models.GetRedisShareInfo(r.ShareId) @@ -71,25 +67,25 @@ func VaildateShare(c echo.Context) error { return utils.HTTPErrorHandler(c, err) } if shareInfo == nil { - return utils.HTTPErrorHandler(c, errors.New("分享不存在")) + return utils.HTTPErrorHandler(c, ErrShareNotFound) } if shareInfo.Password != "" { if r.Password == "" { - return utils.HTTPErrorHandler(c, errors.New("缺少分享密码")) + return utils.HTTPErrorHandler(c, ErrInvalidRequest) } hash, err := utils.GeneratePasswordHash(r.Password) if err != nil { return utils.HTTPErrorHandler(c, err) } if hash != shareInfo.Password { - return utils.HTTPErrorHandler(c, errors.New("分享密码错误")) + return utils.HTTPErrorHandler(c, ErrInvalidSharePassword) } } // 如果下载次数为0,则设置为-1 防止空值问题 if shareInfo.ViewNum < 1 { - return utils.HTTPErrorHandler(c, errors.New("下载次数不足")) + return utils.HTTPErrorHandler(c, ErrInsufficientDownloadQuota) } - downloadWindow := utils.GetEnvWithDefault("share.download_window", "12") + downloadWindow := u.GetEnvWithDefault("share.download_window", "12") token := jwt.NewWithClaims(jwt.SigningMethodHS256, DownloadShareClaims{ ShareId: r.ShareId, RegisteredClaims: jwt.RegisteredClaims{ @@ -98,7 +94,7 @@ func VaildateShare(c echo.Context) error { }) // Sign and get the complete encoded token as a string using the secret - downloadToken, err := token.SignedString([]byte(utils.GetEnv("share.download_secret"))) + downloadToken, err := token.SignedString([]byte(u.GetEnv("share.download_secret"))) if err != nil { return utils.HTTPErrorHandler(c, err) } @@ -108,10 +104,10 @@ func VaildateShare(c echo.Context) error { return utils.HTTPErrorHandler(c, err) } if fileInfo == nil { - return utils.HTTPErrorHandler(c, errors.New("分享文件不存在")) + return utils.HTTPErrorHandler(c, ErrShareFileNotFound) } if fileInfo.FileType != models.FileTypeUpload { - return utils.HTTPErrorHandler(c, errors.New("分享文件状态错误")) + return utils.HTTPErrorHandler(c, ErrInvalidShareFileState) } } // download_nums 必须放在创建token的时候减掉,不然多线程下载会导致多次减掉 @@ -120,9 +116,12 @@ func VaildateShare(c echo.Context) error { if latestViewNum < 1 { latestViewNum = -1 } - models.SetRedisShareInfo(r.ShareId, models.RedisShareInfo{ + err = models.SetRedisShareInfo(r.ShareId, models.RedisShareInfo{ ViewNum: latestViewNum, }) + if err != nil { + return utils.HTTPErrorHandler(c, err) + } // 统计分享数 currentDate := time.Now().Format("2006-01-02") @@ -136,7 +135,10 @@ func VaildateShare(c echo.Context) error { } } statData.DownloadNum += 1 - models.SetRedisStat(currentDate, *statData) + err = models.SetRedisStat(currentDate, *statData) + if err != nil { + return utils.HTTPErrorHandler(c, err) + } if shareInfo.Type == models.ShareTypeFile { return utils.HTTPSuccessHandler(c, map[string]any{ diff --git a/backend/internal/controllers/errors.go b/backend/internal/controllers/errors.go new file mode 100644 index 0000000..bd290dc --- /dev/null +++ b/backend/internal/controllers/errors.go @@ -0,0 +1,30 @@ +package controllers + +import "errors" + +var ( + // 通用错误(参数校验失败) + ErrInvalidRequest = errors.New("InvalidRequest") // 调用接口参数错误 + + // 任务相关 + ErrTaskNotFound = errors.New("TaskNotFound") // 任务不存在 + ErrTaskExpired = errors.New("TaskExpired") // 任务已过期 + + // 文件上传相关 + ErrInsufficientStorage = errors.New("InsufficientStorage") // 存储空间不足 + ErrUploadTaskExpired = errors.New("UploadTaskExpired") // 上传任务已过期 + ErrInvalidUploadTaskState = errors.New("InvalidUploadTaskState") // 上传任务状态错误 + ErrInvalidFileSliceIndex = errors.New("InvalidFileSliceIndex") // 文件切片索引错误 + ErrInvalidFileSliceSize = errors.New("InvalidFileSliceSize") // 文件切片大小错误 + ErrIncompleteFileSlices = errors.New("IncompleteFileSlices") // 文件切片不完整 + ErrFileMD5Mismatch = errors.New("FileMD5Mismatch") // 文件MD5不一致 + + // 分享相关 + ErrShareFileNotFound = errors.New("ShareFileNotFound") // 分享文件不存在 + ErrInvalidShareFileState = errors.New("InvalidShareFileState") // 分享文件状态错误 + ErrShareNotFound = errors.New("ShareNotFound") // 分享不存在 + + // 下载相关 + ErrInvalidSharePassword = errors.New("InvalidSharePassword") // 分享密码错误 + ErrInsufficientDownloadQuota = errors.New("InsufficientDownloadQuota") // 下载次数不足 +) diff --git a/backend/internal/controllers/file.go b/backend/internal/controllers/file.go index 2de837b..b911168 100644 --- a/backend/internal/controllers/file.go +++ b/backend/internal/controllers/file.go @@ -1,22 +1,22 @@ package controllers import ( - "backend/internal/models" "backend/internal/services" "backend/internal/utils" "encoding/json" - "errors" - "fmt" + "math" "mime/multipart" "os" - "path/filepath" + "pkg/models" + s "pkg/services" + u "pkg/utils" "time" - "github.com/hibiken/asynq" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" + "github.com/spf13/cast" ) -func CreateUploadTask(c echo.Context) error { +func CreateUploadTask(c *echo.Context) error { // cc := c.(*middleware.CustomContext) r := new(models.FileInfo) if err := c.Bind(r); err != nil { @@ -24,12 +24,23 @@ func CreateUploadTask(c echo.Context) error { } if r.FileSize == 0 || r.MimeType == "" || r.FileHash == "" { - return utils.HTTPErrorHandler(c, errors.New("调用接口参数错误")) + return utils.HTTPErrorHandler(c, ErrInvalidRequest) + } + fileId := u.GetFileId(r.FileHash, r.FileSize) + fileInfo, err := models.GetRedisFileInfo(fileId) + if err != nil { + return utils.HTTPErrorHandler(c, err) } - fileId := utils.GetFileId(r.FileHash, r.FileSize) - fileInfo, _ := models.GetRedisFileInfo(fileId) if fileInfo != nil { + uploadPath, err := u.GetUploadDirPath() + if err != nil { + return utils.HTTPErrorHandler(c, err) + } + sliceList, err := services.GetFileSliceList(fileId, uploadPath) + if err != nil { + return utils.HTTPErrorHandler(c, err) + } return utils.HTTPSuccessHandler(c, map[string]any{ "size": fileInfo.FileSize, "mime_type": fileInfo.MimeType, @@ -38,9 +49,10 @@ func CreateUploadTask(c echo.Context) error { "expire": fileInfo.Expire, "id": fileId, "chunk_size": fileInfo.ChunkSize, + "chunks": sliceList, }) } - maxStorageSize, err := utils.GetFileSize(utils.GetEnv("upload.maximum")) + maxStorageSize, err := u.GetFileSize(u.GetEnv("upload.maximum")) if err != nil { return utils.HTTPErrorHandler(c, err) } @@ -58,7 +70,7 @@ func CreateUploadTask(c echo.Context) error { totalSize += fileInfo.FileSize } if totalSize+r.FileSize > int64(maxStorageSize) { - return utils.HTTPErrorHandler(c, errors.New("存储空间不足")) + return utils.HTTPErrorHandler(c, ErrInsufficientStorage) } ChunkSize := int64(0.25 * 1024 * 1024) @@ -66,7 +78,7 @@ func CreateUploadTask(c echo.Context) error { for r.FileSize/ChunkSize > 1000 { ChunkSize *= 2 } - uploadTaskExpire := int64(3600) + uploadTaskExpire := cast.ToInt64(u.GetEnvWithDefault("upload.remove_expire", "2")) * 3600 newFileInfo := models.RedisFileInfo{ FileType: models.FileTypeInit, FileInfo: models.FileInfo{ @@ -83,14 +95,7 @@ func CreateUploadTask(c echo.Context) error { return utils.HTTPErrorHandler(c, err) } - client := utils.GetQueueClient() - json, err := json.Marshal(map[string]any{ - "file_id": fileId, - }) - if err != nil { - return utils.HTTPErrorHandler(c, err) - } - _, err = client.Enqueue(asynq.NewTask("file:remove", json), asynq.ProcessIn(time.Duration(uploadTaskExpire)*time.Second)) + err = s.SetFileRemoveTask(fileId, time.Duration(uploadTaskExpire)*time.Second) if err != nil { return utils.HTTPErrorHandler(c, err) } @@ -112,14 +117,14 @@ type UploadFileSliceProps struct { FileSlice *multipart.FileHeader `form:"file"` } -func UploadFileSlice(c echo.Context) error { +func UploadFileSlice(c *echo.Context) error { r := new(UploadFileSliceProps) if err := c.Bind(r); err != nil { return utils.HTTPErrorHandler(c, err) } if r.FileId == "" || r.FileIndex == 0 || r.FileSlice == nil { - return utils.HTTPErrorHandler(c, errors.New("调用接口参数错误")) + return utils.HTTPErrorHandler(c, ErrInvalidRequest) } fileInfo, err := models.GetRedisFileInfo(r.FileId) if err != nil { @@ -128,18 +133,18 @@ func UploadFileSlice(c echo.Context) error { now := time.Now().Unix() if fileInfo.CreatedAt+fileInfo.Expire < now { - return utils.HTTPErrorHandler(c, errors.New("上传任务已过期")) + return utils.HTTPErrorHandler(c, ErrUploadTaskExpired) } if fileInfo.FileType != models.FileTypeInit { - return utils.HTTPErrorHandler(c, errors.New("上传任务状态错误")) + return utils.HTTPErrorHandler(c, ErrInvalidUploadTaskState) } if r.FileIndex > ((fileInfo.FileSize / fileInfo.ChunkSize) + 1) { - return utils.HTTPErrorHandler(c, errors.New("文件切片索引错误")) + return utils.HTTPErrorHandler(c, ErrInvalidFileSliceIndex) } if r.FileSlice.Size > fileInfo.ChunkSize { - return utils.HTTPErrorHandler(c, errors.New("文件切片大小错误")) + return utils.HTTPErrorHandler(c, ErrInvalidFileSliceSize) } // 打开文件 @@ -147,9 +152,14 @@ func UploadFileSlice(c echo.Context) error { if err != nil { return utils.HTTPErrorHandler(c, err) } - defer file.Close() + defer file.Close() //nolint:errcheck - if err := services.CreateFileSlice(file, r.FileId, r.FileIndex); err != nil { + uploadPath, err := u.GetUploadDirPath() + if err != nil { + return utils.HTTPErrorHandler(c, err) + } + + if _, err := services.CreateFileSlice(r.FileId, uploadPath, file, r.FileIndex); err != nil { return utils.HTTPErrorHandler(c, err) } @@ -162,14 +172,14 @@ type FinishUploadTaskProps struct { FileId string `json:"id"` } -func FinishUploadTask(c echo.Context) error { +func FinishUploadTask(c *echo.Context) error { r := new(FinishUploadTaskProps) if err := c.Bind(r); err != nil { return utils.HTTPErrorHandler(c, err) } if r.FileId == "" { - return utils.HTTPErrorHandler(c, errors.New("文件ID不能为空")) + return utils.HTTPErrorHandler(c, ErrInvalidRequest) } fileInfo, err := models.GetRedisFileInfo(r.FileId) @@ -178,50 +188,57 @@ func FinishUploadTask(c echo.Context) error { } if fileInfo.FileType != models.FileTypeInit { - return utils.HTTPErrorHandler(c, errors.New("上传任务状态错误")) + return utils.HTTPErrorHandler(c, ErrInvalidUploadTaskState) } now := time.Now().Unix() if fileInfo.CreatedAt+fileInfo.Expire < now { - return utils.HTTPErrorHandler(c, errors.New("上传任务已过期")) + return utils.HTTPErrorHandler(c, ErrUploadTaskExpired) } - // 合并文件切片 - uploadPath, _ := utils.GetUploadDirPath() - slicesPath := filepath.Join(uploadPath, fmt.Sprintf("%s_%s", r.FileId, "tmp")) + uploadPath, err := u.GetUploadDirPath() + if err != nil { + return utils.HTTPErrorHandler(c, err) + } + + fileSliceList, err := services.GetFileSliceList(r.FileId, uploadPath) + if err != nil { + return utils.HTTPErrorHandler(c, err) + } + + if len(fileSliceList) != int(math.Ceil(float64(fileInfo.FileSize)/float64(fileInfo.ChunkSize))) { + return utils.HTTPErrorHandler(c, ErrIncompleteFileSlices) + } // 最终合并后的文件路径 - mergeFilePath := filepath.Join(uploadPath, r.FileId) - if err := services.MergeFileSlices(slicesPath, mergeFilePath); err != nil { + mergeFilePath, err := services.MergeFileSlices(r.FileId, uploadPath) + if err != nil { return utils.HTTPErrorHandler(c, err) } // 计算文件MD5 file, err := os.Open(mergeFilePath) if err != nil { - file.Close() - os.Remove(mergeFilePath) + return utils.HTTPErrorHandler(c, err) + } + defer file.Close() //nolint:errcheck + + file_hash, err := u.GetFileMd5(file) + if err != nil || file_hash != fileInfo.FileHash { + defer os.Remove(mergeFilePath) //nolint:errcheck + if err == nil { + return utils.HTTPErrorHandler(c, ErrFileMD5Mismatch) + } return utils.HTTPErrorHandler(c, err) } - file_hash, err := utils.GetFileMd5(file) - - if err != nil { - file.Close() - os.Remove(mergeFilePath) - return utils.HTTPErrorHandler(c, err) - } - - if file_hash != fileInfo.FileHash { - file.Close() - os.Remove(mergeFilePath) - return utils.HTTPErrorHandler(c, errors.New("文件MD5不一致")) - } - defer file.Close() // 更新文件信息 - models.SetRedisFileInfo(r.FileId, models.RedisFileInfo{ + err = models.SetRedisFileInfo(r.FileId, models.RedisFileInfo{ FileType: models.FileTypeUpload, }) + if err != nil { + return utils.HTTPErrorHandler(c, err) + } // 统计 currentDate := time.Now().Format("2006-01-02") statData, _ := models.GetRedisStat(currentDate) @@ -235,7 +252,10 @@ func FinishUploadTask(c echo.Context) error { } statData.FileSize += fileInfo.FileSize statData.FileNum += 1 - models.SetRedisStat(currentDate, *statData) + err = models.SetRedisStat(currentDate, *statData) + if err != nil { + return utils.HTTPErrorHandler(c, err) + } return utils.HTTPSuccessHandler(c, map[string]any{ "size": fileInfo.FileSize, diff --git a/backend/internal/controllers/share.go b/backend/internal/controllers/share.go index 1cab032..ab89a97 100644 --- a/backend/internal/controllers/share.go +++ b/backend/internal/controllers/share.go @@ -1,16 +1,15 @@ package controllers import ( - "backend/internal/models" "backend/internal/utils" - "backend/middleware" "encoding/json" - "errors" + "pkg/models" + u "pkg/utils" "strings" "time" "github.com/hibiken/asynq" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" gonanoid "github.com/matoous/go-nanoid/v2" "github.com/spf13/cast" ) @@ -33,19 +32,19 @@ type ShareConfig struct { HasPickupCode bool `json:"has_pickup_code"` } -func CreateShareInfo(c echo.Context) error { - cc := c.(*middleware.CustomContext) +func CreateShareInfo(c *echo.Context) error { + owner, _ := echo.ContextGet[string](c, "auth") r := new(CreateShareProps) - if err := cc.Bind(r); err != nil { + if err := c.Bind(r); err != nil { return utils.HTTPErrorHandler(c, err) } if r.Config.ExpireAt < 1 { - return utils.HTTPErrorHandler(c, errors.New("非法的分享过期时间")) + return utils.HTTPErrorHandler(c, ErrInvalidRequest) } ExpireTime := time.Now().Add(time.Duration(r.Config.ExpireAt) * time.Minute) if r.Data == "" || (r.Type != models.ShareTypeFile && r.Type != models.ShareTypeText) || ExpireTime.Before(time.Now()) || r.Config.ViewNum < 1 { - return utils.HTTPErrorHandler(c, errors.New("调用接口参数错误")) + return utils.HTTPErrorHandler(c, ErrInvalidRequest) } id, err := gonanoid.New() @@ -59,10 +58,10 @@ func CreateShareInfo(c echo.Context) error { return utils.HTTPErrorHandler(c, err) } if fileInfo == nil { - return utils.HTTPErrorHandler(c, errors.New("分享文件不存在")) + return utils.HTTPErrorHandler(c, ErrShareFileNotFound) } if fileInfo.FileType != models.FileTypeUpload { - return utils.HTTPErrorHandler(c, errors.New("分享文件状态错误")) + return utils.HTTPErrorHandler(c, ErrInvalidShareFileState) } } password := "" @@ -74,17 +73,20 @@ func CreateShareInfo(c echo.Context) error { password = hash } - models.SetRedisShareInfo(id, models.RedisShareInfo{ + err = models.SetRedisShareInfo(id, models.RedisShareInfo{ Data: r.Data, Type: r.Type, CreatedAt: time.Now().Unix(), - Owner: cc.Auth.(string), + Owner: owner, ViewNum: r.Config.ViewNum, Password: password, // NotifyEmail: r.Config.NotifyEmail, FileName: r.FileName, ExpireAt: ExpireTime.Unix(), }) + if err != nil { + return utils.HTTPErrorHandler(c, err) + } var pickupCode string if r.Config.HasPickupCode { for { @@ -106,14 +108,17 @@ func CreateShareInfo(c echo.Context) error { return utils.HTTPErrorHandler(c, err) } shareIDs = append(shareIDs, id) - models.SetRedisFileShareRelational(r.Data, shareIDs) - client := utils.GetQueueClient() + err = models.SetRedisFileShareRelational(r.Data, shareIDs) + if err != nil { + return utils.HTTPErrorHandler(c, err) + } + client := u.GetQueueClient() json, err := json.Marshal(map[string]any{"share_id": id, "file_id": r.Data}) if err != nil { return utils.HTTPErrorHandler(c, err) } // 这里延时分享过期时间基础上加下载窗口期后1小时删除,防止用户过期前几分钟才开始下载,下载一半文件不见了 - downloadWindow := utils.GetEnvWithDefault("share.download_window", "12") + downloadWindow := u.GetEnvWithDefault("share.download_window", "12") deleteTime := time.Duration(r.Config.ExpireAt)*time.Minute + cast.ToDuration(downloadWindow+"h") + 1*time.Hour _, err = client.Enqueue(asynq.NewTask("share:remove", json), asynq.ProcessIn(deleteTime)) if err != nil { @@ -133,7 +138,10 @@ func CreateShareInfo(c echo.Context) error { } } statData.ShareNum += 1 - models.SetRedisStat(currentDate, *statData) + err = models.SetRedisStat(currentDate, *statData) + if err != nil { + return utils.HTTPErrorHandler(c, err) + } return utils.HTTPSuccessHandler(c, map[string]any{ "id": id, @@ -148,11 +156,10 @@ type GetShareProps struct { ShareId string `param:"id"` } -func GetShareInfo(c echo.Context) error { - cc := c.(*middleware.CustomContext) - shareId := cc.Param("id") +func GetShareInfo(c *echo.Context) error { + shareId := c.Param("id") if shareId == "" { - return utils.HTTPErrorHandler(c, errors.New("缺少分享ID")) + return utils.HTTPErrorHandler(c, ErrInvalidRequest) } shareInfo, err := models.GetRedisShareInfo(shareId) @@ -160,7 +167,7 @@ func GetShareInfo(c echo.Context) error { return utils.HTTPErrorHandler(c, err) } if shareInfo == nil || shareInfo.ViewNum < 1 { - return utils.HTTPErrorHandler(c, errors.New("分享不存在")) + return utils.HTTPErrorHandler(c, ErrShareNotFound) } if shareInfo.Type == models.ShareTypeFile { @@ -169,10 +176,10 @@ func GetShareInfo(c echo.Context) error { return utils.HTTPErrorHandler(c, err) } if fileInfo == nil { - return utils.HTTPErrorHandler(c, errors.New("分享文件不存在")) + return utils.HTTPErrorHandler(c, ErrShareFileNotFound) } if fileInfo.FileType != models.FileTypeUpload { - return utils.HTTPErrorHandler(c, errors.New("分享文件状态错误")) + return utils.HTTPErrorHandler(c, ErrInvalidShareFileState) } return utils.HTTPSuccessHandler(c, map[string]any{ "id": shareId, @@ -198,18 +205,17 @@ func GetShareInfo(c echo.Context) error { }) } -func GetShareByPickupCode(c echo.Context) error { - cc := c.(*middleware.CustomContext) - pickupCode := cc.Param("code") +func GetShareByPickupCode(c *echo.Context) error { + pickupCode := c.Param("code") if pickupCode == "" { - return utils.HTTPErrorHandler(c, errors.New("缺少提取码")) + return utils.HTTPErrorHandler(c, ErrInvalidRequest) } shareId, err := models.GetRedisPickupData(strings.ToUpper(pickupCode)) if err != nil { return utils.HTTPErrorHandler(c, err) } if shareId == "" { - return utils.HTTPErrorHandler(c, errors.New("分享不存在")) + return utils.HTTPErrorHandler(c, ErrShareNotFound) } return utils.HTTPSuccessHandler(c, map[string]any{ "share_id": shareId, diff --git a/backend/internal/controllers/stat.go b/backend/internal/controllers/stat.go index 1bf34a5..236e31d 100644 --- a/backend/internal/controllers/stat.go +++ b/backend/internal/controllers/stat.go @@ -1,11 +1,12 @@ package controllers import ( - "backend/internal/models" "backend/internal/utils" "encoding/json" + "pkg/models" + u "pkg/utils" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) const ( @@ -26,7 +27,7 @@ type QueueChartData struct { Failed int `json:"failed"` } -func GetStat(c echo.Context) error { +func GetStat(c *echo.Context) error { statInfoMap, err := models.GetRedisStatAll() if err != nil { return utils.HTTPErrorHandler(c, err) @@ -47,7 +48,7 @@ func GetStat(c echo.Context) error { } } - queueInspector := utils.GetQueueInspector() + queueInspector := u.GetQueueInspector() queues, err := queueInspector.History("default", QueueHistoryDays) if err != nil { return utils.HTTPErrorHandler(c, err) diff --git a/backend/internal/controllers/image.go b/backend/internal/controllers/task.go similarity index 55% rename from backend/internal/controllers/image.go rename to backend/internal/controllers/task.go index 05a7435..4c3b17c 100644 --- a/backend/internal/controllers/image.go +++ b/backend/internal/controllers/task.go @@ -1,38 +1,36 @@ package controllers import ( - "backend/internal/models" + "backend/internal/controllers/task" "backend/internal/utils" - "backend/middleware" - "encoding/json" - "errors" + "pkg/models" + u "pkg/utils" "github.com/hibiken/asynq" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) -type GenCompressImageRequest struct { - FileId string `json:"file_id"` +var handleTaskMap = map[string]func(c *echo.Context) ([]byte, error){ + "image:compress": task.HandleImageCompress, + "image:convert": task.HandleImageConvert, } -func GenCompressImage(c echo.Context) error { - cc := c.(*middleware.CustomContext) - r := new(GenCompressImageRequest) - if err := cc.Bind(r); err != nil { - return utils.HTTPErrorHandler(c, err) +func CreateTask(c *echo.Context) error { + taskType := c.Param("type") + if taskType == "" { + return utils.HTTPErrorHandler(c, ErrInvalidRequest) } - - if r.FileId == "" { - return utils.HTTPErrorHandler(c, errors.New("调用接口参数错误")) + handleTask, ok := handleTaskMap[taskType] + if !ok { + return utils.HTTPErrorHandler(c, ErrTaskNotFound) } - client := utils.GetQueueClient() - json, err := json.Marshal(map[string]any{ - "file_id": r.FileId, - }) + json, err := handleTask(c) if err != nil { return utils.HTTPErrorHandler(c, err) } - info, err := client.Enqueue(asynq.NewTask("image:compress", json), asynq.MaxRetry(3)) + + client := u.GetQueueClient() + info, err := client.Enqueue(asynq.NewTask(taskType, json), asynq.MaxRetry(3)) if err != nil { return utils.HTTPErrorHandler(c, err) } @@ -42,11 +40,10 @@ func GenCompressImage(c echo.Context) error { }) } -func GetCompressImage(c echo.Context) error { - cc := c.(*middleware.CustomContext) - taskId := cc.Param("id") +func GetTask(c *echo.Context) error { + taskId := c.Param("id") if taskId == "" { - return utils.HTTPErrorHandler(c, errors.New("调用接口参数错误")) + return utils.HTTPErrorHandler(c, ErrInvalidRequest) } taskInfo, err := models.GetRedisTaskInfo(taskId) @@ -54,11 +51,11 @@ func GetCompressImage(c echo.Context) error { return utils.HTTPErrorHandler(c, err) } if taskInfo == nil { - client := utils.GetQueueInspector() + client := u.GetQueueInspector() queneTaskInfo, err := client.GetTaskInfo("default", taskId) if err != nil { - return utils.HTTPErrorHandler(c, errors.New("任务已过期")) + return utils.HTTPErrorHandler(c, ErrTaskExpired) } stateMap := map[asynq.TaskState]string{ asynq.TaskStateActive: "processing", diff --git a/backend/internal/controllers/task/errors.go b/backend/internal/controllers/task/errors.go new file mode 100644 index 0000000..759459f --- /dev/null +++ b/backend/internal/controllers/task/errors.go @@ -0,0 +1,7 @@ +package task + +import "errors" + +var ( + ErrInvalidRequest = errors.New("InvalidRequest") +) diff --git a/backend/internal/controllers/task/image.go b/backend/internal/controllers/task/image.go new file mode 100644 index 0000000..bee32f6 --- /dev/null +++ b/backend/internal/controllers/task/image.go @@ -0,0 +1,53 @@ +package task + +import ( + "encoding/json" + + "github.com/labstack/echo/v5" +) + +type BaseImageRequest struct { + FileId string `json:"file_id"` +} + +func HandleImageCompress(c *echo.Context) ([]byte, error) { + r := new(BaseImageRequest) + if err := c.Bind(r); err != nil { + return nil, err + } + if r.FileId == "" { + return nil, ErrInvalidRequest + } + json, err := json.Marshal(map[string]any{ + "file_id": r.FileId, + }) + if err != nil { + return nil, err + } + + return json, nil +} + +type ImageConvertRequest struct { + BaseImageRequest + TargetExt string `json:"target_ext"` +} + +func HandleImageConvert(c *echo.Context) ([]byte, error) { + r := new(ImageConvertRequest) + if err := c.Bind(r); err != nil { + return nil, err + } + if r.FileId == "" || r.TargetExt == "" { + return nil, ErrInvalidRequest + } + json, err := json.Marshal(map[string]any{ + "file_id": r.FileId, + "target_ext": r.TargetExt, + }) + if err != nil { + return nil, err + } + + return json, nil +} diff --git a/backend/internal/models/file_share_relational.go b/backend/internal/models/file_share_relational.go deleted file mode 100644 index 53c7b02..0000000 --- a/backend/internal/models/file_share_relational.go +++ /dev/null @@ -1,31 +0,0 @@ -package models - -import ( - "backend/internal/utils" - "encoding/json" - - "github.com/redis/go-redis/v9" -) - -func GetRedisFileShareRelational(fileId string) ([]string, error) { - rdb, ctx := utils.GetRedisClient() - fileShareRelationalUnmarshalData, err := rdb.HGet(ctx, "015:fileShareRelational", fileId).Result() - if err == redis.Nil { - return nil, nil - } - if err != nil { - return nil, err - } - var shareIDs []string - if err := json.Unmarshal([]byte(fileShareRelationalUnmarshalData), &shareIDs); err != nil { - return nil, err - } - return shareIDs, nil -} - -func SetRedisFileShareRelational(fileId string, shareIDs []string) error { - rdb, ctx := utils.GetRedisClient() - jsonData, _ := json.Marshal(shareIDs) - _, err := rdb.HSet(ctx, "015:fileShareRelational", fileId, string(jsonData)).Result() - return err -} diff --git a/backend/internal/models/task.go b/backend/internal/models/task.go deleted file mode 100644 index 26da36e..0000000 --- a/backend/internal/models/task.go +++ /dev/null @@ -1,35 +0,0 @@ -package models - -import ( - "backend/internal/utils" - "encoding/json" - "fmt" - "time" - - "github.com/redis/go-redis/v9" -) - -func GetRedisTaskInfo(taskId string) (*map[string]any, error) { - rdb, ctx := utils.GetRedisClient() - taskInfo := rdb.Get(ctx, fmt.Sprintf("015:taskInfoMap:%s", taskId)) - taskInfoUnmarshalData, err := taskInfo.Result() - if err == redis.Nil { - return nil, nil - } - if err != nil { - return nil, err - } - var taskInfoData map[string]any - - if err := json.Unmarshal([]byte(taskInfoUnmarshalData), &taskInfoData); err != nil { - return nil, err - } - return &taskInfoData, nil -} - -func SetRedisTaskInfo(taskId string, taskInfo map[string]any) error { - rdb, ctx := utils.GetRedisClient() - jsonData, _ := json.Marshal(taskInfo) - _, err := rdb.Set(ctx, fmt.Sprintf("015:taskInfoMap:%s", taskId), jsonData, time.Hour).Result() - return err -} diff --git a/backend/internal/services/file.go b/backend/internal/services/file.go index e142d5c..8ff5ad5 100644 --- a/backend/internal/services/file.go +++ b/backend/internal/services/file.go @@ -1,89 +1,89 @@ package services import ( - "backend/internal/utils" "fmt" "io" "os" "path/filepath" + "sort" "strconv" ) -func CreateFileSlice(fileSlice io.Reader, fileId string, fileIndex int64) error { - uploadPath, err := utils.GetUploadDirPath() - if err != nil { - return err - } - +func CreateFileSlice(fileId string, uploadPath string, fileSlice io.Reader, fileIndex int64) (string, error) { filePath := filepath.Join(uploadPath, fmt.Sprintf("%s_%s", fileId, "tmp")) if err := os.MkdirAll(filePath, 0755); err != nil { - return err + return "", err } dst, err := os.Create(filepath.Join(filePath, fmt.Sprintf("%d", fileIndex))) if err != nil { - return err + return "", err } - defer dst.Close() + defer dst.Close() //nolint:errcheck if _, err = io.Copy(dst, fileSlice); err != nil { - return err + return "", err } - return nil + return filePath, nil } -// MergeFileSlices 合并文件切片 -func MergeFileSlices(slicesPath string, mergeFilePath string) error { - // 创建最终文件 - destFile, err := os.Create(mergeFilePath) - if err != nil { - return fmt.Errorf("创建合并文件失败: %v", err) - } - defer destFile.Close() - - // 读取目录下的所有文件 +func GetFileSliceList(fileId string, uploadPath string) ([]int, error) { + slicesPath := filepath.Join(uploadPath, fmt.Sprintf("%s_%s", fileId, "tmp")) files, err := os.ReadDir(slicesPath) - if err != nil { - return fmt.Errorf("读取切片目录失败: %v", err) + if err != nil && !os.IsNotExist(err) { + return nil, fmt.Errorf("读取切片目录失败: %v", err) } - - // 按照索引排序文件切片 - sliceFiles := make([]string, len(files)) + fileSliceList := []int{} for _, file := range files { index, err := strconv.Atoi(file.Name()) if err != nil { - return fmt.Errorf("无效的切片文件名: %v", err) + return nil, fmt.Errorf("无效的切片文件名: %v", err) } - sliceFiles[index-1] = filepath.Join(slicesPath, file.Name()) + fileSliceList = append(fileSliceList, index) + } + sort.Ints(fileSliceList) + return fileSliceList, nil +} + +// MergeFileSlices 合并文件切片 +func MergeFileSlices(fileId string, uploadPath string) (string, error) { + mergeFilePath := filepath.Join(uploadPath, fileId) + slicesPath := filepath.Join(uploadPath, fmt.Sprintf("%s_%s", fileId, "tmp")) + defer os.RemoveAll(slicesPath) //nolint:errcheck + // 创建最终文件 + destFile, err := os.Create(mergeFilePath) + if err != nil { + return "", fmt.Errorf("创建合并文件失败: %v", err) + } + defer destFile.Close() //nolint:errcheck + + fileSliceList, err := GetFileSliceList(fileId, uploadPath) + if err != nil { + return "", err } // 合并文件 buffer := make([]byte, 4*1024*1024) // 4MB buffer - for _, sliceFile := range sliceFiles { - sf, err := os.Open(sliceFile) + for _, index := range fileSliceList { + sliceFilePath := filepath.Join(slicesPath, fmt.Sprintf("%d", index)) + sf, err := os.Open(sliceFilePath) if err != nil { - return fmt.Errorf("打开切片文件失败: %v", err) + return "", fmt.Errorf("打开切片文件失败: %v", err) } - + defer sf.Close() //nolint:errcheck for { n, err := sf.Read(buffer) if err == io.EOF { break } if err != nil { - sf.Close() - return fmt.Errorf("读取切片文件失败: %v", err) + return "", fmt.Errorf("读取切片文件失败: %v", err) } - if _, err := destFile.Write(buffer[:n]); err != nil { - sf.Close() - return fmt.Errorf("写入合并文件失败: %v", err) + return "", fmt.Errorf("写入合并文件失败: %v", err) } } - - sf.Close() } - defer os.RemoveAll(slicesPath) - return nil + return mergeFilePath, nil } diff --git a/backend/internal/utils/env.go b/backend/internal/utils/env.go deleted file mode 100644 index 0fba7c2..0000000 --- a/backend/internal/utils/env.go +++ /dev/null @@ -1,53 +0,0 @@ -package utils - -import ( - "strings" - - "github.com/spf13/viper" -) - -var v *viper.Viper - -func init() { - InitEnv() -} - -func InitEnv() { - if v != nil { - return - } - v = viper.New() - v.SetConfigName("config.yaml") - v.SetConfigType("yaml") - v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) - v.AddConfigPath(".") - v.AddConfigPath("../") - v.AutomaticEnv() - v.WatchConfig() - err := v.ReadInConfig() - if err != nil { - panic(err) - // if _, ok := err.(viper.ConfigFileNotFoundError); !ok { - // // 只有当错误不是"配置文件未找到"时才 panic - // panic(err) - // } - } -} - -func GetEnv(key string) string { - InitEnv() - return v.GetString(key) -} - -func GetEnvWithDefault(key string, defaultValue string) string { - value := v.GetString(key) - if value == "" { - return defaultValue - } - return value -} - -func GetEnvMapString(key string) map[string]string { - InitEnv() - return v.GetStringMapString(key) -} diff --git a/backend/internal/utils/file.go b/backend/internal/utils/file.go deleted file mode 100644 index 4b09b8f..0000000 --- a/backend/internal/utils/file.go +++ /dev/null @@ -1,53 +0,0 @@ -package utils - -import ( - "bufio" - "crypto/md5" - "fmt" - "io" - "os" - "path/filepath" - - humanize "github.com/dustin/go-humanize" -) - -func GetFileId(fileHash string, fileSize int64) string { - return fmt.Sprintf("%s_%d", fileHash, fileSize) -} - -func GetFileMd5(file io.Reader) (string, error) { - - const bufferSize = 1024 * 1000 // 1MB - - hash := md5.New() - for buf, reader := make([]byte, bufferSize), bufio.NewReader(file); ; { - n, err := reader.Read(buf) - if err != nil { - if err == io.EOF { - break - } - return "", err - } - - hash.Write(buf[:n]) - } - - return fmt.Sprintf("%x", hash.Sum(nil)), nil -} - -func GetUploadDirPath() (string, error) { - basepath, err := os.Getwd() - if err != nil { - return "", err - } - finalPath := filepath.Join(basepath, "uploads") - uploadPath := GetEnvWithDefault("upload.path", finalPath) - if err := os.MkdirAll(uploadPath, 0755); err != nil { - return "", err - } - return uploadPath, nil -} - -func GetFileSize(size string) (uint64, error) { - return humanize.ParseBytes(size) -} diff --git a/backend/internal/utils/http_result.go b/backend/internal/utils/http_result.go index b46f78b..accd1ae 100644 --- a/backend/internal/utils/http_result.go +++ b/backend/internal/utils/http_result.go @@ -3,7 +3,7 @@ package utils import ( "net/http" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) type Option interface { @@ -36,7 +36,7 @@ func (o WithData) applyTo(props *HTTPBaseResponse) { props.data = o } -func HTTPBaseHandler(c echo.Context, options ...Option) error { +func HTTPBaseHandler(c *echo.Context, options ...Option) error { props := HTTPBaseResponse{code: http.StatusOK, message: "success", data: map[string]any{}} for _, option := range options { option.applyTo(&props) @@ -49,10 +49,10 @@ func HTTPBaseHandler(c echo.Context, options ...Option) error { }) } -func HTTPSuccessHandler(c echo.Context, data map[string]any, options ...HTTPBaseResponseProps) error { +func HTTPSuccessHandler(c *echo.Context, data map[string]any, options ...HTTPBaseResponseProps) error { return HTTPBaseHandler(c, WithData(data)) } -func HTTPErrorHandler(c echo.Context, err error, options ...HTTPBaseResponseProps) error { +func HTTPErrorHandler(c *echo.Context, err error, options ...HTTPBaseResponseProps) error { return HTTPBaseHandler(c, WithMessage(err.Error()), WithCode(http.StatusBadRequest)) } diff --git a/backend/internal/utils/password.go b/backend/internal/utils/password.go index a503022..14e3974 100644 --- a/backend/internal/utils/password.go +++ b/backend/internal/utils/password.go @@ -3,14 +3,19 @@ package utils import ( "errors" "fmt" + "pkg/utils" "golang.org/x/crypto/argon2" ) +var ( + ErrPasswordSaltNotSet = errors.New("PasswordSaltNotSet") +) + func GeneratePasswordHash(password string) (string, error) { - salt := GetEnv("share.password_salt") + salt := utils.GetEnv("share.password_salt") if salt == "" { - return "", errors.New("请配置PASSWORD_SALT") + return "", ErrPasswordSaltNotSet } hash := argon2.IDKey([]byte(password), []byte(salt), 1, 64*1024, 4, 32) return fmt.Sprintf("%x", hash), nil diff --git a/backend/main.go b/backend/main.go index d57dfba..c74f786 100644 --- a/backend/main.go +++ b/backend/main.go @@ -1,12 +1,10 @@ package main import ( - "backend/internal/controllers" - "backend/internal/utils" - "backend/middleware" "fmt" + "pkg/utils" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" "go.uber.org/zap" ) @@ -18,30 +16,18 @@ func main() { } else { logger, _ = zap.NewDevelopment() } - defer logger.Sync() + defer logger.Sync() //nolint:errcheck zap.ReplaceGlobals(logger) e := echo.New() - e.Use(middleware.ContextMiddleware()) - e.Use(middleware.SessionMiddleware()) - e.Use(middleware.AuthMiddleware()) - e.Use(middleware.RateLimiterMiddleware()) - e.Use(middleware.LoggerMiddleware()) + for _, middleware := range middlewares { + e.Use(middleware()) + } - e.POST("/file/create", controllers.CreateUploadTask) - e.POST("/file/slice", controllers.UploadFileSlice) - e.POST("/file/finish", controllers.FinishUploadTask) - e.GET("/share/:id", controllers.GetShareInfo) - e.POST("/share", controllers.CreateShareInfo) - e.GET("/download", controllers.DownloadShare) - e.POST("/download", controllers.VaildateShare) - e.GET("/share/pickup/:code", controllers.GetShareByPickupCode) - - e.POST("/image/compress", controllers.GenCompressImage) - e.GET("/image/compress/:id", controllers.GetCompressImage) - - e.GET("/stat", controllers.GetStat) - e.GET("/config", controllers.GetConfig) - e.GET("/about", controllers.GetAbout) - e.Logger.Fatal(e.Start(fmt.Sprintf(":%s", utils.GetEnvWithDefault("api.port", "5001")))) + for _, route := range routes { + e.Match(route.Method, route.Path, route.Handler) + } + if err := e.Start(fmt.Sprintf(":%s", utils.GetEnvWithDefault("api.port", "5001"))); err != nil { + logger.Fatal("server failed", zap.Error(err)) + } } diff --git a/backend/middleware.go b/backend/middleware.go new file mode 100644 index 0000000..1bbd1ab --- /dev/null +++ b/backend/middleware.go @@ -0,0 +1,14 @@ +package main + +import ( + "backend/middleware" + + "github.com/labstack/echo/v5" +) + +var middlewares = []func() echo.MiddlewareFunc{ + middleware.SessionMiddleware, + middleware.AuthMiddleware, + middleware.RateLimiterMiddleware, + middleware.LoggerMiddleware, +} diff --git a/backend/middleware/auth.go b/backend/middleware/auth.go index a1a767a..b567e27 100644 --- a/backend/middleware/auth.go +++ b/backend/middleware/auth.go @@ -3,14 +3,14 @@ package middleware import ( "github.com/gorilla/sessions" "github.com/labstack/echo-contrib/session" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" gonanoid "github.com/matoous/go-nanoid/v2" ) // CustomMiddleware 创建自定义中间件 func AuthMiddleware() echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { + return func(c *echo.Context) error { sess, err := session.Get("session", c) if err != nil { return err @@ -30,11 +30,8 @@ func AuthMiddleware() echo.MiddlewareFunc { return err } } - - cc := c.(*CustomContext) - cc.Auth = sess.Values["auth"] - // 将自定义上下文传递给下一个处理器 - return next(cc) + c.Set("auth", sess.Values["auth"]) + return next(c) } } } diff --git a/backend/middleware/context.go b/backend/middleware/context.go deleted file mode 100644 index 5124f2f..0000000 --- a/backend/middleware/context.go +++ /dev/null @@ -1,28 +0,0 @@ -package middleware - -import ( - "github.com/labstack/echo/v4" -) - -// CustomContext 扩展 echo.Context 以添加自定义功能 -type CustomContext struct { - echo.Context - Auth interface{} -} - -// NewCustomContext 创建自定义上下文的构造函数 -func NewCustomContext(c echo.Context) *CustomContext { - return &CustomContext{ - Context: c, - } -} - -// ContextMiddleware 中间件用于将标准 echo.Context 转换为 CustomContext -func ContextMiddleware() echo.MiddlewareFunc { - return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - cc := NewCustomContext(c) - return next(cc) - } - } -} diff --git a/backend/middleware/logger.go b/backend/middleware/logger.go index 1818af3..80fbac0 100644 --- a/backend/middleware/logger.go +++ b/backend/middleware/logger.go @@ -1,8 +1,8 @@ package middleware import ( - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" "go.uber.org/zap" ) @@ -10,7 +10,7 @@ func LoggerMiddleware() echo.MiddlewareFunc { return middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ LogURI: true, LogStatus: true, - LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error { + LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error { zap.L().Info("request", zap.String("url", v.URI), zap.Int("status", v.Status), diff --git a/backend/middleware/rate_limit.go b/backend/middleware/rate_limit.go index 4e218a9..7eb382c 100644 --- a/backend/middleware/rate_limit.go +++ b/backend/middleware/rate_limit.go @@ -5,9 +5,8 @@ import ( "slices" "time" - "github.com/labstack/echo/v4" - echo_middleware "github.com/labstack/echo/v4/middleware" - "golang.org/x/time/rate" + "github.com/labstack/echo/v5" + echo_middleware "github.com/labstack/echo/v5/middleware" ) type RateSkiper struct { @@ -23,22 +22,22 @@ var RateSkipList = []RateSkiper{ func RateLimiterMiddleware() echo.MiddlewareFunc { config := echo_middleware.RateLimiterConfig{ - Skipper: func(e echo.Context) bool { + Skipper: func(e *echo.Context) bool { path := e.Path() r := e.Request() return slices.Contains(RateSkipList, RateSkiper{Path: path, Method: r.Method}) }, Store: echo_middleware.NewRateLimiterMemoryStoreWithConfig( - echo_middleware.RateLimiterMemoryStoreConfig{Rate: rate.Limit(10), Burst: 30, ExpiresIn: 3 * time.Minute}, + echo_middleware.RateLimiterMemoryStoreConfig{Rate: 10, Burst: 30, ExpiresIn: 3 * time.Minute}, ), - IdentifierExtractor: func(ctx echo.Context) (string, error) { + IdentifierExtractor: func(ctx *echo.Context) (string, error) { id := ctx.RealIP() return id, nil }, - ErrorHandler: func(context echo.Context, err error) error { + ErrorHandler: func(context *echo.Context, err error) error { return context.JSON(http.StatusForbidden, nil) }, - DenyHandler: func(context echo.Context, identifier string, err error) error { + DenyHandler: func(context *echo.Context, identifier string, err error) error { return context.JSON(http.StatusTooManyRequests, nil) }, } diff --git a/backend/middleware/session.go b/backend/middleware/session.go index 6f8a21c..2f22ec7 100644 --- a/backend/middleware/session.go +++ b/backend/middleware/session.go @@ -3,7 +3,7 @@ package middleware import ( "github.com/gorilla/sessions" "github.com/labstack/echo-contrib/session" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" ) func SessionMiddleware() echo.MiddlewareFunc { diff --git a/backend/route.go b/backend/route.go new file mode 100644 index 0000000..842a0e6 --- /dev/null +++ b/backend/route.go @@ -0,0 +1,33 @@ +package main + +import ( + "backend/internal/controllers" + + "github.com/labstack/echo/v5" +) + +type Route struct { + Method []string + Path string + Handler func(c *echo.Context) error +} + +var routes = []Route{ + {Method: []string{"POST"}, Path: "/file/create", Handler: controllers.CreateUploadTask}, + {Method: []string{"POST"}, Path: "/file/slice", Handler: controllers.UploadFileSlice}, + {Method: []string{"POST"}, Path: "/file/finish", Handler: controllers.FinishUploadTask}, + + {Method: []string{"GET"}, Path: "/share/:id", Handler: controllers.GetShareInfo}, + {Method: []string{"POST"}, Path: "/share", Handler: controllers.CreateShareInfo}, + + {Method: []string{"GET"}, Path: "/download", Handler: controllers.DownloadShare}, + {Method: []string{"POST"}, Path: "/download", Handler: controllers.VaildateShare}, + {Method: []string{"GET"}, Path: "/share/pickup/:code", Handler: controllers.GetShareByPickupCode}, + + {Method: []string{"GET"}, Path: "/stat", Handler: controllers.GetStat}, + {Method: []string{"GET"}, Path: "/config", Handler: controllers.GetConfig}, + {Method: []string{"GET"}, Path: "/about", Handler: controllers.GetAbout}, + + {Method: []string{"POST"}, Path: "/task/:type", Handler: controllers.CreateTask}, + {Method: []string{"GET"}, Path: "/task/:id", Handler: controllers.GetTask}, +} diff --git a/backend/internal/utils/http_result_test.go b/backend/test/utils/http_result_test.go similarity index 89% rename from backend/internal/utils/http_result_test.go rename to backend/test/utils/http_result_test.go index 8656686..68a218b 100644 --- a/backend/internal/utils/http_result_test.go +++ b/backend/test/utils/http_result_test.go @@ -6,7 +6,9 @@ import ( "net/http/httptest" "testing" - "github.com/labstack/echo/v4" + "backend/internal/utils" + + "github.com/labstack/echo/v5" "github.com/stretchr/testify/assert" ) @@ -17,7 +19,7 @@ func TestHTTPSuccessHandler(t *testing.T) { c := e.NewContext(req, rec) data := map[string]interface{}{"result": "success"} - err := HTTPSuccessHandler(c, data) + err := utils.HTTPSuccessHandler(c, data) assert.NoError(t, err) assert.Equal(t, http.StatusOK, rec.Code) @@ -40,7 +42,7 @@ func TestHTTPErrorHandler(t *testing.T) { rec := httptest.NewRecorder() c := e.NewContext(req, rec) - err := HTTPErrorHandler(c, assert.AnError) + err := utils.HTTPErrorHandler(c, assert.AnError) assert.NoError(t, err) assert.Equal(t, http.StatusBadRequest, rec.Code) diff --git a/backend/internal/utils/password_test.go b/backend/test/utils/password_test.go similarity index 72% rename from backend/internal/utils/password_test.go rename to backend/test/utils/password_test.go index 6584560..bcec6f3 100644 --- a/backend/internal/utils/password_test.go +++ b/backend/test/utils/password_test.go @@ -1,30 +1,31 @@ package utils import ( - "os" + "bytes" + "fmt" "testing" + "backend/internal/utils" + u "pkg/utils" + "github.com/stretchr/testify/assert" ) func TestGeneratePasswordHash(t *testing.T) { - // 保存原始环境变量 - originalSalt := os.Getenv("share.password_salt") - defer os.Setenv("share.password_salt", originalSalt) tests := []struct { name string password string salt string expectError bool - errorMsg string + err error }{ { name: "share.password_salt未配置", password: "testpassword", salt: "", expectError: true, - errorMsg: "请配置share.password_salt", + err: utils.ErrPasswordSaltNotSet, }, { name: "正常生成哈希", @@ -37,21 +38,23 @@ func TestGeneratePasswordHash(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // 设置环境变量 - if tt.salt != "" { - os.Setenv("share.password_salt", tt.salt) - } else { - os.Unsetenv("share.password_salt") - } + u.InitEnv(u.EnvOption{ + ConfigData: bytes.NewBuffer([]byte(fmt.Sprintf(` + share: + password_salt: %s + `, tt.salt))), + }) + u.SetEnv("share.password_salt", tt.salt) - hash, err := GeneratePasswordHash(tt.password) + hash, err := utils.GeneratePasswordHash(tt.password) if tt.expectError { if err == nil { t.Errorf("期望错误,但得到了 nil") return } - if err.Error() != tt.errorMsg { - t.Errorf("期望错误信息 '%s',但得到了 '%s'", tt.errorMsg, err.Error()) + if err != tt.err { + t.Errorf("期望错误信息 '%s',但得到了 '%s'", tt.err.Error(), err.Error()) } return } diff --git a/front/components/About/AboutBaseInfo.vue b/front/components/About/AboutBaseInfo.vue index 8de4eff..8743651 100644 --- a/front/components/About/AboutBaseInfo.vue +++ b/front/components/About/AboutBaseInfo.vue @@ -52,14 +52,14 @@ const genUserAvatar = (email: string) => {
{{ renderI18n(appConfig?.site_title ?? {}, 'en', locale) }}
- + 015
-
{{ t('about.systemInfo') }}
+
{{ t('page.about.systemInfo') }}
diff --git a/front/components/Preprocessing/FileShareHandle.vue b/front/components/Preprocessing/FileShareHandle.vue index ac26eff..4b70ae8 100644 --- a/front/components/Preprocessing/FileShareHandle.vue +++ b/front/components/Preprocessing/FileShareHandle.vue @@ -15,37 +15,37 @@ const props = defineProps<{