mirror of
https://github.com/keven1024/015.git
synced 2026-05-27 23:49:35 +00:00
Merge pull request '0.0.9' (#1) from dev into main
Reviewed-on: https://gitea.fudaoyuan.icu/keven/015/pulls/1
This commit is contained in:
31
.gitea/workflows/build.yaml
Normal file
31
.gitea/workflows/build.yaml
Normal file
@@ -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
|
||||
47
.gitea/workflows/lint.yaml
Normal file
47
.gitea/workflows/lint.yaml
Normal file
@@ -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
|
||||
52
.gitea/workflows/publish.yaml
Normal file
52
.gitea/workflows/publish.yaml
Normal file
@@ -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"
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -22,6 +22,7 @@ logs
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
config.yaml
|
||||
|
||||
# Serwist
|
||||
/front/public/sw*
|
||||
|
||||
21
Dockerfile
21
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
|
||||
|
||||
17
README-zh.md
17
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
|
||||
```
|
||||
|
||||
|
||||
## 🏗️ 技术架构
|
||||
|
||||
### 前端技术栈
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()))),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
30
backend/internal/controllers/errors.go
Normal file
30
backend/internal/controllers/errors.go
Normal file
@@ -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") // 下载次数不足
|
||||
)
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
7
backend/internal/controllers/task/errors.go
Normal file
7
backend/internal/controllers/task/errors.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package task
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrInvalidRequest = errors.New("InvalidRequest")
|
||||
)
|
||||
53
backend/internal/controllers/task/image.go
Normal file
53
backend/internal/controllers/task/image.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
14
backend/middleware.go
Normal file
14
backend/middleware.go
Normal file
@@ -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,
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
33
backend/route.go
Normal file
33
backend/route.go
Normal file
@@ -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},
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
}
|
||||
@@ -52,14 +52,14 @@ const genUserAvatar = (email: string) => {
|
||||
<div class="flex flex-col gap-2 items-center">
|
||||
<div class="text-xl">{{ renderI18n(appConfig?.site_title ?? {}, 'en', locale) }}</div>
|
||||
<div class="text-sm opacity-75 text-center px-5">
|
||||
<I18nT keypath="about.powerBy" tag="span">
|
||||
<I18nT keypath="page.about.powerBy" tag="span">
|
||||
<NuxtLink href="https://github.com/keven1024/015" target="_blank" class="text-primary hover:underline">015</NuxtLink>
|
||||
</I18nT>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="font-semibold">{{ t('about.systemInfo') }}</div>
|
||||
<div class="font-semibold">{{ t('page.about.systemInfo') }}</div>
|
||||
<template v-if="isLoading">
|
||||
<div class="flex flex-row gap-2">
|
||||
<Skeleton class="w-full h-20 rounded-xl" v-for="i in 2" :key="i" />
|
||||
@@ -68,7 +68,7 @@ const genUserAvatar = (email: string) => {
|
||||
<template v-else>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
<div class="rounded-xl bg-white/50 flex-1 flex flex-col p-3 gap-2">
|
||||
<div class="opacity-75 text-xs">{{ t('about.admin') }}</div>
|
||||
<div class="opacity-75 text-xs">{{ t('page.about.admin') }}</div>
|
||||
<div
|
||||
class="flex flex-row gap-2 items-center cursor-pointer"
|
||||
@click="
|
||||
@@ -98,7 +98,7 @@ const genUserAvatar = (email: string) => {
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-xl bg-white/50 flex-1 flex flex-col p-3 gap-2">
|
||||
<div class="opacity-75 text-xs">{{ t('about.storage') }}</div>
|
||||
<div class="opacity-75 text-xs">{{ t('page.about.storage') }}</div>
|
||||
<div class="text-right flex flex-row items-baseline">
|
||||
<span class="text-lg font-semibold">{{ getFileSize(data?.file?.current ?? 0) }}</span>
|
||||
<span class="text-md opacity-75">/ {{ getFileSize(data?.file?.maximun ?? 0) }}</span>
|
||||
@@ -115,7 +115,7 @@ const genUserAvatar = (email: string) => {
|
||||
<Accordion type="single" collapsible>
|
||||
<AccordionItem value="about">
|
||||
<AccordionTrigger>
|
||||
<span class="font-semibold">{{ t('about.about') }}</span>
|
||||
<span class="font-semibold">{{ t('page.about.about') }}</span>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<MarkdownRender class="max-w-full" :markdown="renderI18n(data?.content ?? {}, 'en', locale) ?? ''" />
|
||||
|
||||
@@ -51,28 +51,28 @@ const { t } = useI18n()
|
||||
const chartTabs = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: t('about.file'),
|
||||
label: t('page.about.file'),
|
||||
value: 'storage',
|
||||
total: data.value?.chart?.storage
|
||||
? Object.values(data.value.chart.storage).reduce((acc: number, curr: StatChartData) => acc + curr.file_num, 0)
|
||||
: 0,
|
||||
},
|
||||
{
|
||||
label: t('about.share'),
|
||||
label: t('page.about.share'),
|
||||
value: 'share',
|
||||
total: data.value?.chart?.storage
|
||||
? Object.values(data.value.chart.storage).reduce((acc: number, curr: StatChartData) => acc + curr.share_num, 0)
|
||||
: 0,
|
||||
},
|
||||
{
|
||||
label: t('about.download'),
|
||||
label: t('page.about.download'),
|
||||
value: 'download',
|
||||
total: data.value?.chart?.storage
|
||||
? Object.values(data.value.chart.storage).reduce((acc: number, curr: StatChartData) => acc + curr.download_num, 0)
|
||||
: 0,
|
||||
},
|
||||
{
|
||||
label: t('about.task'),
|
||||
label: t('page.about.task'),
|
||||
value: 'queue',
|
||||
total: data.value?.chart?.queue
|
||||
? Object.values(data.value.chart.queue).reduce((acc: number, curr: QueueChartData) => acc + curr.processed + curr.failed, 0)
|
||||
@@ -144,7 +144,7 @@ const currentChartData = computed((): ChartConfig => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="font-semibold">{{ t('about.analysis') }}</div>
|
||||
<div class="font-semibold">{{ t('page.about.analysis') }}</div>
|
||||
<template v-if="isLoading">
|
||||
<div class="flex flex-row gap-2">
|
||||
<Skeleton class="w-full h-96 rounded-xl" />
|
||||
|
||||
29
front/components/BaseCard.vue
Normal file
29
front/components/BaseCard.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
title?: string
|
||||
showBackButton?: boolean
|
||||
}>()
|
||||
const router = useRouter()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rounded-xl p-5 bg-white/50 backdrop-blur-xl w-full lg:w-200">
|
||||
<div v-if="title" class="flex flex-row justify-between">
|
||||
<h1 class="text-xl font-normal">{{ title }}</h1>
|
||||
<Button
|
||||
v-if="!!showBackButton"
|
||||
variant="outline"
|
||||
class="bg-white/70"
|
||||
size="icon"
|
||||
@click="
|
||||
() => {
|
||||
router.push('/')
|
||||
}
|
||||
"
|
||||
>
|
||||
<LucideHome />
|
||||
</Button>
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,19 +1,21 @@
|
||||
<script lang="ts" setup>
|
||||
import { VisSingleContainer, VisDonut } from '@unovis/vue'
|
||||
import { withDefaults, defineProps } from 'vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
value?: number
|
||||
size?: number
|
||||
color?: string
|
||||
}>(), {
|
||||
value: 0,
|
||||
size: 40,
|
||||
color: '#06b6d4'
|
||||
})
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
value?: number
|
||||
size?: number
|
||||
color?: string
|
||||
}>(),
|
||||
{
|
||||
value: 0,
|
||||
size: 40,
|
||||
color: '#06b6d4',
|
||||
}
|
||||
)
|
||||
const data = computed(() => {
|
||||
const progress = Math.min(Math.max(props.value, 0), 100)
|
||||
return [progress, 100 - progress ]
|
||||
return [progress, 100 - progress]
|
||||
})
|
||||
const getValue = (d: number) => d
|
||||
const getColor = (d: number, i: number) => [props?.color, 'transparent'][i]
|
||||
|
||||
@@ -15,6 +15,7 @@ import { cx } from 'class-variance-authority'
|
||||
import { isObject } from 'lodash-es'
|
||||
import showDrawer from '@/lib/showDrawer'
|
||||
import FileShareHandle from '@/components/Preprocessing/FileShareHandle.vue'
|
||||
import ImageConvertHandle from '@/components/Preprocessing/ImageConvertHandle.vue'
|
||||
import type { FileShareHandleProps } from '../Preprocessing/types'
|
||||
const { t } = useI18n()
|
||||
const props = defineProps<{
|
||||
@@ -35,7 +36,7 @@ const isPPT = computed(() => props.file.every((r) => r?.type?.startsWith('applic
|
||||
const isDocument = computed(() => isPDF.value || isDOC.value || isXLS.value || isPPT.value)
|
||||
const actions = [
|
||||
{
|
||||
label: t('file.handleType.file-share'),
|
||||
label: t('page.upload.file.handleType.file-share'),
|
||||
icon: LucideShare,
|
||||
className: 'bg-green-300',
|
||||
onClick: () => {
|
||||
@@ -45,13 +46,23 @@ const actions = [
|
||||
},
|
||||
},
|
||||
isImage.value && {
|
||||
label: t('file.handleType.file-image-compress'),
|
||||
label: t('page.upload.file.handleType.file-image-compress'),
|
||||
icon: LucideImageMinus,
|
||||
className: 'bg-red-300',
|
||||
onClick: () => {
|
||||
props.onFileHandle({ type: 'file-image-compress', config: {} })
|
||||
},
|
||||
},
|
||||
isImage.value && {
|
||||
label: t('page.upload.file.handleType.file-image-convert'),
|
||||
icon: LucideArrowRightLeft,
|
||||
className: 'bg-purple-300',
|
||||
onClick: () => {
|
||||
showDrawer({
|
||||
render: ({ hide }) => h(ImageConvertHandle, { ...props, hide }),
|
||||
})
|
||||
},
|
||||
},
|
||||
// isImage.value && {
|
||||
// label: '图片翻译', icon: LucideLanguages, className: 'bg-orange-300', onClick: () => {
|
||||
// console.log('复制链接')
|
||||
|
||||
@@ -18,7 +18,7 @@ const handleSubmit = async (form: FormContext<GenericObject, GenericObject>) =>
|
||||
}
|
||||
}>(`/api/share/pickup/${code}`)
|
||||
if (!data.data.share_id) {
|
||||
toast.error(t('pickup.codeError'))
|
||||
toast.error(t('page.upload.pickup.codeError'))
|
||||
form.resetForm()
|
||||
return
|
||||
}
|
||||
@@ -29,7 +29,7 @@ const handleSubmit = async (form: FormContext<GenericObject, GenericObject>) =>
|
||||
})
|
||||
return
|
||||
} catch (error) {
|
||||
toast.error(t('pickup.codeError'))
|
||||
toast.error(t('page.upload.pickup.codeError'))
|
||||
form.resetForm()
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ const handleSubmit = async (form: FormContext<GenericObject, GenericObject>) =>
|
||||
<template>
|
||||
<VeeForm>
|
||||
<div class="flex flex-col gap-5">
|
||||
<div class="text-xl font-bold">{{ t('pickup.title') }}</div>
|
||||
<div class="text-xl font-bold">{{ t('page.upload.pickup.title') }}</div>
|
||||
<PinInputField
|
||||
name="code"
|
||||
:rules="
|
||||
|
||||
@@ -1,74 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
LucideShare,
|
||||
LucideImage,
|
||||
LucideBot,
|
||||
LucideLanguages,
|
||||
} from "lucide-vue-next";
|
||||
import { cx } from "class-variance-authority";
|
||||
import showDrawer from "@/lib/showDrawer";
|
||||
import TextShareHandle from "@/components/Preprocessing/TextShareHandle.vue";
|
||||
import { LucideShare, LucideImage, LucideBot, LucideLanguages } from 'lucide-vue-next'
|
||||
import { cx } from 'class-variance-authority'
|
||||
import showDrawer from '@/lib/showDrawer'
|
||||
import TextShareHandle from '@/components/Preprocessing/TextShareHandle.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
hide: () => void;
|
||||
text: string;
|
||||
onTextHandle: ({ type, config }: { type: string; config: any }) => void;
|
||||
}>();
|
||||
const { t } = useI18n();
|
||||
hide: () => void
|
||||
text: string
|
||||
onTextHandle: ({ type, config }: { type: string; config: any }) => void
|
||||
}>()
|
||||
const { t } = useI18n()
|
||||
const actions = [
|
||||
{
|
||||
label: t("text.handleType.text-share"),
|
||||
icon: LucideShare,
|
||||
className: "bg-green-300",
|
||||
onClick: () => {
|
||||
showDrawer({
|
||||
render: ({ hide }) => h(TextShareHandle, { ...props, hide }),
|
||||
});
|
||||
{
|
||||
label: t('page.upload.text.handleType.text-share'),
|
||||
icon: LucideShare,
|
||||
className: 'bg-green-300',
|
||||
onClick: () => {
|
||||
showDrawer({
|
||||
render: ({ hide }) => h(TextShareHandle, { ...props, hide }),
|
||||
})
|
||||
},
|
||||
},
|
||||
},
|
||||
// {
|
||||
// label: '生成配图', icon: LucideImage, className: 'bg-red-300', onClick: () => {
|
||||
// console.log('复制链接')
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// label: '问大模型', icon: LucideBot, className: 'bg-blue-300', onClick: () => {
|
||||
// console.log('复制链接')
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// label: '文本翻译', icon: LucideLanguages, className: 'bg-orange-300', onClick: () => {
|
||||
// console.log('复制链接')
|
||||
// }
|
||||
// },
|
||||
];
|
||||
// {
|
||||
// label: '生成配图', icon: LucideImage, className: 'bg-red-300', onClick: () => {
|
||||
// console.log('复制链接')
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// label: '问大模型', icon: LucideBot, className: 'bg-blue-300', onClick: () => {
|
||||
// console.log('复制链接')
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// label: '文本翻译', icon: LucideLanguages, className: 'bg-orange-300', onClick: () => {
|
||||
// console.log('复制链接')
|
||||
// }
|
||||
// },
|
||||
]
|
||||
</script>
|
||||
<template>
|
||||
<div class="flex flex-col gap-5 p-5">
|
||||
<div class="flex flex-row gap-2">
|
||||
<div
|
||||
v-for="item in actions"
|
||||
:key="item.label"
|
||||
class="flex flex-col items-center gap-2 max-w-20"
|
||||
@click="
|
||||
() => {
|
||||
props?.hide();
|
||||
item?.onClick();
|
||||
}
|
||||
"
|
||||
>
|
||||
<div
|
||||
:class="
|
||||
cx(
|
||||
'size-14 flex justify-center items-center rounded-full mx-3',
|
||||
item?.className,
|
||||
)
|
||||
"
|
||||
>
|
||||
<component :is="item?.icon" />
|
||||
<div class="flex flex-col gap-5 p-5">
|
||||
<div class="flex flex-row gap-2">
|
||||
<div
|
||||
v-for="item in actions"
|
||||
:key="item.label"
|
||||
class="flex flex-col items-center gap-2 max-w-20"
|
||||
@click="
|
||||
() => {
|
||||
props?.hide()
|
||||
item?.onClick()
|
||||
}
|
||||
"
|
||||
>
|
||||
<div :class="cx('size-14 flex justify-center items-center rounded-full mx-3', item?.className)">
|
||||
<component :is="item?.icon" />
|
||||
</div>
|
||||
<div class="text-xs truncate w-full text-center">{{ item?.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs truncate w-full text-center">{{ item?.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -56,14 +56,14 @@ const { t } = useI18n()
|
||||
<div class="size-16 flex justify-center items-center rounded-xl bg-white/80">
|
||||
<PlusIcon class="size-7" />
|
||||
</div>
|
||||
<div class="mb-3">{{ t('file.addMore') }}</div>
|
||||
<div class="mb-3">{{ t('page.upload.file.addMore') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<LucideUpload class="size-10" />
|
||||
<div class="text-sm select-none">
|
||||
{{ t('file.uploadFilePlaceholder') }}
|
||||
{{ t('page.upload.file.uploadFilePlaceholder') }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -32,7 +32,7 @@ const fileIcon = computed(() => {
|
||||
if (baseType === 'audio') {
|
||||
return LucideFileAudio
|
||||
}
|
||||
if (baseType === 'text' || ['json', 'ld+json', 'html']?.includes(type)) {
|
||||
if (baseType === 'text' || ['json', 'ld+json', 'html']?.includes(type ?? '')) {
|
||||
return LucideFileCode
|
||||
}
|
||||
if (
|
||||
@@ -44,11 +44,11 @@ const fileIcon = computed(() => {
|
||||
'vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'vnd.ms-powerpoint',
|
||||
'vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
].includes(type)
|
||||
].includes(type ?? '')
|
||||
) {
|
||||
return LucideFileText
|
||||
}
|
||||
if (['zip', 'vnd.rar', 'x-tar', 'gz', 'bz2', 'x-7z-compressed'].includes(type)) {
|
||||
if (['zip', 'vnd.rar', 'x-tar', 'gz', 'bz2', 'x-7z-compressed'].includes(type ?? '')) {
|
||||
return LucideFileArchive
|
||||
}
|
||||
return LucideFile
|
||||
|
||||
@@ -12,3 +12,5 @@ watchEffect(() => {
|
||||
dayjs.locale(locale.value.toLowerCase())
|
||||
})
|
||||
</script>
|
||||
|
||||
<template></template>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import VeeForm from '@/components/VeeForm.vue'
|
||||
import FileUploadInputFileView from './FileUploadInputFileView.vue'
|
||||
import FileUploadProgressView from './FileUploadProgressView.vue'
|
||||
import FileUploadProgressView from './FileUploadProgressView/index.vue'
|
||||
import ResultIndexView from '@/components/Result/ResultIndexView.vue'
|
||||
|
||||
const fileStepList = [
|
||||
@@ -16,19 +16,26 @@ const renderComponent = computed(() => {
|
||||
return fileStepList.find((item) => item.key === step.value)?.component
|
||||
})
|
||||
const formRef = ref<InstanceType<typeof VeeForm>>()
|
||||
watch(() => step.value, (newVal) => {
|
||||
if (newVal === 'input') {
|
||||
formRef.value?.form?.resetForm()
|
||||
formRef.value?.form?.setValues({ file: null })
|
||||
watch(
|
||||
() => step.value,
|
||||
(newVal) => {
|
||||
if (newVal === 'input') {
|
||||
formRef.value?.form?.resetForm()
|
||||
formRef.value?.form?.setValues({ file: null })
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
</script>
|
||||
<template>
|
||||
<VeeForm ref="formRef" v-slot="{ values }" :keepValues="true">
|
||||
<div class="rounded-xl p-5 bg-white/50 backdrop-blur-xl w-full lg:w-200">
|
||||
<component :is="renderComponent" :data="values" @change="(key: string) => {
|
||||
step = key
|
||||
}" />
|
||||
</div>
|
||||
<component
|
||||
:is="renderComponent"
|
||||
:data="values"
|
||||
@change="
|
||||
(key: string) => {
|
||||
step = key
|
||||
}
|
||||
"
|
||||
/>
|
||||
</VeeForm>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -1,42 +1,39 @@
|
||||
<script lang="ts" setup>
|
||||
import showDrawer from "@/lib/showDrawer";
|
||||
import FileShareDrawer from "@/components/Drawer/FileShareDrawer.vue";
|
||||
import FileUploadField from "@/components/Field/FileUploadField.vue";
|
||||
import FormButton from "@/components/Field/FormButton.vue";
|
||||
import PickupShareBtn from "@/components/PickupShareBtn.vue";
|
||||
import showDrawer from '@/lib/showDrawer'
|
||||
import FileShareDrawer from '@/components/Drawer/FileShareDrawer.vue'
|
||||
import FileUploadField from '@/components/Field/FileUploadField.vue'
|
||||
import FormButton from '@/components/Field/FormButton.vue'
|
||||
import PickupShareBtn from '@/components/PickupShareBtn.vue'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "change", key: string): void;
|
||||
}>();
|
||||
(e: 'change', key: string): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n();
|
||||
const { t } = useI18n()
|
||||
|
||||
const handleFormSubmit = async (form: any) => {
|
||||
const { file } = form?.values || {};
|
||||
showDrawer({
|
||||
render: ({ hide }) =>
|
||||
h(FileShareDrawer, {
|
||||
hide,
|
||||
file,
|
||||
onFileHandle: ({ type, config }) => {
|
||||
form.setFieldValue("handle_type", type);
|
||||
form.setFieldValue("config", config);
|
||||
emit("change", "progress");
|
||||
},
|
||||
}),
|
||||
});
|
||||
};
|
||||
const { file } = form?.values || {}
|
||||
showDrawer({
|
||||
render: ({ hide }) =>
|
||||
h(FileShareDrawer, {
|
||||
hide,
|
||||
file,
|
||||
onFileHandle: ({ type, config }) => {
|
||||
form.setFieldValue('handle_type', type)
|
||||
form.setFieldValue('config', config)
|
||||
emit('change', 'progress')
|
||||
},
|
||||
}),
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gap-5 flex flex-col">
|
||||
<div class="text-xl font-normal">{{ t("file.uploadFile") }}</div>
|
||||
<FileUploadField name="file" rules="required" />
|
||||
<div class="flex flex-row gap-3">
|
||||
<FormButton @click="handleFormSubmit">
|
||||
<LucideShare class="size-4" />{{ t("btn.submit") }}
|
||||
</FormButton>
|
||||
<PickupShareBtn />
|
||||
</div>
|
||||
</div>
|
||||
<BaseCard class="gap-5 flex flex-col" :title="t('page.upload.file.uploadFile')">
|
||||
<FileUploadField name="file" rules="required" />
|
||||
<div class="flex flex-row gap-3">
|
||||
<FormButton @click="handleFormSubmit"> <LucideShare class="size-4" />{{ t('btn.submit') }} </FormButton>
|
||||
<PickupShareBtn />
|
||||
</div>
|
||||
</BaseCard>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<script setup lang="ts">
|
||||
import getFileSize from '~/lib/getFileSize'
|
||||
import { clamp } from 'lodash-es'
|
||||
import FileUploadBlockProgressView from '@/components/FileUploadBlockProgressView.vue'
|
||||
import FileUploadHeatMapView from '@/components/FileUploadHeatMapView.vue'
|
||||
import type { SelectedFile, Uploadfile } from './types'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
uploadfiles: Uploadfile[]
|
||||
selectedFile: SelectedFile
|
||||
}>()
|
||||
const selectedUploadfile = computed(() => props.uploadfiles.find((item) => item.fileId === props.selectedFile))
|
||||
const selectedUploadfileChunk = computed(() => Object.values(selectedUploadfile.value?.uploadInfo?.chunks || {}))
|
||||
const selectedUploadfileViewMode = ref<'progress' | 'heatmap'>('progress')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="col-span-4 flex flex-col bg-white/80 rounded-xl p-3 gap-5" v-if="selectedFile">
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<LucideInfo class="size-4" />
|
||||
{{ t('page.progress.file.uploadDetails') }}
|
||||
</div>
|
||||
<div class="grid grid-cols-3 text-sm gap-3">
|
||||
<div>
|
||||
{{ t('page.progress.file.chunk') }}: {{ selectedUploadfile?.uploadInfo?.chunkLength }} x
|
||||
{{ getFileSize(selectedUploadfile?.uploadInfo?.ChunkSize as number) }}
|
||||
</div>
|
||||
<div class="truncate col-span-2">Hash: {{ selectedUploadfile?.hash }}</div>
|
||||
<div>{{ t('page.progress.file.completed') }}: {{ selectedUploadfileChunk?.filter((r) => r.status === 'success')?.length || 0 }}</div>
|
||||
<div>{{ t('page.progress.file.discarded') }}: {{ selectedUploadfileChunk?.filter((r) => r.status === 'error')?.length || 0 }}</div>
|
||||
<div>
|
||||
{{ t('page.progress.file.pending') }}:
|
||||
{{ (selectedUploadfile?.uploadInfo?.chunkLength || 0) - (selectedUploadfileChunk?.length || 0) }}
|
||||
</div>
|
||||
<div class="col-span-3 flex flex-row justify-between items-center">
|
||||
<div class="text-md font-bold">
|
||||
{{ selectedUploadfileViewMode === 'progress' ? t('page.progress.file.chunkProgress') : t('page.progress.file.chunkHeatmap') }}
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
class="ml-auto text-xs"
|
||||
@click="selectedUploadfileViewMode = selectedUploadfileViewMode === 'progress' ? 'heatmap' : 'progress'"
|
||||
>
|
||||
<LucideArrowDownUp class="size-4" />
|
||||
{{ selectedUploadfileViewMode === 'progress' ? t('page.progress.file.heatmap') : t('page.progress.file.progressBar') }}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="h-7 col-span-3 flex flex-row gap-2 items-center" v-if="selectedUploadfileViewMode === 'progress'">
|
||||
<div class="flex-1 h-full">
|
||||
<FileUploadBlockProgressView :data="selectedUploadfile?.uploadInfo" />
|
||||
</div>
|
||||
{{ clamp(((selectedUploadfileChunk?.length || 0) / (selectedUploadfile?.uploadInfo?.chunkLength || 0)) * 100, 0, 100)?.toFixed(2) }}%
|
||||
</div>
|
||||
<div v-if="selectedUploadfileViewMode === 'heatmap'" class="col-span-3 bg-black/5 rounded p-2">
|
||||
<FileUploadHeatMapView :size="12" :data="selectedUploadfile?.uploadInfo" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
<template>
|
||||
<div class="flex flex-col gap-2 pt-3">
|
||||
<div class="text-xl font-bold">{{ t('page.progress.file.uploadSpeedInfo.title') }}</div>
|
||||
<div class="opacity-75">
|
||||
<i18n-t keypath="page.progress.file.uploadSpeedInfo.desc.base">
|
||||
<template #chunkNum>
|
||||
<b>{{ t('page.progress.file.uploadSpeedInfo.desc.chunkNum') }}</b>
|
||||
</template>
|
||||
<template #chunkSize>
|
||||
<b>{{ t('page.progress.file.uploadSpeedInfo.desc.chunkSize') }}</b>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,78 @@
|
||||
<script setup lang="ts">
|
||||
import type { Uploadfile } from './types'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { uploadfiles } = defineProps<{
|
||||
uploadfiles: Uploadfile[]
|
||||
}>()
|
||||
const totalTaskStatus = computed(() => {
|
||||
if (uploadfiles.some((r) => r.status === 'start')) {
|
||||
return 'start'
|
||||
}
|
||||
if (uploadfiles.some((r) => r.status === 'pause')) {
|
||||
return 'pause'
|
||||
}
|
||||
return 'disabled'
|
||||
})
|
||||
const totalUploadProgress = computed(() => {
|
||||
const successCount = uploadfiles.reduce((acc, curr) => {
|
||||
const { status, uploadInfo } = curr || {}
|
||||
if (status === 'finish') return acc
|
||||
const { chunks } = uploadInfo || {}
|
||||
return acc + Object.entries(chunks || {}).filter(([index, chunk]) => chunk.status === 'success').length
|
||||
}, 0)
|
||||
const totalCount = uploadfiles.reduce((acc, curr) => {
|
||||
const { status, uploadInfo } = curr || {}
|
||||
if (status === 'finish') return acc
|
||||
const { chunkLength } = uploadInfo || {}
|
||||
return acc + (chunkLength || 0)
|
||||
}, 0)
|
||||
return ((successCount || 0) / (totalCount || 0)) * 100
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rounded-xl col-span-4 md:col-span-1 bg-white/80 h-32 md:h-auto md:aspect-square flex flex-col gap-2 relative overflow-hidden">
|
||||
<div class="absolute top-0 left-0 w-full h-full z-[0] flex flex-col justify-end">
|
||||
<div class="w-full bg-green-100 border-t border-green-500" :style="`height: ${totalUploadProgress || 0}%`"></div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 justify-between p-3 h-full relative z-[1]">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="text-xs opacity-70">{{ t('page.progress.file.totalUploadProgress') }}</div>
|
||||
<div class="text-4xl font-bold">{{ (totalUploadProgress || 0).toFixed(1) }}%</div>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2">
|
||||
<Button
|
||||
class="aspect-square hover:text-white p-0 bg-green-200 hover:bg-green-300 text-green-500"
|
||||
:disabled="['start', 'disabled'].includes(totalTaskStatus)"
|
||||
@click="
|
||||
() => {
|
||||
uploadfiles.forEach((r) => {
|
||||
r.status = 'start'
|
||||
})
|
||||
}
|
||||
"
|
||||
>
|
||||
<LucidePlay class="size-1/2" />
|
||||
</Button>
|
||||
<Button
|
||||
class="aspect-square hover:text-white p-0 bg-orange-200 hover:bg-orange-300 text-orange-500"
|
||||
:disabled="['pause', 'disabled'].includes(totalTaskStatus)"
|
||||
@click="
|
||||
() => {
|
||||
uploadfiles.forEach((r) => {
|
||||
r.status = 'pause'
|
||||
})
|
||||
}
|
||||
"
|
||||
>
|
||||
<LucideSquare class="size-1/2" />
|
||||
</Button>
|
||||
<!-- <Button class="aspect-square bg-blue-200 hover:bg-blue-300 text-blue-500 hover:text-white p-0">
|
||||
<LucideSettings class="size-1/2" />
|
||||
</Button> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,60 @@
|
||||
<script setup lang="ts">
|
||||
import dayjs from 'dayjs'
|
||||
import { motion } from 'motion-v'
|
||||
import getFileSize from '~/lib/getFileSize'
|
||||
import showDrawer from '~/lib/showDrawer'
|
||||
import FileUploadSpeedInfoView from './FileUploadSpeedInfoView.vue'
|
||||
import { clamp } from 'lodash-es'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
speedChartData: Record<string, { fileId: string; index: number; value: number }[]>
|
||||
}>()
|
||||
const speedChartList = ref<{ timestamp: number; value: number }[]>([])
|
||||
useIntervalFn(() => {
|
||||
speedChartList.value.push({
|
||||
timestamp: dayjs().unix() - 1,
|
||||
value: props.speedChartData[dayjs().unix() - 1]?.reduce((acc, curr) => acc + curr.value, 0) ?? 0,
|
||||
})
|
||||
if (speedChartList.value.length > 60) {
|
||||
speedChartList.value.shift()
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
const handleShowSpeedInfo = () => {
|
||||
showDrawer({
|
||||
render: ({ hide }) => h(FileUploadSpeedInfoView, { hide }),
|
||||
})
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="rounded-xl p-3 bg-white/80 flex flex-col gap-2 col-span-4 md:col-span-3 h-32 md:h-auto">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div @click="handleShowSpeedInfo" class="flex flex-row gap-1 items-center text-xs opacity-70">
|
||||
{{ t('page.progress.file.totalUploadProgress') }}
|
||||
<LucideInfo class="size-3" />
|
||||
</div>
|
||||
<div class="text-2xl font-bold">
|
||||
{{ `${getFileSize(speedChartData[dayjs().unix() - 1]?.reduce((acc, curr) => acc + curr.value, 0) ?? 0) ?? 0}/s` }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 relative overflow-hidden flex flex-row gap-0.5 justify-end items-end">
|
||||
<motion.div
|
||||
class="w-2 shrink-0 bg-primary relative"
|
||||
:style="{
|
||||
height: `${clamp((i.value / Math.max(...(speedChartList?.map((r) => r.value) || [1]))) * 100, 1, 100)}%`,
|
||||
}"
|
||||
:layoutId="i.timestamp"
|
||||
v-for="i in speedChartList"
|
||||
:key="i.timestamp"
|
||||
:initial="{ x: 10, opacity: 0 }"
|
||||
:animate="{ x: 0, opacity: 1 }"
|
||||
:transition="{ duration: 1 }"
|
||||
>
|
||||
</motion.div>
|
||||
<!-- <BarChart class="h-full" :data="data" index="time" :categories="['value']" :showTooltip="false"
|
||||
:showLegend="false" :showXAxis="false" :showYAxis="false" :showGrid="false" :groupMaxWidth="10" /> -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,21 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import { LucidePlay, LucideSettings, LucideSquare } from 'lucide-vue-next'
|
||||
import { LucideSquare, LucideInfo, LucideFolders, LucideArrowUpFromLine, LucideCircleX, LucideCheckCircle, LucideLoaderCircle } from 'lucide-vue-next'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import FileUploadBlockProgressView from '@/components/FileUploadBlockProgressView.vue'
|
||||
import { motion } from 'motion-v'
|
||||
|
||||
import getFileSize from '~/lib/getFileSize'
|
||||
import { cx } from 'class-variance-authority'
|
||||
import asyncWorker from '@/lib/asyncWorker'
|
||||
import calcFileHashWorker from '@/lib/calcFileHashWorker?worker'
|
||||
import { clamp, get, groupBy, isEmpty, isNumber, isString, sample, shuffle, times } from 'lodash-es'
|
||||
import { clamp, get, isEmpty, isNumber, sample, shuffle, throttle, times } from 'lodash-es'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { toast } from 'vue-sonner'
|
||||
import dayjs from 'dayjs'
|
||||
import showDrawer from '~/lib/showDrawer'
|
||||
import FileUploadTotalSpeedView from './FileUploadTotalSpeedView.vue'
|
||||
import FileUploadTotalProgressControlView from './FileUploadTotalProgressControlView.vue'
|
||||
import FileUploadSpeedInfoView from './FileUploadSpeedInfoView.vue'
|
||||
import getFileChunk from '~/lib/getFileChunk'
|
||||
import type { FileHandleKey } from '~/components/Preprocessing/types'
|
||||
import asyncWait from '@/lib/asyncWait'
|
||||
import axios from 'axios'
|
||||
import type { SelectedFile, Uploadfile } from './types'
|
||||
import FileUploadDetailView from './FileUploadDetailView.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
data: { file: File[]; config: Record<string, any>; handle_type: FileHandleKey }
|
||||
@@ -24,81 +28,17 @@ const emit = defineEmits<{
|
||||
(e: 'change', key: string): void
|
||||
}>()
|
||||
const form = useFormContext()
|
||||
const { t } = useI18n()
|
||||
|
||||
const selectedFile = ref()
|
||||
const uploadfiles = ref<
|
||||
{
|
||||
fileId: string
|
||||
id?: string // 后端文件id
|
||||
file: File
|
||||
status: 'start' | 'pause' | 'finish' | 'error'
|
||||
hash?: string
|
||||
procressType: 'hash' | 'create' | 'upload' | 'finish'
|
||||
uploadInfo?: {
|
||||
chunks: Record<number, { status: 'success' | 'error' | 'processing'; createdAt: number }>
|
||||
chunkLength: number
|
||||
ChunkSize: number
|
||||
}
|
||||
queue: {
|
||||
taskId: string
|
||||
taskType: 'hash' | 'create' | 'chunk' | 'upload' | 'finish'
|
||||
queueType: 'sync' | 'async' // sync任务禁止并发
|
||||
index?: number
|
||||
retry?: number
|
||||
}[]
|
||||
}[]
|
||||
>([])
|
||||
const selectedUploadfile = computed(() => uploadfiles.value.find((item) => item.fileId === selectedFile.value))
|
||||
const selectedUploadfileChunk = computed(() => Object.values(selectedUploadfile.value?.uploadInfo?.chunks || {}))
|
||||
const selectedUploadfileViewMode = ref<'progress' | 'heatmap'>('progress')
|
||||
const selectedFile = ref<SelectedFile>(null)
|
||||
const uploadfiles = ref<Uploadfile[]>([])
|
||||
|
||||
const procressTaskList = ref<Map<string, any>>(new Map())
|
||||
const activeTaskList = computed(() => uploadfiles.value.filter((r) => r.queue.length > 0 && r.status === 'start'))
|
||||
const activeTaskAllQueue = computed(() => activeTaskList.value.flatMap((r) => r.queue))
|
||||
const batchNum = ref(3)
|
||||
|
||||
const totalTaskStatus = computed(() => {
|
||||
if (uploadfiles.value.some((r) => r.status === 'start')) {
|
||||
return 'start'
|
||||
}
|
||||
if (uploadfiles.value.some((r) => r.status === 'pause')) {
|
||||
return 'pause'
|
||||
}
|
||||
return 'disabled'
|
||||
})
|
||||
const totalUploadProgress = computed(() => {
|
||||
const successCount = uploadfiles.value.reduce((acc, curr) => {
|
||||
const { status, uploadInfo } = curr || {}
|
||||
if (status === 'finish') return acc
|
||||
const { chunks } = uploadInfo || {}
|
||||
return acc + Object.entries(chunks || {}).filter(([index, chunk]) => chunk.status === 'success').length
|
||||
}, 0)
|
||||
const totalCount = uploadfiles.value.reduce((acc, curr) => {
|
||||
const { status, uploadInfo } = curr || {}
|
||||
if (status === 'finish') return acc
|
||||
const { chunkLength } = uploadInfo || {}
|
||||
return acc + (chunkLength || 0)
|
||||
}, 0)
|
||||
return ((successCount || 0) / (totalCount || 0)) * 100
|
||||
})
|
||||
|
||||
const counter = useInterval(1000)
|
||||
const speedChartData = ref<Record<number, { createdAt: number; value: number }[]>>({})
|
||||
watch(counter, () => {
|
||||
const speed = uploadfiles.value?.flatMap((item) => {
|
||||
const { chunks, ChunkSize } = item?.uploadInfo || {}
|
||||
return Object.entries(chunks || {})
|
||||
.filter(([index, chunk]) => chunk.status === 'success' && dayjs().unix() - 60 < chunk.createdAt)
|
||||
?.map(([index, chunk]) => {
|
||||
const { createdAt } = chunk || {}
|
||||
return {
|
||||
createdAt,
|
||||
value: ChunkSize || 0,
|
||||
}
|
||||
})
|
||||
})
|
||||
speedChartData.value = groupBy(speed, 'createdAt')
|
||||
})
|
||||
const speedChartData = ref<Record<number, { fileId: string; index: number; value: number }[]>>({})
|
||||
|
||||
onMounted(() => {
|
||||
props.data.file.forEach((file) => {
|
||||
@@ -136,10 +76,10 @@ const { error, execute, isLoading } = useAsyncState(
|
||||
|
||||
if (queueType === 'async') {
|
||||
if (!!retry && retry >= 3) {
|
||||
toast.error('上传错误', {
|
||||
description: `文件 ${file?.file?.name} 的${index}分块经过多次尝试依然上传失败, 我们已经终止该文件上传`,
|
||||
toast.error(t('page.progress.file.uploadError'), {
|
||||
description: t('page.progress.file.chunkUploadFailed', [file?.file?.name, index]),
|
||||
})
|
||||
uploadfiles.value[uploadFileIndex].status = 'error'
|
||||
uploadfiles.value[uploadFileIndex]!.status = 'error'
|
||||
return
|
||||
}
|
||||
uploadfiles.value[uploadFileIndex]?.queue.shift()
|
||||
@@ -170,14 +110,14 @@ const { error, execute, isLoading } = useAsyncState(
|
||||
// todo 重新加入队列
|
||||
if (queueType === 'async') {
|
||||
uploadfiles.value[uploadFileIndex]?.queue.push({ ...task, retry: (task?.retry || 0) + 1 })
|
||||
toast.warning('上传错误', {
|
||||
description: `文件 ${file?.file?.name} 的${index}分块上传失败, 我们将在稍后再次尝试上传`,
|
||||
toast.warning(t('page.progress.file.uploadError'), {
|
||||
description: t('page.progress.file.chunkUploadRetry', [file?.file?.name, index]),
|
||||
})
|
||||
}
|
||||
if (queueType === 'sync') {
|
||||
uploadfiles.value[uploadFileIndex].status = 'error'
|
||||
toast.error('上传错误', {
|
||||
description: `文件${file?.file?.name}上传失败, 请重试`,
|
||||
uploadfiles.value[uploadFileIndex]!.status = 'error'
|
||||
toast.error(t('page.progress.file.uploadError'), {
|
||||
description: t('page.progress.file.fileUploadFailed', [file?.file?.name]),
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
@@ -218,6 +158,7 @@ const handleCreate = async (fileId: string) => {
|
||||
id: string
|
||||
type: 'init' | 'already'
|
||||
chunk_size: number
|
||||
chunks: number[]
|
||||
}
|
||||
}>('/api/file/create', {
|
||||
method: 'POST',
|
||||
@@ -227,10 +168,10 @@ const handleCreate = async (fileId: string) => {
|
||||
hash,
|
||||
},
|
||||
})
|
||||
const { id, chunk_size, type: createType } = createData?.data || {}
|
||||
const { id, chunk_size, type: createType, chunks = [] } = createData?.data || {}
|
||||
uploadfile.id = id
|
||||
uploadfile.uploadInfo = {
|
||||
chunks: {},
|
||||
chunks: Object.fromEntries(chunks.map((index: number) => [index - 1, { status: 'success', createdAt: dayjs().unix() }])),
|
||||
chunkLength: Math.ceil(size / chunk_size),
|
||||
ChunkSize: chunk_size,
|
||||
}
|
||||
@@ -277,17 +218,18 @@ const handleUpload = async (fileId: string, index: number) => {
|
||||
formData.append('file', new Blob([chunk]))
|
||||
formData.append('index', (index + 1).toString())
|
||||
formData.append('id', id)
|
||||
const res = await $fetch<{
|
||||
code: number
|
||||
}>('/api/file/slice', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
const { data: res } = await axios.post('/api/file/slice', formData, {
|
||||
onUploadProgress: throttle((progressEvent) => {
|
||||
const { rate } = progressEvent || {}
|
||||
const timestamp = dayjs().unix()
|
||||
speedChartData.value[timestamp] = [...(speedChartData.value[timestamp] || []), { fileId, index, value: rate || 0 }]
|
||||
}, 1000),
|
||||
})
|
||||
const { code } = res || {}
|
||||
if (code !== 200) {
|
||||
throw new Error('上传失败')
|
||||
throw new Error(t('page.progress.file.uploadFailed'))
|
||||
}
|
||||
uploadfile.uploadInfo!.chunks[index].status = 'success'
|
||||
uploadfile.uploadInfo!.chunks![index]!.status = 'success'
|
||||
if (Object.entries(uploadfile.uploadInfo!.chunks || {}).filter(([index, chunk]) => chunk.status === 'success').length === chunkLength) {
|
||||
uploadfile.queue.push({ taskType: 'finish', queueType: 'sync', taskId: nanoid() })
|
||||
}
|
||||
@@ -307,7 +249,7 @@ const handleFinish = async (fileId: string) => {
|
||||
},
|
||||
})
|
||||
if (res?.code !== 200) {
|
||||
throw new Error('上传失败')
|
||||
throw new Error(t('page.progress.file.uploadFailed'))
|
||||
}
|
||||
uploadfile.status = 'finish'
|
||||
}
|
||||
@@ -341,102 +283,29 @@ const handleShowSpeedInfo = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid grid-cols-4 gap-5">
|
||||
<div class="rounded-xl p-3 bg-white/80 flex flex-col gap-2 col-span-4 md:col-span-3 h-32 md:h-auto">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div @click="handleShowSpeedInfo" class="flex flex-row gap-1 items-center text-xs opacity-70">
|
||||
总上传进度
|
||||
<LucideInfo class="size-3" />
|
||||
</div>
|
||||
<div class="text-2xl font-bold">
|
||||
{{
|
||||
`${
|
||||
getFileSize(
|
||||
Object.entries(speedChartData)
|
||||
?.filter((r) => dayjs().unix() - 60 < parseInt(r[0]))
|
||||
?.reduce((acc, curr) => acc + curr[1]?.reduce((_acc, _curr) => _acc + _curr.value, 0), 0) / 60
|
||||
) ?? 0
|
||||
}/s`
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 relative overflow-hidden flex flex-row gap-0.5 justify-end items-end">
|
||||
<motion.div
|
||||
class="w-2 shrink-0 bg-primary relative"
|
||||
:style="{
|
||||
height: `${(i[1]?.reduce((acc, curr) => acc + curr.value, 0) / Math.max(...Object.entries(speedChartData)?.map((r) => r[1]?.reduce((acc, curr) => acc + curr.value, 0)))) * 100}%`,
|
||||
}"
|
||||
:layoutId="i[0]"
|
||||
v-for="i in Object.entries(speedChartData)"
|
||||
:key="i[0]"
|
||||
:initial="{ x: 10, opacity: 0 }"
|
||||
:animate="{ x: 0, opacity: 1 }"
|
||||
:transition="{ duration: 1 }"
|
||||
>
|
||||
</motion.div>
|
||||
<!-- <BarChart class="h-full" :data="data" index="time" :categories="['value']" :showTooltip="false"
|
||||
:showLegend="false" :showXAxis="false" :showYAxis="false" :showGrid="false" :groupMaxWidth="10" /> -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-xl col-span-4 md:col-span-1 bg-white/80 h-32 md:h-auto md:aspect-square flex flex-col gap-2 relative overflow-hidden">
|
||||
<div class="absolute top-0 left-0 w-full h-full z-[0] flex flex-col justify-end">
|
||||
<div class="w-full bg-green-100 border-t border-green-500" :style="`height: ${totalUploadProgress || 0}%`"></div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 justify-between p-3 h-full relative z-[1]">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="text-xs opacity-70">总上传进度</div>
|
||||
<div class="text-4xl font-bold">{{ (totalUploadProgress || 0).toFixed(1) }}%</div>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2">
|
||||
<Button
|
||||
class="aspect-square hover:text-white p-0 bg-green-200 hover:bg-green-300 text-green-500"
|
||||
:disabled="['start', 'disabled'].includes(totalTaskStatus)"
|
||||
@click="
|
||||
() => {
|
||||
uploadfiles.forEach((r) => {
|
||||
r.status = 'start'
|
||||
})
|
||||
}
|
||||
"
|
||||
>
|
||||
<LucidePlay class="size-1/2" />
|
||||
</Button>
|
||||
<Button
|
||||
class="aspect-square hover:text-white p-0 bg-orange-200 hover:bg-orange-300 text-orange-500"
|
||||
:disabled="['pause', 'disabled'].includes(totalTaskStatus)"
|
||||
@click="
|
||||
() => {
|
||||
uploadfiles.forEach((r) => {
|
||||
r.status = 'pause'
|
||||
})
|
||||
}
|
||||
"
|
||||
>
|
||||
<LucideSquare class="size-1/2" />
|
||||
</Button>
|
||||
<!-- <Button class="aspect-square bg-blue-200 hover:bg-blue-300 text-blue-500 hover:text-white p-0">
|
||||
<LucideSettings class="size-1/2" />
|
||||
</Button> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<BaseCard class="grid grid-cols-4 gap-5">
|
||||
<FileUploadTotalSpeedView :speedChartData="speedChartData" />
|
||||
<FileUploadTotalProgressControlView :uploadfiles="uploadfiles" />
|
||||
<div class="col-span-4 flex flex-col bg-white/80 rounded-xl p-3 text-md gap-5">
|
||||
<div>文件列表</div>
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<LucideFolders class="size-4" />
|
||||
{{ t('page.progress.file.fileList') }}
|
||||
</div>
|
||||
<div class="flex flex-col -mx-3 text-sm">
|
||||
<div class="grid grid-cols-[2fr_6rem_6rem] md:grid-cols-[2fr_6rem_6rem_4fr] gap-2 border-b border-black/20 pb-2 px-3">
|
||||
<div>文件名</div>
|
||||
<div>文件大小</div>
|
||||
<div>{{ t('page.progress.file.fileName') }}</div>
|
||||
<div>{{ t('page.progress.file.fileSize') }}</div>
|
||||
<div @click="handleShowSpeedInfo" class="flex flex-row gap-1 items-center">
|
||||
上传速度
|
||||
{{ t('page.progress.file.uploadSpeed') }}
|
||||
<LucideInfo class="size-3" />
|
||||
</div>
|
||||
<div class="hidden md:block">进度</div>
|
||||
<div class="hidden md:block">{{ t('page.progress.file.progress') }}</div>
|
||||
</div>
|
||||
<div
|
||||
:class="
|
||||
cx(
|
||||
'grid grid-cols-[2fr_6rem_6rem] md:grid-cols-[2fr_6rem_6rem_4fr] gap-2 py-2 border-b border-black/20 items-center hover:bg-primary/30 px-3 cursor-pointer',
|
||||
selectedFile === item?.fileId && 'bg-primary text-white hover:!bg-primary'
|
||||
selectedFile === item?.fileId && 'bg-primary text-white hover:bg-primary!'
|
||||
)
|
||||
"
|
||||
v-for="(item, index) in uploadfiles"
|
||||
@@ -459,9 +328,9 @@ const handleShowSpeedInfo = () => {
|
||||
(e: Event) => {
|
||||
e.stopPropagation()
|
||||
if (item?.status === 'start') {
|
||||
uploadfiles[index].status = 'pause'
|
||||
uploadfiles[index]!.status = 'pause'
|
||||
} else {
|
||||
uploadfiles[index].status = 'start'
|
||||
uploadfiles[index]!.status = 'start'
|
||||
}
|
||||
}
|
||||
"
|
||||
@@ -475,26 +344,26 @@ const handleShowSpeedInfo = () => {
|
||||
</div>
|
||||
<div>{{ getFileSize(item?.file?.size) }}</div>
|
||||
<div>
|
||||
{{
|
||||
`${getFileSize(
|
||||
(Object.entries(item?.uploadInfo?.chunks || {})?.filter(
|
||||
([, chunk]) => chunk.status === 'success' && dayjs().unix() - 60 < chunk.createdAt
|
||||
)?.length /
|
||||
60) *
|
||||
(item?.uploadInfo?.ChunkSize || 0)
|
||||
)} /s`
|
||||
}}
|
||||
<template v-if="item?.status === 'start' && item?.procressType === 'upload'">
|
||||
{{
|
||||
`${getFileSize(
|
||||
Object.entries(speedChartData)
|
||||
?.filter(([key, value]) => value.some((r) => r.fileId === item?.fileId) && dayjs().unix() - 1 === Number(key))
|
||||
?.reduce((acc, curr) => acc + curr[1]?.reduce((acc, curr) => acc + curr.value, 0), 0) ?? 0
|
||||
)} /s`
|
||||
}}
|
||||
</template>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-row gap-2 items-center col-span-3 md:col-span-1"
|
||||
v-if="['hash', 'create', 'chunk']?.includes(item?.procressType)"
|
||||
>
|
||||
<LucideLoaderCircle class="size-4 animate-spin" />
|
||||
<div>正在{{ item?.procressType }}中...</div>
|
||||
<div>{{ t(`page.progress.file.processing.${item?.procressType}`) }}</div>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 items-center col-span-3 md:col-span-1" v-if="item?.procressType === 'finish'">
|
||||
{{ item?.status === 'finish' ? '云端已有相同hash文件, 秒传成功' : null }}
|
||||
{{ item?.status === 'error' ? '上传失败,请稍后重试' : null }}
|
||||
{{ item?.status === 'finish' ? t('page.progress.file.instantUploadSuccess') : null }}
|
||||
{{ item?.status === 'error' ? t('page.progress.file.uploadFailedRetry') : null }}
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 items-center col-span-3 md:col-span-1" v-if="item?.procressType === 'upload'">
|
||||
<div class="rounded-full bg-white/50 w-full h-2 overflow-hidden border border-white">
|
||||
@@ -526,44 +395,6 @@ const handleShowSpeedInfo = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-4 flex flex-col bg-white/80 rounded-xl p-3 gap-5" v-if="selectedFile">
|
||||
<div>上传详情</div>
|
||||
<div class="grid grid-cols-3 text-sm gap-3">
|
||||
<div>
|
||||
区块: {{ selectedUploadfile?.uploadInfo?.chunkLength }} x {{ getFileSize(selectedUploadfile?.uploadInfo?.ChunkSize as number) }}
|
||||
</div>
|
||||
<div class="truncate col-span-2">hash: {{ selectedUploadfile?.hash }}</div>
|
||||
<div>已完成: {{ selectedUploadfileChunk?.filter((r) => r.status === 'success')?.length || 0 }}</div>
|
||||
<div>已丢弃: {{ selectedUploadfileChunk?.filter((r) => r.status === 'error')?.length || 0 }}</div>
|
||||
<div>
|
||||
待完成:
|
||||
{{ (selectedUploadfile?.uploadInfo?.chunkLength || 0) - (selectedUploadfileChunk?.length || 0) }}
|
||||
</div>
|
||||
<div class="col-span-3 flex flex-row justify-between items-center">
|
||||
<div class="text-md font-bold">{{ selectedUploadfileViewMode === 'progress' ? '区块进度条' : '区块热力图' }}</div>
|
||||
<Button
|
||||
size="sm"
|
||||
class="ml-auto text-xs"
|
||||
@click="selectedUploadfileViewMode = selectedUploadfileViewMode === 'progress' ? 'heatmap' : 'progress'"
|
||||
>
|
||||
<LucideArrowDownUp class="size-4" />
|
||||
{{ selectedUploadfileViewMode === 'progress' ? '热力图' : '进度条' }}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="h-7 col-span-3 flex flex-row gap-2 items-center" v-if="selectedUploadfileViewMode === 'progress'">
|
||||
<div class="flex-1 h-full">
|
||||
<FileUploadBlockProgressView :data="selectedUploadfile?.uploadInfo" />
|
||||
</div>
|
||||
{{
|
||||
clamp(((selectedUploadfileChunk?.length || 0) / (selectedUploadfile?.uploadInfo?.chunkLength || 0)) * 100, 0, 100)?.toFixed(
|
||||
2
|
||||
)
|
||||
}}%
|
||||
</div>
|
||||
<div v-if="selectedUploadfileViewMode === 'heatmap'" class="col-span-3 bg-black/5 rounded p-2">
|
||||
<FileUploadHeatMapView :size="12" :data="selectedUploadfile?.uploadInfo" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<FileUploadDetailView :uploadfiles="uploadfiles" :selectedFile="selectedFile" />
|
||||
</BaseCard>
|
||||
</template>
|
||||
24
front/components/Home/File/FileUploadProgressView/types.ts
Normal file
24
front/components/Home/File/FileUploadProgressView/types.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
type Uploadfile = {
|
||||
fileId: string
|
||||
id?: string // 后端文件id
|
||||
file: File
|
||||
status: 'start' | 'pause' | 'finish' | 'error'
|
||||
hash?: string
|
||||
procressType: 'hash' | 'create' | 'upload' | 'finish'
|
||||
uploadInfo?: {
|
||||
chunks: Record<number, { status: 'success' | 'error' | 'processing'; createdAt: number }>
|
||||
chunkLength: number
|
||||
ChunkSize: number
|
||||
}
|
||||
queue: {
|
||||
taskId: string
|
||||
taskType: 'hash' | 'create' | 'chunk' | 'upload' | 'finish'
|
||||
queueType: 'sync' | 'async' // sync任务禁止并发
|
||||
index?: number
|
||||
retry?: number
|
||||
}[]
|
||||
}
|
||||
|
||||
type SelectedFile = string | null
|
||||
|
||||
export type { Uploadfile, SelectedFile }
|
||||
@@ -1,8 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-2 pt-3">
|
||||
<div class="text-xl font-bold">上传速度如何计算</div>
|
||||
<div class="opacity-75">
|
||||
上传速度根据当前秒上传了 <b>文件区块的数量</b> * <b>每个文件区块的大小</b> 估算而来,可能与真实上传速度有一定的误差,仅供参考
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -14,19 +14,26 @@ const renderComponent = computed(() => {
|
||||
return textStepList.find((item) => item.key === step.value)?.component
|
||||
})
|
||||
const formRef = ref<InstanceType<typeof VeeForm>>()
|
||||
watch(() => step.value, (newVal) => {
|
||||
if (newVal === 'input') {
|
||||
formRef.value?.form?.resetForm()
|
||||
// formRef.value?.form?.setValues({ file: null })
|
||||
watch(
|
||||
() => step.value,
|
||||
(newVal) => {
|
||||
if (newVal === 'input') {
|
||||
formRef.value?.form?.resetForm()
|
||||
// formRef.value?.form?.setValues({ file: null })
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
</script>
|
||||
<template>
|
||||
<VeeForm ref="formRef" v-slot="{ values }" :keepValues="true">
|
||||
<div class="rounded-xl p-5 bg-white/50 backdrop-blur-xl w-full lg:w-200">
|
||||
<component :is="renderComponent" :data="values" @change="(key: string) => {
|
||||
step = key
|
||||
}" />
|
||||
</div>
|
||||
<component
|
||||
:is="renderComponent"
|
||||
:data="values"
|
||||
@change="
|
||||
(key: string) => {
|
||||
step = key
|
||||
}
|
||||
"
|
||||
/>
|
||||
</VeeForm>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -21,13 +21,12 @@ const handleTextShare = ({ type, config }: { type: string; config: any }) => {
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="gap-5 flex flex-col">
|
||||
<div class="text-xl font-normal">{{ t('text.uploadText') }}</div>
|
||||
<BaseCard class="gap-5 flex flex-col" :title="t('page.upload.text.uploadText')">
|
||||
<div class="relative">
|
||||
<MarkdownInputField
|
||||
name="text"
|
||||
:placeholder="t('text.uploadTextPlaceholder')"
|
||||
class="max-h-[50vh] min-h-40 overflow-y-auto max-w-full [&>*]:pr-10 flex flex-col"
|
||||
:placeholder="t('page.upload.text.uploadTextPlaceholder')"
|
||||
class="max-h-[50vh] min-h-40 overflow-y-auto max-w-full *:pr-10 flex flex-col"
|
||||
rules="required"
|
||||
/>
|
||||
<Button
|
||||
@@ -68,5 +67,5 @@ const handleTextShare = ({ type, config }: { type: string; config: any }) => {
|
||||
</FormButton>
|
||||
<PickupShareBtn />
|
||||
</div>
|
||||
</div>
|
||||
</BaseCard>
|
||||
</template>
|
||||
|
||||
@@ -16,6 +16,6 @@ const { t } = useI18n()
|
||||
"
|
||||
>
|
||||
<LucideArchive class="size-4" />
|
||||
{{ t('pickup.btn') }}
|
||||
{{ t('page.upload.pickup.btn') }}
|
||||
</AsyncButton>
|
||||
</template>
|
||||
|
||||
@@ -15,37 +15,37 @@ const props = defineProps<{
|
||||
<template>
|
||||
<VeeForm v-slot="{ values, setFieldValue }" :initialValues="{ download_nums: 1, expire_time: 1440 }">
|
||||
<div class="flex flex-col gap-3">
|
||||
<h2 class="text-lg font-bold">{{ t('fileshare.title') }}</h2>
|
||||
<h2 class="text-lg font-bold">{{ t('page.shareOptions.file.title') }}</h2>
|
||||
<div class="flex flex-row items-center gap-2 text-sm">
|
||||
<SelectField
|
||||
name="download_nums"
|
||||
:label="t('fileshare.downloadNums')"
|
||||
:label="t('page.shareOptions.file.downloadNums')"
|
||||
:options="[
|
||||
{ label: t('fileshare.downloadOptions.1time'), value: 1 },
|
||||
{ label: t('fileshare.downloadOptions.2times'), value: 2 },
|
||||
{ label: t('fileshare.downloadOptions.3times'), value: 3 },
|
||||
{ label: t('fileshare.downloadOptions.5times'), value: 5 },
|
||||
{ label: t('fileshare.downloadOptions.10times'), value: 10 },
|
||||
{ label: t('page.shareOptions.file.downloadOptions.xdownload', [1]), value: 1 },
|
||||
{ label: t('page.shareOptions.file.downloadOptions.xdownload', [2]), value: 2 },
|
||||
{ label: t('page.shareOptions.file.downloadOptions.xdownload', [3]), value: 3 },
|
||||
{ label: t('page.shareOptions.file.downloadOptions.xdownload', [5]), value: 5 },
|
||||
{ label: t('page.shareOptions.file.downloadOptions.xdownload', [10]), value: 10 },
|
||||
]"
|
||||
/>
|
||||
{{ t('fileshare.or') }}
|
||||
{{ t('page.shareOptions.file.or') }}
|
||||
<SelectField
|
||||
name="expire_time"
|
||||
:label="t('fileshare.expireTime')"
|
||||
:label="t('page.shareOptions.file.expireTime')"
|
||||
:options="[
|
||||
{ label: t('fileshare.expireOptions.5min'), value: 5 },
|
||||
{ label: t('fileshare.expireOptions.1hour'), value: 60 },
|
||||
{ label: t('fileshare.expireOptions.1day'), value: 1440 },
|
||||
{ label: t('fileshare.expireOptions.3days'), value: 4320 },
|
||||
{ label: t('page.shareOptions.file.expireOptions.5min'), value: 5 },
|
||||
{ label: t('page.shareOptions.file.expireOptions.1hour'), value: 60 },
|
||||
{ label: t('page.shareOptions.file.expireOptions.1day'), value: 1440 },
|
||||
{ label: t('page.shareOptions.file.expireOptions.3days'), value: 4320 },
|
||||
]"
|
||||
/>
|
||||
{{ t('fileshare.expireAfter') }}
|
||||
{{ t('page.shareOptions.file.expireAfter') }}
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex flex-row gap-3 min-h-9">
|
||||
<SwitchField
|
||||
name="has_pickup_code"
|
||||
:label="t('fileshare.pickupCode')"
|
||||
:label="t('page.shareOptions.file.pickupCode')"
|
||||
:rules="
|
||||
(value: boolean) => {
|
||||
if (!!value) {
|
||||
@@ -59,7 +59,7 @@ const props = defineProps<{
|
||||
<div class="flex flex-row gap-3 min-h-9">
|
||||
<SwitchField
|
||||
name="has_password"
|
||||
:label="t('fileshare.passwordProtection')"
|
||||
:label="t('page.shareOptions.file.passwordProtection')"
|
||||
:rules="
|
||||
(value: boolean) => {
|
||||
if (!!value) {
|
||||
@@ -69,11 +69,21 @@ const props = defineProps<{
|
||||
}
|
||||
"
|
||||
/>
|
||||
<InputField v-if="!!values.has_password" name="password" :placeholder="t('fileshare.passwordPlaceholder')" rules="required" />
|
||||
<InputField
|
||||
v-if="!!values.has_password"
|
||||
name="password"
|
||||
:placeholder="t('page.shareOptions.file.passwordPlaceholder')"
|
||||
rules="required"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-row gap-3 min-h-9">
|
||||
<SwitchField name="has_notify" :label="t('fileshare.downloadNotify')" />
|
||||
<InputField v-if="!!values.has_notify" name="notify_email" :placeholder="t('fileshare.emailPlaceholder')" rules="required" />
|
||||
<SwitchField name="has_notify" :label="t('page.shareOptions.file.downloadNotify')" />
|
||||
<InputField
|
||||
v-if="!!values.has_notify"
|
||||
name="notify_email"
|
||||
:placeholder="t('page.shareOptions.file.emailPlaceholder')"
|
||||
rules="required"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<FormButton
|
||||
|
||||
43
front/components/Preprocessing/ImageConvertHandle.vue
Normal file
43
front/components/Preprocessing/ImageConvertHandle.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import SelectField from '../Field/SelectField.vue'
|
||||
import FormButton from '../Field/FormButton.vue'
|
||||
import type { FileShareHandleProps } from './types'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
const props = defineProps<{
|
||||
hide: () => void
|
||||
file: File[]
|
||||
onFileHandle: (props: FileShareHandleProps) => void
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VeeForm v-slot="{ values }" :initialValues="{ target_ext: 'jpg' }">
|
||||
<div class="flex flex-col gap-3">
|
||||
<h2 class="text-lg font-bold">{{ t('page.shareOptions.imageConvert.title') }}</h2>
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<Label>{{ t('page.shareOptions.imageConvert.targetFormat') }}</Label>
|
||||
<SelectField
|
||||
name="target_ext"
|
||||
:label="t('page.shareOptions.imageConvert.targetFormat')"
|
||||
:options="[
|
||||
{ label: 'JPG', value: 'jpg' },
|
||||
{ label: 'PNG', value: 'png' },
|
||||
{ label: 'WEBP', value: 'webp' },
|
||||
]"
|
||||
rules="required"
|
||||
/>
|
||||
</div>
|
||||
<FormButton
|
||||
@click="
|
||||
async (form) => {
|
||||
onFileHandle({ type: 'file-image-convert', config: values })
|
||||
hide()
|
||||
}
|
||||
"
|
||||
>{{ t('btn.submit') }}</FormButton
|
||||
>
|
||||
</div>
|
||||
</VeeForm>
|
||||
</template>
|
||||
@@ -15,37 +15,37 @@ const props = defineProps<{
|
||||
<template>
|
||||
<VeeForm v-slot="{ values, setFieldValue }" :initialValues="{ download_nums: 1, expire_time: 1440 }">
|
||||
<div class="flex flex-col gap-3">
|
||||
<h2 class="text-lg font-bold">{{ t('textshare.title') }}</h2>
|
||||
<h2 class="text-lg font-bold">{{ t('page.shareOptions.text.title') }}</h2>
|
||||
<div class="flex flex-row items-center gap-2 text-sm">
|
||||
<SelectField
|
||||
name="download_nums"
|
||||
:label="t('textshare.viewNums')"
|
||||
:label="t('page.shareOptions.text.viewNums')"
|
||||
:options="[
|
||||
{ label: t('textshare.viewOptions.1time'), value: 1 },
|
||||
{ label: t('textshare.viewOptions.2times'), value: 2 },
|
||||
{ label: t('textshare.viewOptions.3times'), value: 3 },
|
||||
{ label: t('textshare.viewOptions.5times'), value: 5 },
|
||||
{ label: t('textshare.viewOptions.10times'), value: 10 },
|
||||
{ label: t('page.shareOptions.text.viewOptions.xview', [1]), value: 1 },
|
||||
{ label: t('page.shareOptions.text.viewOptions.xview', [2]), value: 2 },
|
||||
{ label: t('page.shareOptions.text.viewOptions.xview', [3]), value: 3 },
|
||||
{ label: t('page.shareOptions.text.viewOptions.xview', [5]), value: 5 },
|
||||
{ label: t('page.shareOptions.text.viewOptions.xview', [10]), value: 10 },
|
||||
]"
|
||||
/>
|
||||
{{ t('textshare.or') }}
|
||||
{{ t('page.shareOptions.text.or') }}
|
||||
<SelectField
|
||||
name="expire_time"
|
||||
:label="t('textshare.expireTime')"
|
||||
:label="t('page.shareOptions.text.expireTime')"
|
||||
:options="[
|
||||
{ label: t('textshare.expireOptions.5min'), value: 5 },
|
||||
{ label: t('textshare.expireOptions.1hour'), value: 60 },
|
||||
{ label: t('textshare.expireOptions.1day'), value: 1440 },
|
||||
{ label: t('textshare.expireOptions.3days'), value: 4320 },
|
||||
{ label: t('page.shareOptions.text.expireOptions.5min'), value: 5 },
|
||||
{ label: t('page.shareOptions.text.expireOptions.1hour'), value: 60 },
|
||||
{ label: t('page.shareOptions.text.expireOptions.1day'), value: 1440 },
|
||||
{ label: t('page.shareOptions.text.expireOptions.3days'), value: 4320 },
|
||||
]"
|
||||
/>
|
||||
{{ t('textshare.expireAfter') }}
|
||||
{{ t('page.shareOptions.text.expireAfter') }}
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex flex-row gap-3 min-h-9">
|
||||
<SwitchField
|
||||
name="has_pickup_code"
|
||||
:label="t('textshare.pickupCode')"
|
||||
:label="t('page.shareOptions.text.pickupCode')"
|
||||
:rules="
|
||||
(value: boolean) => {
|
||||
if (!!value) {
|
||||
@@ -59,7 +59,7 @@ const props = defineProps<{
|
||||
<div class="flex flex-row gap-3 min-h-9">
|
||||
<SwitchField
|
||||
name="has_password"
|
||||
:label="t('textshare.passwordProtection')"
|
||||
:label="t('page.shareOptions.text.passwordProtection')"
|
||||
:rules="
|
||||
(value: boolean) => {
|
||||
if (!!value) {
|
||||
@@ -69,11 +69,21 @@ const props = defineProps<{
|
||||
}
|
||||
"
|
||||
/>
|
||||
<InputField v-if="!!values.has_password" name="password" :placeholder="t('textshare.passwordPlaceholder')" rules="required" />
|
||||
<InputField
|
||||
v-if="!!values.has_password"
|
||||
name="password"
|
||||
:placeholder="t('page.shareOptions.text.passwordPlaceholder')"
|
||||
rules="required"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-row gap-3 min-h-9">
|
||||
<SwitchField name="has_notify" :label="t('textshare.readNotify')" />
|
||||
<InputField v-if="!!values.has_notify" name="notify_email" :placeholder="t('textshare.emailPlaceholder')" rules="required" />
|
||||
<SwitchField name="has_notify" :label="t('page.shareOptions.text.readNotify')" />
|
||||
<InputField
|
||||
v-if="!!values.has_notify"
|
||||
name="notify_email"
|
||||
:placeholder="t('page.shareOptions.text.emailPlaceholder')"
|
||||
rules="required"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<FormButton
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type FileHandleKey = 'file-share' | 'file-image-compress'
|
||||
export type FileHandleKey = 'file-share' | 'file-image-compress' | 'file-image-convert'
|
||||
export type FileShareHandleProps = { type: FileHandleKey; config: Record<string, any> }
|
||||
|
||||
export type TextHandleKey = 'text-share'
|
||||
|
||||
@@ -12,11 +12,9 @@ import showDrawer from '@/lib/showDrawer'
|
||||
import QrCoreDrawer from '@/components/Drawer/QrCoreDrawer.vue'
|
||||
import { h } from 'vue'
|
||||
import { cx } from 'class-variance-authority'
|
||||
import type { FileHandleKey } from '../Preprocessing/types'
|
||||
import type { handleFileComponentProps } from './types'
|
||||
|
||||
const props = defineProps<{
|
||||
data: { files: { id: string; file: File }[]; config: Record<string, any>; handle_type: FileHandleKey }
|
||||
}>()
|
||||
const props = defineProps<handleFileComponentProps>()
|
||||
const emit = defineEmits<{
|
||||
(e: 'change', key: string): void
|
||||
}>()
|
||||
@@ -59,14 +57,13 @@ const { copy } = useClipboard()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-3">
|
||||
<h2 class="text-lg">{{ t('fileshareresult.title') }}</h2>
|
||||
<BaseCard class="flex flex-col gap-3" :title="t('page.result.file.title')" :showBackButton="true">
|
||||
<div class="flex flex-col gap-3 items-center">
|
||||
<div v-if="data?.length === 1" class="flex flex-col h-30 items-center">
|
||||
<FilePreviewView :value="props?.data?.files?.[0]?.file as File" />
|
||||
</div>
|
||||
<div v-else class="flex flex-col gap-2 w-full p-5 bg-white/20 backdrop-blur-xl rounded-md">
|
||||
<div class="text-sm font-semibold">{{ t('fileshareresult.fileList') }}</div>
|
||||
<div class="text-sm font-semibold">{{ t('page.result.file.fileList') }}</div>
|
||||
<div
|
||||
v-for="file in data"
|
||||
:class="
|
||||
@@ -92,7 +89,7 @@ const { copy } = useClipboard()
|
||||
@click="
|
||||
() => {
|
||||
copy(getShareUrl(file?.id as string))
|
||||
toast.success(t('fileshareresult.copySuccess'))
|
||||
toast.success(t('page.result.file.copySuccess'))
|
||||
}
|
||||
"
|
||||
>
|
||||
@@ -121,21 +118,21 @@ const { copy } = useClipboard()
|
||||
</div>
|
||||
<div v-if="!!selectedFileShare" class="flex flex-col md:flex-row gap-5 rounded-md p-5 bg-white/20 backdrop-blur-xl w-full">
|
||||
<div class="flex flex-col gap-2 flex-1">
|
||||
<div class="text-sm font-semibold">{{ t('fileshareresult.info') }}</div>
|
||||
<div class="text-sm font-semibold">{{ t('page.result.file.info') }}</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div class="rounded-xl flex flex-col bg-black/10 px-3 py-2 gap-1">
|
||||
<div class="text-xs font-semibold">{{ t('fileshareresult.downloadNums') }}</div>
|
||||
<div class="text-xs font-semibold">{{ t('page.result.file.downloadNums') }}</div>
|
||||
<div class="text-3xl font-light">{{ selectedFileShare?.download_nums }}</div>
|
||||
</div>
|
||||
<div class="rounded-xl flex flex-col bg-black/10 px-3 py-2 gap-1">
|
||||
<div class="text-xs font-semibold">{{ t('fileshareresult.expireTime') }}</div>
|
||||
<div class="text-xs font-semibold">{{ t('page.result.file.expireTime') }}</div>
|
||||
<div class="text-md font-light">
|
||||
{{ dayjs((selectedFileShare?.expire_at || 0) * 1000).format('YYYY-MM-DD HH:mm:ss') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-xl flex flex-col bg-black/10 px-3 py-2 gap-1" v-if="selectedFileShare?.pickup_code">
|
||||
<div class="flex flex-row justify-between w-full items-center">
|
||||
<div class="text-xs font-semibold">{{ t('fileshareresult.pickupCode') }}</div>
|
||||
<div class="text-xs font-semibold">{{ t('page.result.file.pickupCode') }}</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
class="bg-white/70 p-0 size-6"
|
||||
@@ -143,7 +140,7 @@ const { copy } = useClipboard()
|
||||
@click="
|
||||
() => {
|
||||
copy(selectedFileShare?.pickup_code as string)
|
||||
toast.success(t('fileshareresult.copySuccess'))
|
||||
toast.success(t('page.result.file.copySuccess'))
|
||||
}
|
||||
"
|
||||
>
|
||||
@@ -159,7 +156,7 @@ const { copy } = useClipboard()
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-5 flex-1">
|
||||
<div class="text-sm font-semibold">{{ t('fileshareresult.link') }}</div>
|
||||
<div class="text-sm font-semibold">{{ t('page.result.file.link') }}</div>
|
||||
<div class="flex flex-row gap-2">
|
||||
<Input :model-value="getShareUrl(selectedFileShare?.id as string)" class="bg-white/70" readonly />
|
||||
<Button
|
||||
@@ -169,7 +166,7 @@ const { copy } = useClipboard()
|
||||
@click="
|
||||
() => {
|
||||
copy(getShareUrl(selectedFileShare?.id as string))
|
||||
toast.success(t('fileshareresult.copySuccess'))
|
||||
toast.success(t('page.result.file.copySuccess'))
|
||||
}
|
||||
"
|
||||
>
|
||||
@@ -197,16 +194,6 @@ const { copy } = useClipboard()
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
class="w-40 hover:bg-primary/90"
|
||||
@click="
|
||||
() => {
|
||||
emit('change', 'input')
|
||||
}
|
||||
"
|
||||
>
|
||||
{{ t('btn.backToHome') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</BaseCard>
|
||||
</template>
|
||||
|
||||
@@ -1,66 +1,92 @@
|
||||
<script setup lang="ts">
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import { AsyncButton, Button } from '@/components/ui/button'
|
||||
import { useQueries, useQuery } from '@tanstack/vue-query'
|
||||
import { AsyncButton } from '@/components/ui/button'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { filesize } from 'filesize'
|
||||
import useMyAppShare from '@/composables/useMyAppShare'
|
||||
import { toast } from 'vue-sonner'
|
||||
import type { handleFileComponentProps } from './types'
|
||||
import { get } from 'lodash-es'
|
||||
|
||||
const { t } = useI18n()
|
||||
const emit = defineEmits<{
|
||||
(e: 'change', key: string): void
|
||||
}>()
|
||||
const props = defineProps<{
|
||||
data: { file: File; config: any; handle_type: string; file_id: string }
|
||||
}>()
|
||||
|
||||
const { data } = useQuery({
|
||||
queryKey: ['create-image-compress', props?.data?.file_id],
|
||||
const props = defineProps<handleFileComponentProps>()
|
||||
const fileIds = computed(() => props?.data?.files?.map((item) => item.id))
|
||||
const { data: taskIds } = useQuery({
|
||||
queryKey: ['create-image-compress', fileIds.value],
|
||||
queryFn: async () => {
|
||||
const { file_id } = props?.data || {}
|
||||
const data = await $fetch<{
|
||||
code: number
|
||||
data: {
|
||||
id?: string
|
||||
}
|
||||
}>(`/api/image/compress`, {
|
||||
method: 'POST',
|
||||
body: {
|
||||
file_id,
|
||||
},
|
||||
})
|
||||
return data?.data
|
||||
return await Promise.all(
|
||||
props?.data?.files?.map(async (file) => {
|
||||
const { id } = file || {}
|
||||
if (!id) return
|
||||
const data = await $fetch<{
|
||||
code: number
|
||||
data: {
|
||||
id?: string
|
||||
}
|
||||
}>(`/api/task/image:compress`, {
|
||||
method: 'POST',
|
||||
body: {
|
||||
file_id: id,
|
||||
},
|
||||
})
|
||||
return data?.data?.id
|
||||
})
|
||||
)
|
||||
},
|
||||
staleTime: Infinity,
|
||||
enabled: !!fileIds.value && fileIds.value?.length > 0,
|
||||
})
|
||||
|
||||
const taskId = computed(() => data?.value?.id)
|
||||
|
||||
const { data: taskData, refetch } = useQuery({
|
||||
queryKey: ['image-compress-task', taskId],
|
||||
queryFn: async () => {
|
||||
const data = await $fetch<{
|
||||
code: number
|
||||
data: {
|
||||
result: {
|
||||
old_file: {
|
||||
id: string
|
||||
size: number
|
||||
}
|
||||
new_file: {
|
||||
id: string
|
||||
size: number
|
||||
}
|
||||
}[]
|
||||
status: 'success' | 'retry' | 'archived'
|
||||
err?: {
|
||||
message?: string
|
||||
retry?: number
|
||||
max_retry?: number
|
||||
const taskResults = useQueries({
|
||||
queries: computed(
|
||||
() =>
|
||||
taskIds?.value?.filter(Boolean).map((taskId) => {
|
||||
return {
|
||||
queryKey: ['task-image-compress', taskId],
|
||||
queryFn: async () => {
|
||||
const data = await $fetch<{
|
||||
code: number
|
||||
data: {
|
||||
result: {
|
||||
old_file: {
|
||||
id: string
|
||||
size: number
|
||||
}
|
||||
new_file: {
|
||||
id: string
|
||||
size: number
|
||||
}
|
||||
}[]
|
||||
status: 'success' | 'retry' | 'archived'
|
||||
err?: {
|
||||
message?: string
|
||||
retry?: number
|
||||
max_retry?: number
|
||||
}
|
||||
}
|
||||
}>(`/api/task/${taskId}`)
|
||||
return data?.data
|
||||
},
|
||||
enabled: !!taskId,
|
||||
}
|
||||
}) ?? []
|
||||
),
|
||||
})
|
||||
|
||||
const totalSize = computed(() => {
|
||||
return taskResults.value.reduce(
|
||||
(acc, item) => {
|
||||
const { new_file, old_file } = get(item, 'data.result.0') || {}
|
||||
return {
|
||||
oldSize: acc.oldSize + (old_file?.size ?? 0),
|
||||
newSize: acc.newSize + (new_file?.size ?? 0),
|
||||
}
|
||||
}>(`/api/image/compress/${taskId.value}`)
|
||||
return data?.data
|
||||
},
|
||||
enabled: !!taskId.value,
|
||||
},
|
||||
{ oldSize: 0, newSize: 0 }
|
||||
)
|
||||
})
|
||||
|
||||
const { downloadFileByShareId, createFileShare } = useMyAppShare()
|
||||
@@ -70,66 +96,98 @@ const { counter, pause } = useInterval(2000, { controls: true })
|
||||
watch(
|
||||
() => counter.value,
|
||||
() => {
|
||||
if (['success', 'archived'].includes(taskData.value?.status ?? '')) {
|
||||
pause()
|
||||
return
|
||||
}
|
||||
refetch()
|
||||
}
|
||||
)
|
||||
taskResults.value.forEach((item) => {
|
||||
if (['success', 'archived'].includes(item.data?.status ?? '')) return
|
||||
item.refetch()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => taskData.value?.err?.retry,
|
||||
(newVal, oldVal) => {
|
||||
if (!oldVal || !newVal || !taskData.value?.err?.max_retry) {
|
||||
return
|
||||
}
|
||||
if (newVal <= taskData.value?.err?.max_retry) {
|
||||
toast.error(`处理错误: ${taskData.value?.err?.message}, 将再次重试`)
|
||||
if (taskResults.value.every((item) => ['success', 'archived'].includes(item.data?.status ?? ''))) {
|
||||
pause()
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
<template>
|
||||
<div class="flex flex-col gap-3">
|
||||
<h2 class="text-lg">上传成功</h2>
|
||||
<div class="flex flex-col gap-1 items-center">
|
||||
<div class="flex flex-col h-30 items-center justify-center">
|
||||
<FilePreviewView :value="props?.data?.file" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="taskData?.status === 'success'" class="flex flex-col gap-2" v-for="item in taskData?.result">
|
||||
<div class="bg-white/80 p-2 rounded-md w-full flex flex-row items-center justify-between gap-2">
|
||||
<div class="flex flex-row gap-2 items-center max-w-2/3">
|
||||
<div class="flex flex-row items-center justify-center rounded-md bg-black/5 p-2">
|
||||
<LucideImage />
|
||||
</div>
|
||||
<div class="truncate w-auto">{{ props?.data?.file?.name }}</div>
|
||||
<div class="flex flex-row gap-2 items-center text-sm shrink-0">
|
||||
<span class="opacity-75">{{ filesize(item.new_file.size ?? 0) }}</span>
|
||||
<span class="bg-green-200 text-green-600 rounded-md px-1 py-0.5 flex flex-row gap-1 items-center text-xs">
|
||||
<LucideChevronDown class="size-4" />
|
||||
{{ ((1 - item.new_file.size / item.old_file.size) * 100).toFixed(2) }}%
|
||||
</span>
|
||||
<BaseCard class="flex flex-col gap-3" :title="t('page.result.imageCompress.title')" :showBackButton="true">
|
||||
<div class="flex flex-row gap-3">
|
||||
<div class="rounded-xl flex flex-col bg-white/70 px-3 py-2 gap-1 basis-2/3">
|
||||
<div class="text-sm font-semibold">{{ t('page.result.imageCompress.totalSize') }}</div>
|
||||
<div class="text-2xl font-light flex flex-row items-center gap-1">
|
||||
<span v-if="totalSize.oldSize > 0" class="opacity-75">{{ filesize(totalSize.oldSize) }}</span>
|
||||
<span v-else><Skeleton class="w-12 h-10" /></span>
|
||||
<LucideChevronsRight class="size-6" />
|
||||
<span v-if="totalSize.newSize > 0">{{ filesize(totalSize.newSize) }}</span>
|
||||
<span v-else><Skeleton class="w-12 h-10" /></span>
|
||||
<div
|
||||
v-if="totalSize.oldSize > 0 && totalSize.newSize > 0"
|
||||
class="rounded flex flex-row items-center bg-green-100 text-green-600 p-1 py-0.5 text-sm"
|
||||
>
|
||||
<LucideArrowDown class="size-4" />
|
||||
{{ ((1 - totalSize.newSize / totalSize.oldSize) * 100).toFixed(2) }}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-xl flex flex-col bg-white/70 px-3 py-2 gap-1 basis-1/3">
|
||||
<div class="text-sm font-semibold">{{ t('page.result.imageCompress.task') }}</div>
|
||||
<div class="text-3xl font-light">{{ taskResults.length }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="(item, index) in props?.data?.files" class="flex flex-row rounded-xl bg-white/70 p-3 justify-between w-full gap-3">
|
||||
<div class="flex flex-row gap-2 items-center w-full overflow-hidden">
|
||||
<div class="*:h-12 overflow-hidden">
|
||||
<FileIcon :file="item?.file" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-0.5 flex-1 overflow-hidden">
|
||||
<div class="truncate w-auto">{{ item?.file?.name }}</div>
|
||||
<div class="text-xs opacity-50">{{ filesize(item?.file?.size ?? 0) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-center" v-if="!taskResults?.[index]?.data">
|
||||
<Skeleton class="w-16 h-12" />
|
||||
</div>
|
||||
<div class="flex flex-row gap-1 items-center text-sm" v-if="taskResults?.[index]?.data?.status === 'retry'">
|
||||
<LucideLoader2 class="size-4 animate-spin" />
|
||||
{{
|
||||
t('page.result.imageCompress.retry', [
|
||||
taskResults?.[index]?.data?.err?.retry ?? 0,
|
||||
taskResults?.[index]?.data?.err?.max_retry ?? 0,
|
||||
])
|
||||
}}
|
||||
</div>
|
||||
<div class="flex items-center justify-center" v-if="taskResults?.[index]?.data?.status === 'archived'">
|
||||
<div class="text-sm text-red-500 px-2 py-1 rounded-md bg-red-100">{{ t('page.result.imageCompress.failed') }}</div>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 items-center shrink-0" v-if="taskResults?.[index]?.data?.status === 'success'">
|
||||
<div class="flex flex-col gap-1 items-center">
|
||||
<div class="rounded flex flex-row items-center bg-green-100 text-green-600 px-1 text-xs">
|
||||
<LucideArrowDown class="size-4" />
|
||||
{{
|
||||
(
|
||||
(1 -
|
||||
(taskResults?.[index]?.data?.result?.[0]?.new_file?.size ?? 0) /
|
||||
(taskResults?.[index]?.data?.result?.[0]?.old_file?.size ?? 0)) *
|
||||
100
|
||||
).toFixed(2)
|
||||
}}%
|
||||
</div>
|
||||
<div class="text-xs opacity-50">{{ filesize(taskResults?.[index]?.data?.result?.[0]?.new_file?.size ?? 0) }}</div>
|
||||
</div>
|
||||
<AsyncButton
|
||||
variant="outline"
|
||||
class="bg-black/5"
|
||||
size="icon"
|
||||
@click="
|
||||
async () => {
|
||||
const { new_file } = taskResults?.[index]?.data?.result?.[0] || {}
|
||||
if (!new_file?.id) return
|
||||
const data = await createFileShare({
|
||||
file_id: item.new_file.id,
|
||||
files: [{ id: new_file?.id as string, name: item?.file?.name }],
|
||||
config: {
|
||||
download_nums: 1,
|
||||
expire_time: 60,
|
||||
has_pickup_code: false,
|
||||
has_password: false,
|
||||
},
|
||||
file_name: props?.data?.file?.name,
|
||||
})
|
||||
const { id } = data?.data || {}
|
||||
const { id } = data?.[0]?.data || {}
|
||||
if (!id) {
|
||||
return
|
||||
}
|
||||
@@ -140,27 +198,9 @@ watch(
|
||||
}
|
||||
}
|
||||
"
|
||||
>
|
||||
<LucideDownload />
|
||||
</AsyncButton>
|
||||
><LucideArrowDown
|
||||
/></AsyncButton>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="taskData?.status !== 'retry' && !!taskData?.err?.message" class="flex flex-col gap-2">
|
||||
<div class="w-full h-16 flex flex-row items-center gap-3 bg-white/80 rounded-md p-2">
|
||||
<div class="size-10 flex items-center justify-center rounded-md bg-red-200">
|
||||
<LucideAlertTriangle class="size-5 text-red-600" />
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
{{ `经过 ${taskData?.err?.retry} 次重试后任务处理失败: ${taskData?.err?.message}` }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row justify-center">
|
||||
<Button @click="emit('change', 'input')"> 返回首页 </Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else="taskData?.status !== 'retry' && !!taskData?.err?.message" class="flex flex-col gap-2">
|
||||
<Skeleton class="w-full h-16 flex flex-row items-center justify-between" v-for="i in 3" />
|
||||
</div>
|
||||
</div>
|
||||
</BaseCard>
|
||||
</template>
|
||||
|
||||
190
front/components/Result/ImageConvertResult.vue
Normal file
190
front/components/Result/ImageConvertResult.vue
Normal file
@@ -0,0 +1,190 @@
|
||||
<script setup lang="ts">
|
||||
import { useQueries, useQuery } from '@tanstack/vue-query'
|
||||
import { AsyncButton } from '@/components/ui/button'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { filesize } from 'filesize'
|
||||
import useMyAppShare from '@/composables/useMyAppShare'
|
||||
import { toast } from 'vue-sonner'
|
||||
import type { handleFileComponentProps } from './types'
|
||||
|
||||
const { t } = useI18n()
|
||||
const emit = defineEmits<{
|
||||
(e: 'change', key: string): void
|
||||
}>()
|
||||
const props = defineProps<handleFileComponentProps>()
|
||||
const fileIds = computed(() => props?.data?.files?.map((item) => item.id))
|
||||
const targetExt = computed(() => props?.data?.config?.target_ext)
|
||||
const { data: taskIds } = useQuery({
|
||||
queryKey: ['create-image-convert', fileIds.value, targetExt.value],
|
||||
queryFn: async () => {
|
||||
return await Promise.all(
|
||||
props?.data?.files?.map(async (file) => {
|
||||
const { id } = file || {}
|
||||
if (!id) return
|
||||
const data = await $fetch<{
|
||||
code: number
|
||||
data: {
|
||||
id?: string
|
||||
}
|
||||
}>(`/api/task/image:convert`, {
|
||||
method: 'POST',
|
||||
body: {
|
||||
file_id: id,
|
||||
target_ext: targetExt.value,
|
||||
},
|
||||
})
|
||||
return data?.data?.id
|
||||
})
|
||||
)
|
||||
},
|
||||
staleTime: Infinity,
|
||||
enabled: !!fileIds.value && fileIds.value?.length > 0,
|
||||
})
|
||||
|
||||
const taskResults = useQueries({
|
||||
queries: computed(
|
||||
() =>
|
||||
taskIds?.value?.filter(Boolean).map((taskId) => {
|
||||
return {
|
||||
queryKey: ['task-image-convert', taskId],
|
||||
queryFn: async () => {
|
||||
const data = await $fetch<{
|
||||
code: number
|
||||
data: {
|
||||
result: {
|
||||
old_file: {
|
||||
id: string
|
||||
size: number
|
||||
}
|
||||
new_file: {
|
||||
id: string
|
||||
size: number
|
||||
}
|
||||
}[]
|
||||
status: 'success' | 'retry' | 'archived'
|
||||
err?: {
|
||||
message?: string
|
||||
retry?: number
|
||||
max_retry?: number
|
||||
}
|
||||
}
|
||||
}>(`/api/task/${taskId}`)
|
||||
return data?.data
|
||||
},
|
||||
enabled: !!taskId,
|
||||
}
|
||||
}) ?? []
|
||||
),
|
||||
})
|
||||
|
||||
const { downloadFileByShareId, createFileShare } = useMyAppShare()
|
||||
|
||||
const { counter, pause } = useInterval(2000, { controls: true })
|
||||
|
||||
watch(
|
||||
() => counter.value,
|
||||
() => {
|
||||
taskResults.value.forEach((item) => {
|
||||
if (['success', 'archived'].includes(item.data?.status ?? '')) return
|
||||
item.refetch()
|
||||
})
|
||||
|
||||
if (taskResults.value.every((item) => ['success', 'archived'].includes(item.data?.status ?? ''))) {
|
||||
pause()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const totalExt = computed(() => {
|
||||
return props?.data?.files?.reduce<string[]>((acc, item) => {
|
||||
const [, originalName, originalExt] = item?.file?.name?.match(/(.*?)\.([^.]+(?:\.[^.]+)*)$/) ?? []
|
||||
if (originalExt) acc.push(originalExt)
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<BaseCard class="flex flex-col gap-3" :title="t('page.result.imageConvert.title')" :showBackButton="true">
|
||||
<div class="flex flex-row gap-3">
|
||||
<div class="rounded-xl flex flex-col bg-white/70 px-3 py-2 gap-1 basis-2/3">
|
||||
<div class="text-sm font-semibold">{{ t('page.result.imageConvert.convert') }}</div>
|
||||
<div class="text-2xl font-light flex flex-row items-center gap-1">
|
||||
<div v-for="ext in totalExt" class="rounded flex flex-row items-center bg-primary/20 text-primary p-1 py-0.5 text-sm">
|
||||
{{ ext?.toUpperCase() }}
|
||||
</div>
|
||||
<LucideChevronsRight class="size-6" />
|
||||
<div class="rounded flex flex-row items-center bg-primary/20 text-primary p-1 py-0.5 text-sm">
|
||||
{{ targetExt?.toUpperCase() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-xl flex flex-col bg-white/70 px-3 py-2 gap-1 basis-1/3">
|
||||
<div class="text-sm font-semibold">{{ t('page.result.imageConvert.task') }}</div>
|
||||
<div class="text-3xl font-light">{{ taskResults.length }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="(item, index) in props?.data?.files" class="flex flex-row rounded-xl bg-white/70 p-3 justify-between w-full gap-3">
|
||||
<div class="flex flex-row gap-2 items-center w-full overflow-hidden">
|
||||
<div class="*:h-12 overflow-hidden">
|
||||
<FileIcon :file="item?.file" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-0.5 flex-1 overflow-hidden">
|
||||
<div class="truncate w-auto">{{ item?.file?.name }}</div>
|
||||
<div class="text-xs opacity-50">{{ filesize(item?.file?.size ?? 0) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-center" v-if="!taskResults?.[index]?.data">
|
||||
<Skeleton class="w-16 h-12" />
|
||||
</div>
|
||||
<div class="flex flex-row gap-1 items-center text-sm" v-if="taskResults?.[index]?.data?.status === 'retry'">
|
||||
<LucideLoader2 class="size-4 animate-spin" />
|
||||
{{
|
||||
t('page.result.imageConvert.retry', [
|
||||
taskResults?.[index]?.data?.err?.retry ?? 0,
|
||||
taskResults?.[index]?.data?.err?.max_retry ?? 0,
|
||||
])
|
||||
}}
|
||||
</div>
|
||||
<div class="flex items-center justify-center" v-if="taskResults?.[index]?.data?.status === 'archived'">
|
||||
<div class="text-sm text-red-500 px-2 py-1 rounded-md bg-red-100">{{ t('page.result.imageConvert.failed') }}</div>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 items-center shrink-0" v-if="taskResults?.[index]?.data?.status === 'success'">
|
||||
<div class="flex flex-col gap-1 items-center">
|
||||
<div class="rounded flex flex-row items-center bg-primary/20 text-primary px-1 text-xs">
|
||||
{{ targetExt.toUpperCase() }}
|
||||
</div>
|
||||
<div class="text-xs opacity-50">{{ filesize(taskResults?.[index]?.data?.result?.[0]?.new_file?.size ?? 0) }}</div>
|
||||
</div>
|
||||
<AsyncButton
|
||||
size="icon"
|
||||
@click="
|
||||
async () => {
|
||||
const { new_file } = taskResults?.[index]?.data?.result?.[0] || {}
|
||||
if (!new_file?.id) return
|
||||
const [, originalName, originalExt] = item?.file?.name?.match(/(.*?)\.([^.]+(?:\.[^.]+)*)$/) ?? []
|
||||
const data = await createFileShare({
|
||||
files: [{ id: new_file?.id as string, name: `${originalName}.${targetExt}` }],
|
||||
config: {
|
||||
download_nums: 1,
|
||||
expire_time: 60,
|
||||
has_pickup_code: false,
|
||||
has_password: false,
|
||||
},
|
||||
})
|
||||
const { id } = data?.[0]?.data || {}
|
||||
if (!id) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await downloadFileByShareId(id)
|
||||
} catch (error: any) {
|
||||
toast.error(error?.data?.message || error?.message || error)
|
||||
}
|
||||
}
|
||||
"
|
||||
><LucideArrowDown
|
||||
/></AsyncButton>
|
||||
</div>
|
||||
</div>
|
||||
</BaseCard>
|
||||
</template>
|
||||
@@ -2,13 +2,8 @@
|
||||
import FileShareResult from '@/components/Result/FileShareResult.vue'
|
||||
import TextShareResult from '@/components/Result/TextShareResult.vue'
|
||||
import ImageCompressResult from '@/components/Result/ImageCompressResult.vue'
|
||||
import type { FileHandleKey, TextHandleKey } from '../Preprocessing/types'
|
||||
|
||||
type basehandleData = { config: Record<string, any> }
|
||||
|
||||
type filehandleData = { files: { id: string; file: File }[]; handle_type: FileHandleKey } & basehandleData
|
||||
type texthandleData = { text: string; handle_type: TextHandleKey } & basehandleData
|
||||
type handleKey = 'file-share' | 'text-share' | 'file-image-compress'
|
||||
import ImageConvertResult from '@/components/Result/ImageConvertResult.vue'
|
||||
import type { filehandleData, handleComponent, handleKey, texthandleData } from './types'
|
||||
|
||||
const props = defineProps<{
|
||||
data: filehandleData | texthandleData
|
||||
@@ -18,10 +13,11 @@ const emit = defineEmits<{
|
||||
(e: 'change', key: string): void
|
||||
}>()
|
||||
|
||||
const handleList: { component: Component<{ data: filehandleData | texthandleData }>; key: handleKey }[] = [
|
||||
const handleList: { component: handleComponent; key: handleKey }[] = [
|
||||
{ component: FileShareResult, key: 'file-share' },
|
||||
{ component: TextShareResult, key: 'text-share' },
|
||||
{ component: ImageCompressResult, key: 'file-image-compress' },
|
||||
{ component: ImageConvertResult, key: 'file-image-convert' },
|
||||
]
|
||||
|
||||
const activeHandle = computed(() => {
|
||||
@@ -30,8 +26,6 @@ const activeHandle = computed(() => {
|
||||
// vue这个ts蠢的没边了,本来想写component: FileShareResult | TextShareResult,结果不行
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<component v-if="'file' in data" :is="activeHandle?.component" :data="data" @change="(key: string) => emit('change', key)" />
|
||||
<component v-if="'text' in data" :is="activeHandle?.component" :data="data" @change="(key: string) => emit('change', key)" />
|
||||
</div>
|
||||
<component v-if="'files' in data" :is="activeHandle?.component" :data="data" @change="(key: string) => emit('change', key)" />
|
||||
<component v-if="'text' in data" :is="activeHandle?.component" :data="data" @change="(key: string) => emit('change', key)" />
|
||||
</template>
|
||||
|
||||
@@ -10,11 +10,9 @@ import showDrawer from '@/lib/showDrawer'
|
||||
import QrCoreDrawer from '@/components/Drawer/QrCoreDrawer.vue'
|
||||
import dayjs from 'dayjs'
|
||||
import { h } from 'vue'
|
||||
import type { TextHandleKey } from '../Preprocessing/types'
|
||||
import type { handleTextComponentProps } from './types'
|
||||
|
||||
const props = defineProps<{
|
||||
data: { text: string; config: Record<string, any>; handle_type: TextHandleKey }
|
||||
}>()
|
||||
const props = defineProps<handleTextComponentProps>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'change', key: string): void
|
||||
@@ -44,41 +42,24 @@ const { t } = useI18n()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex flex-row gap-2">
|
||||
<div class="flex flex-row justify-between w-full">
|
||||
<h2 class="text-lg">{{ t('textshareresult.title') }}</h2>
|
||||
<Button
|
||||
variant="outline"
|
||||
class="bg-white/70"
|
||||
size="icon"
|
||||
@click="
|
||||
() => {
|
||||
emit('change', 'input')
|
||||
}
|
||||
"
|
||||
>
|
||||
<LucideHome />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<BaseCard class="flex flex-col gap-3" :title="t('page.result.text.title')" :showBackButton="true">
|
||||
<div class="flex flex-col md:flex-row gap-5 rounded-md p-5 bg-white/20 backdrop-blur-xl w-full">
|
||||
<div class="flex flex-col gap-2 flex-1">
|
||||
<div class="text-sm font-semibold">{{ t('textshareresult.info') }}</div>
|
||||
<div class="text-sm font-semibold">{{ t('page.result.text.info') }}</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div class="rounded-xl flex flex-col bg-black/10 px-3 py-2 gap-1">
|
||||
<div class="text-xs font-semibold">{{ t('textshareresult.viewNums') }}</div>
|
||||
<div class="text-xs font-semibold">{{ t('page.result.text.viewNums') }}</div>
|
||||
<div class="text-3xl font-light">{{ data?.download_nums }}</div>
|
||||
</div>
|
||||
<div class="rounded-xl flex flex-col bg-black/5 px-3 py-2 gap-1">
|
||||
<div class="text-xs font-semibold">{{ t('textshareresult.expireTime') }}</div>
|
||||
<div class="text-xs font-semibold">{{ t('page.result.text.expireTime') }}</div>
|
||||
<div class="text-md font-light">
|
||||
{{ dayjs((data?.expire_at ?? 0) * 1000).format('YYYY-MM-DD HH:mm:ss') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-xl flex flex-col bg-black/10 px-3 py-2 gap-1" v-if="data?.pickup_code">
|
||||
<div class="flex flex-row justify-between w-full items-center">
|
||||
<div class="text-xs font-semibold">{{ t('textshareresult.pickupCode') }}</div>
|
||||
<div class="text-xs font-semibold">{{ t('page.result.text.pickupCode') }}</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
class="bg-white/70 p-0 size-6"
|
||||
@@ -86,7 +67,7 @@ const { t } = useI18n()
|
||||
@click="
|
||||
() => {
|
||||
copy(data?.pickup_code as string)
|
||||
toast.success(t('textshareresult.copySuccess'))
|
||||
toast.success(t('page.result.text.copySuccess'))
|
||||
}
|
||||
"
|
||||
>
|
||||
@@ -102,7 +83,7 @@ const { t } = useI18n()
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-5 flex-1">
|
||||
<div class="text-sm font-semibold">{{ t('textshareresult.link') }}</div>
|
||||
<div class="text-sm font-semibold">{{ t('page.result.text.link') }}</div>
|
||||
<div class="flex flex-row gap-2">
|
||||
<Input v-model="url" class="bg-white/70" readonly />
|
||||
<Button
|
||||
@@ -112,7 +93,7 @@ const { t } = useI18n()
|
||||
@click="
|
||||
() => {
|
||||
copy(url)
|
||||
toast.success(t('textshareresult.copySuccess'))
|
||||
toast.success(t('page.result.text.copySuccess'))
|
||||
}
|
||||
"
|
||||
>
|
||||
@@ -140,7 +121,7 @@ const { t } = useI18n()
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="text-md">{{ t('textshareresult.content') }}</h2>
|
||||
<h2 class="text-md">{{ t('page.result.text.content') }}</h2>
|
||||
<MarkdownRender class="prose rounded-md bg-white/70 p-3 w-full max-w-full min-h-[30vh]" :markdown="props?.data?.text" />
|
||||
</div>
|
||||
</BaseCard>
|
||||
</template>
|
||||
|
||||
11
front/components/Result/types.ts
Normal file
11
front/components/Result/types.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { FileHandleKey, TextHandleKey } from '../Preprocessing/types'
|
||||
|
||||
type basehandleData = { config: Record<string, any> }
|
||||
|
||||
export type filehandleData = { files: { id: string; file: File }[]; handle_type: FileHandleKey } & basehandleData
|
||||
export type texthandleData = { text: string; handle_type: TextHandleKey } & basehandleData
|
||||
export type handleKey = FileHandleKey | TextHandleKey
|
||||
|
||||
export type handleTextComponentProps = { data: texthandleData }
|
||||
export type handleFileComponentProps = { data: filehandleData }
|
||||
export type handleComponent = Component<handleTextComponentProps | handleFileComponentProps>
|
||||
@@ -57,4 +57,5 @@ onUnmounted(() => {
|
||||
"
|
||||
>
|
||||
</editor-content>
|
||||
<!-- <BubbleMenuView :editor="editor as any" /> -->
|
||||
</template>
|
||||
|
||||
@@ -14,9 +14,9 @@ const handleError = () => clearError({ redirect: '/' })
|
||||
|
||||
<template>
|
||||
<NuxtLayout>
|
||||
<div class="rounded-xl p-5 bg-white/50 backdrop-blur-xl w-full lg:w-200 flex flex-col items-center justify-center min-h-[50vh] mt-5 gap-10">
|
||||
<BaseCard class="flex flex-col items-center justify-center min-h-[50vh] mt-5 gap-10">
|
||||
<div class="font-bold text-5xl">{{ error?.statusCode }}</div>
|
||||
<Button @click="handleError">{{ t('btn.backToHome') }}</Button>
|
||||
</div>
|
||||
</BaseCard>
|
||||
</NuxtLayout>
|
||||
</template>
|
||||
|
||||
@@ -13,114 +13,173 @@
|
||||
"submit": "Submit",
|
||||
"backToHome": "Back to Home"
|
||||
},
|
||||
"file": {
|
||||
"uploadFile": "Upload File",
|
||||
"uploadFilePlaceholder": "Drag and drop files or click to upload",
|
||||
"addMore": "Add More",
|
||||
"handleType": {
|
||||
"file-share": "File Share",
|
||||
"file-image-compress": "Image Compress"
|
||||
}
|
||||
},
|
||||
"text": {
|
||||
"uploadText": "Upload Text",
|
||||
"uploadTextPlaceholder": "Share, translate, summarize, generate images, and ask large models with our text processor",
|
||||
"handleType": {
|
||||
"text-share": "Text Share"
|
||||
}
|
||||
},
|
||||
"pickup": {
|
||||
"title": "Enter Pickup Code",
|
||||
"codeError": "Invalid pickup code",
|
||||
"btn": "Pickup"
|
||||
},
|
||||
"fileshare": {
|
||||
"title": "Share Options",
|
||||
"downloadNums": "Download Count",
|
||||
"expireTime": "Expire Time",
|
||||
"or": "or",
|
||||
"expireAfter": "expire",
|
||||
"pickupCode": "Pickup Code",
|
||||
"passwordProtection": "Password Protection",
|
||||
"downloadNotify": "Download Notification",
|
||||
"passwordPlaceholder": "Enter password",
|
||||
"emailPlaceholder": "Enter email",
|
||||
"downloadOptions": {
|
||||
"1time": "1 download",
|
||||
"2times": "2 downloads",
|
||||
"3times": "3 downloads",
|
||||
"5times": "5 downloads",
|
||||
"10times": "10 downloads"
|
||||
"page": {
|
||||
"upload": {
|
||||
"file": {
|
||||
"uploadFile": "Upload File",
|
||||
"uploadFilePlaceholder": "Drag and drop files or click to upload",
|
||||
"addMore": "Add More",
|
||||
"handleType": {
|
||||
"file-share": "File Share",
|
||||
"file-image-compress": "Image Compress",
|
||||
"file-image-convert": "Format Convert"
|
||||
}
|
||||
},
|
||||
"text": {
|
||||
"uploadText": "Upload Text",
|
||||
"uploadTextPlaceholder": "Share, translate, summarize, generate images, and ask large models with our text processor",
|
||||
"handleType": {
|
||||
"text-share": "Text Share"
|
||||
}
|
||||
},
|
||||
"pickup": {
|
||||
"title": "Enter Pickup Code",
|
||||
"codeError": "Invalid pickup code",
|
||||
"btn": "Pickup"
|
||||
}
|
||||
},
|
||||
"expireOptions": {
|
||||
"5min": "5 minutes",
|
||||
"1hour": "1 hour",
|
||||
"1day": "1 day",
|
||||
"3days": "3 days"
|
||||
}
|
||||
},
|
||||
"textshare": {
|
||||
"title": "Share Options",
|
||||
"viewNums": "View Count",
|
||||
"expireTime": "Expire Time",
|
||||
"or": "or",
|
||||
"expireAfter": "expire",
|
||||
"pickupCode": "Pickup Code",
|
||||
"passwordProtection": "Password Protection",
|
||||
"readNotify": "Read Notification",
|
||||
"passwordPlaceholder": "Enter password",
|
||||
"emailPlaceholder": "Enter email",
|
||||
"viewOptions": {
|
||||
"1time": "1 view",
|
||||
"2times": "2 views",
|
||||
"3times": "3 views",
|
||||
"5times": "5 views",
|
||||
"10times": "10 views"
|
||||
"shareOptions": {
|
||||
"file": {
|
||||
"title": "Share Options",
|
||||
"downloadNums": "Download Count",
|
||||
"expireTime": "Expire Time",
|
||||
"or": "or",
|
||||
"expireAfter": "expire",
|
||||
"pickupCode": "Pickup Code",
|
||||
"passwordProtection": "Password Protection",
|
||||
"downloadNotify": "Download Notification",
|
||||
"passwordPlaceholder": "Enter password",
|
||||
"emailPlaceholder": "Enter email",
|
||||
"downloadOptions": {
|
||||
"xdownload": "{0} downloads"
|
||||
},
|
||||
"expireOptions": {
|
||||
"5min": "5 minutes",
|
||||
"1hour": "1 hour",
|
||||
"1day": "1 day",
|
||||
"3days": "3 days"
|
||||
}
|
||||
},
|
||||
"imageConvert": {
|
||||
"title": "Image Convert",
|
||||
"targetFormat": "Target Format"
|
||||
},
|
||||
"text": {
|
||||
"title": "Share Options",
|
||||
"viewNums": "View Count",
|
||||
"expireTime": "Expire Time",
|
||||
"or": "or",
|
||||
"expireAfter": "expire",
|
||||
"pickupCode": "Pickup Code",
|
||||
"passwordProtection": "Password Protection",
|
||||
"readNotify": "Read Notification",
|
||||
"passwordPlaceholder": "Enter password",
|
||||
"emailPlaceholder": "Enter email",
|
||||
"viewOptions": {
|
||||
"xview": "{0} views"
|
||||
},
|
||||
"expireOptions": {
|
||||
"5min": "5 minutes",
|
||||
"1hour": "1 hour",
|
||||
"1day": "1 day",
|
||||
"3days": "3 days"
|
||||
}
|
||||
}
|
||||
},
|
||||
"expireOptions": {
|
||||
"5min": "5 minutes",
|
||||
"1hour": "1 hour",
|
||||
"1day": "1 day",
|
||||
"3days": "3 days"
|
||||
"progress": {
|
||||
"file": {
|
||||
"totalUploadProgress": "Total Upload Progress",
|
||||
"fileList": "File List",
|
||||
"fileName": "File Name",
|
||||
"fileSize": "File Size",
|
||||
"uploadSpeed": "Speed",
|
||||
"progress": "Progress",
|
||||
"uploadDetails": "Upload Details",
|
||||
"chunk": "Chunk",
|
||||
"completed": "Completed",
|
||||
"discarded": "Discarded",
|
||||
"pending": "Pending",
|
||||
"chunkProgress": "Chunk Progress",
|
||||
"chunkHeatmap": "Chunk Heatmap",
|
||||
"heatmap": "Heatmap",
|
||||
"progressBar": "Progress Bar",
|
||||
"uploadError": "Upload Error",
|
||||
"chunkUploadFailed": "Chunk {1} of file {0} failed after multiple attempts, we have terminated the file upload",
|
||||
"chunkUploadRetry": "Chunk {1} of file {0} failed to upload, we will try again later",
|
||||
"fileUploadFailed": "File {0} upload failed, please try again",
|
||||
"uploadFailed": "Upload Failed",
|
||||
"processing": {
|
||||
"hash": "Calculating Hash...",
|
||||
"create": "Initializing Upload...",
|
||||
"upload": "Uploading...",
|
||||
"finish": "Upload Finished"
|
||||
},
|
||||
"instantUploadSuccess": "File with same hash exists in cloud, instant upload successful",
|
||||
"uploadFailedRetry": "Upload failed, please try again later",
|
||||
"uploadSpeedInfo": {
|
||||
"title": "How is the upload speed calculated?",
|
||||
"desc": {
|
||||
"base": "The upload speed is estimated based on {chunkNum} * {chunkSize} uploaded in the current second, which may have some deviation from the actual upload speed, for reference only",
|
||||
"chunkNum": "Number of file chunks",
|
||||
"chunkSize": "Size of each file chunk"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"result": {
|
||||
"file": {
|
||||
"title": "Upload Successful",
|
||||
"fileList": "File List",
|
||||
"info": "Info",
|
||||
"downloadNums": "Download Count",
|
||||
"expireTime": "Expire Time",
|
||||
"pickupCode": "Pickup Code",
|
||||
"link": "Link",
|
||||
"copySuccess": "Copy Success"
|
||||
},
|
||||
"imageCompress": {
|
||||
"title": "Image Compress",
|
||||
"totalSize": "Total Size",
|
||||
"task": "Task",
|
||||
"retry": "Retry {0}/{1}",
|
||||
"failed": "Failed"
|
||||
},
|
||||
"imageConvert": {
|
||||
"title": "Image Convert",
|
||||
"convert": "Convert",
|
||||
"task": "Task",
|
||||
"retry": "Retry {0}/{1}",
|
||||
"failed": "Failed"
|
||||
},
|
||||
"text": {
|
||||
"title": "Share Successful",
|
||||
"info": "Info",
|
||||
"viewNums": "View Count",
|
||||
"expireTime": "Expire Time",
|
||||
"pickupCode": "Pickup Code",
|
||||
"link": "Link",
|
||||
"content": "Content",
|
||||
"copySuccess": "Copy Success"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
"powerBy": "Power by {0} as a open source temporary file sharing platform",
|
||||
"file": "File",
|
||||
"share": "Share",
|
||||
"download": "Download",
|
||||
"task": "Task",
|
||||
"admin": "Site Admin",
|
||||
"author": "Author",
|
||||
"title": "About",
|
||||
"about": "About",
|
||||
"systemInfo": "System Info",
|
||||
"systemVersion": "System Version",
|
||||
"storage": "Storage",
|
||||
"analysis": "Analysis",
|
||||
"fileSize": "File Size",
|
||||
"fileNum": "File Num",
|
||||
"processed": "Processed",
|
||||
"failed": "Failed"
|
||||
}
|
||||
},
|
||||
"fileshareresult": {
|
||||
"title": "Upload Successful",
|
||||
"fileList": "File List",
|
||||
"info": "Info",
|
||||
"downloadNums": "Download Count",
|
||||
"expireTime": "Expire Time",
|
||||
"pickupCode": "Pickup Code",
|
||||
"link": "Link",
|
||||
"copySuccess": "Copy Success"
|
||||
},
|
||||
"textshareresult": {
|
||||
"title": "Share Successful",
|
||||
"info": "Info",
|
||||
"viewNums": "View Count",
|
||||
"expireTime": "Expire Time",
|
||||
"pickupCode": "Pickup Code",
|
||||
"link": "Link",
|
||||
"content": "Content",
|
||||
"copySuccess": "Copy Success"
|
||||
},
|
||||
"about": {
|
||||
"powerBy": "Power by {0} as a open source temporary file sharing platform",
|
||||
"file": "File",
|
||||
"share": "Share",
|
||||
"download": "Download",
|
||||
"task": "Task",
|
||||
"admin": "Site Admin",
|
||||
"author": "Author",
|
||||
"title": "About",
|
||||
"about": "About",
|
||||
"systemInfo": "System Info",
|
||||
"systemVersion": "System Version",
|
||||
"storage": "Storage",
|
||||
"analysis": "Analysis",
|
||||
"fileSize": "File Size",
|
||||
"fileNum": "File Num",
|
||||
"processed": "Processed",
|
||||
"failed": "Failed"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,114 +13,173 @@
|
||||
"submit": "提交",
|
||||
"backToHome": "返回首页"
|
||||
},
|
||||
"file": {
|
||||
"uploadFile": "上传文件",
|
||||
"uploadFilePlaceholder": "拖拽文件 或 点击上传",
|
||||
"addMore": "添加更多",
|
||||
"handleType": {
|
||||
"file-share": "文件分享",
|
||||
"file-image-compress": "图片压缩"
|
||||
}
|
||||
},
|
||||
"text": {
|
||||
"uploadText": "上传文本",
|
||||
"uploadTextPlaceholder": "使用我们的文本处理器轻松分享,翻译,总结,生成图片,询问大模型",
|
||||
"handleType": {
|
||||
"text-share": "文本分享"
|
||||
}
|
||||
},
|
||||
"pickup": {
|
||||
"title": "输入取件码",
|
||||
"codeError": "取件码错误",
|
||||
"btn": "取件"
|
||||
},
|
||||
"fileshare": {
|
||||
"title": "分享选项",
|
||||
"downloadNums": "下载次数",
|
||||
"expireTime": "过期时间",
|
||||
"or": "或",
|
||||
"expireAfter": "后过期",
|
||||
"pickupCode": "取件码",
|
||||
"passwordProtection": "密码保护",
|
||||
"downloadNotify": "下载通知",
|
||||
"passwordPlaceholder": "请输入密码",
|
||||
"emailPlaceholder": "请输入邮箱",
|
||||
"downloadOptions": {
|
||||
"1time": "1次下载",
|
||||
"2times": "2次下载",
|
||||
"3times": "3次下载",
|
||||
"5times": "5次下载",
|
||||
"10times": "10次下载"
|
||||
"page": {
|
||||
"upload": {
|
||||
"file": {
|
||||
"uploadFile": "上传文件",
|
||||
"uploadFilePlaceholder": "拖拽文件 或 点击上传",
|
||||
"addMore": "添加更多",
|
||||
"handleType": {
|
||||
"file-share": "文件分享",
|
||||
"file-image-compress": "图片压缩",
|
||||
"file-image-convert": "格式转换"
|
||||
}
|
||||
},
|
||||
"text": {
|
||||
"uploadText": "上传文本",
|
||||
"uploadTextPlaceholder": "使用我们的文本处理器轻松分享,翻译,总结,生成图片,询问大模型",
|
||||
"handleType": {
|
||||
"text-share": "文本分享"
|
||||
}
|
||||
},
|
||||
"pickup": {
|
||||
"title": "输入取件码",
|
||||
"codeError": "取件码错误",
|
||||
"btn": "取件"
|
||||
}
|
||||
},
|
||||
"expireOptions": {
|
||||
"5min": "5分钟",
|
||||
"1hour": "1小时",
|
||||
"1day": "1天",
|
||||
"3days": "3天"
|
||||
}
|
||||
},
|
||||
"textshare": {
|
||||
"title": "分享选项",
|
||||
"viewNums": "浏览次数",
|
||||
"expireTime": "过期时间",
|
||||
"or": "或",
|
||||
"expireAfter": "后过期",
|
||||
"pickupCode": "取件码",
|
||||
"passwordProtection": "密码保护",
|
||||
"readNotify": "已读通知",
|
||||
"passwordPlaceholder": "请输入密码",
|
||||
"emailPlaceholder": "请输入邮箱",
|
||||
"viewOptions": {
|
||||
"1time": "1次浏览",
|
||||
"2times": "2次浏览",
|
||||
"3times": "3次浏览",
|
||||
"5times": "5次浏览",
|
||||
"10times": "10次浏览"
|
||||
"shareOptions": {
|
||||
"file": {
|
||||
"title": "分享选项",
|
||||
"downloadNums": "下载次数",
|
||||
"expireTime": "过期时间",
|
||||
"or": "或",
|
||||
"expireAfter": "后过期",
|
||||
"pickupCode": "取件码",
|
||||
"passwordProtection": "密码保护",
|
||||
"downloadNotify": "下载通知",
|
||||
"passwordPlaceholder": "请输入密码",
|
||||
"emailPlaceholder": "请输入邮箱",
|
||||
"downloadOptions": {
|
||||
"xdownload": "{0}次下载"
|
||||
},
|
||||
"expireOptions": {
|
||||
"5min": "5分钟",
|
||||
"1hour": "1小时",
|
||||
"1day": "1天",
|
||||
"3days": "3天"
|
||||
}
|
||||
},
|
||||
"imageConvert": {
|
||||
"title": "图片转换",
|
||||
"targetFormat": "目标格式"
|
||||
},
|
||||
"text": {
|
||||
"title": "分享选项",
|
||||
"viewNums": "浏览次数",
|
||||
"expireTime": "过期时间",
|
||||
"or": "或",
|
||||
"expireAfter": "后过期",
|
||||
"pickupCode": "取件码",
|
||||
"passwordProtection": "密码保护",
|
||||
"readNotify": "已读通知",
|
||||
"passwordPlaceholder": "请输入密码",
|
||||
"emailPlaceholder": "请输入邮箱",
|
||||
"viewOptions": {
|
||||
"xview": "{0}次浏览"
|
||||
},
|
||||
"expireOptions": {
|
||||
"5min": "5分钟",
|
||||
"1hour": "1小时",
|
||||
"1day": "1天",
|
||||
"3days": "3天"
|
||||
}
|
||||
}
|
||||
},
|
||||
"expireOptions": {
|
||||
"5min": "5分钟",
|
||||
"1hour": "1小时",
|
||||
"1day": "1天",
|
||||
"3days": "3天"
|
||||
"progress": {
|
||||
"file": {
|
||||
"totalUploadProgress": "总上传进度",
|
||||
"fileList": "文件列表",
|
||||
"fileName": "文件名",
|
||||
"fileSize": "文件大小",
|
||||
"uploadSpeed": "上传速度",
|
||||
"progress": "进度",
|
||||
"uploadDetails": "上传详情",
|
||||
"chunk": "区块",
|
||||
"completed": "已完成",
|
||||
"discarded": "已丢弃",
|
||||
"pending": "待完成",
|
||||
"chunkProgress": "区块进度条",
|
||||
"chunkHeatmap": "区块热力图",
|
||||
"heatmap": "热力图",
|
||||
"progressBar": "进度条",
|
||||
"uploadError": "上传错误",
|
||||
"chunkUploadFailed": "文件 {0} 的{1}分块经过多次尝试依然上传失败, 我们已经终止该文件上传",
|
||||
"chunkUploadRetry": "文件 {0} 的{1}分块上传失败, 我们将在稍后再次尝试上传",
|
||||
"fileUploadFailed": "文件{0}上传失败, 请重试",
|
||||
"uploadFailed": "上传失败",
|
||||
"processing": {
|
||||
"hash": "计算Hash...",
|
||||
"create": "初始化上传...",
|
||||
"upload": "上传中...",
|
||||
"finish": "上传完成"
|
||||
},
|
||||
"instantUploadSuccess": "云端已有相同Hash文件, 秒传成功",
|
||||
"uploadFailedRetry": "上传失败,请稍后重试",
|
||||
"uploadSpeedInfo": {
|
||||
"title": "上传速度如何计算",
|
||||
"desc": {
|
||||
"base": "上传速度根据当前秒上传了 {chunkNum} * {chunkSize} 估算而来,可能与真实上传速度有一定的误差,仅供参考",
|
||||
"chunkNum": "文件区块的数量",
|
||||
"chunkSize": "每个文件区块的大小"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"result": {
|
||||
"file": {
|
||||
"title": "上传成功",
|
||||
"fileList": "文件列表",
|
||||
"info": "信息",
|
||||
"downloadNums": "下载次数",
|
||||
"expireTime": "过期时间",
|
||||
"pickupCode": "提取码",
|
||||
"link": "链接",
|
||||
"copySuccess": "复制成功"
|
||||
},
|
||||
"imageCompress": {
|
||||
"title": "图片压缩",
|
||||
"totalSize": "总大小",
|
||||
"task": "任务",
|
||||
"retry": "重试 {0}/{1}",
|
||||
"failed": "失败"
|
||||
},
|
||||
"imageConvert": {
|
||||
"title": "图片转换",
|
||||
"convert": "转换",
|
||||
"task": "任务",
|
||||
"retry": "重试 {0}/{1}",
|
||||
"failed": "失败"
|
||||
},
|
||||
"text": {
|
||||
"title": "分享成功",
|
||||
"info": "信息",
|
||||
"viewNums": "浏览次数",
|
||||
"expireTime": "过期时间",
|
||||
"pickupCode": "提取码",
|
||||
"link": "链接",
|
||||
"content": "内容",
|
||||
"copySuccess": "复制成功"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
"powerBy": "由 {0} 驱动的开源自托管临时文件分享平台",
|
||||
"file": "文件",
|
||||
"share": "分享",
|
||||
"download": "下载",
|
||||
"task": "任务",
|
||||
"admin": "本站管理员",
|
||||
"author": "作者",
|
||||
"title": "关于",
|
||||
"about": "关于",
|
||||
"systemInfo": "系统信息",
|
||||
"systemVersion": "系统版本",
|
||||
"storage": "已托管的文件",
|
||||
"analysis": "分析",
|
||||
"fileSize": "文件大小",
|
||||
"fileNum": "文件数量",
|
||||
"processed": "处理数量",
|
||||
"failed": "失败数量"
|
||||
}
|
||||
},
|
||||
"fileshareresult": {
|
||||
"title": "上传成功",
|
||||
"fileList": "文件列表",
|
||||
"info": "信息",
|
||||
"downloadNums": "下载次数",
|
||||
"expireTime": "过期时间",
|
||||
"pickupCode": "提取码",
|
||||
"link": "链接",
|
||||
"copySuccess": "复制成功"
|
||||
},
|
||||
"textshareresult": {
|
||||
"title": "分享成功",
|
||||
"info": "信息",
|
||||
"viewNums": "浏览次数",
|
||||
"expireTime": "过期时间",
|
||||
"pickupCode": "提取码",
|
||||
"link": "链接",
|
||||
"content": "内容",
|
||||
"copySuccess": "复制成功"
|
||||
},
|
||||
"about": {
|
||||
"powerBy": "由 {0} 驱动的开源自托管临时文件分享平台",
|
||||
"file": "文件",
|
||||
"share": "分享",
|
||||
"download": "下载",
|
||||
"task": "任务",
|
||||
"admin": "本站管理员",
|
||||
"author": "作者",
|
||||
"title": "关于",
|
||||
"about": "关于",
|
||||
"systemInfo": "系统信息",
|
||||
"systemVersion": "系统版本",
|
||||
"storage": "已托管的文件",
|
||||
"analysis": "分析",
|
||||
"fileSize": "文件大小",
|
||||
"fileNum": "文件数量",
|
||||
"processed": "处理数量",
|
||||
"failed": "失败数量"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ export default defineNuxtConfig({
|
||||
],
|
||||
},
|
||||
vite: {
|
||||
transformMixedEsModules: true,
|
||||
plugins: [tailwindcss()],
|
||||
optimizeDeps: {
|
||||
include: ['eventemitter3'],
|
||||
|
||||
@@ -12,55 +12,55 @@
|
||||
"dependencies": {
|
||||
"@nuxt/image": "1.10.0",
|
||||
"@nuxtjs/i18n": "9.5.5",
|
||||
"@pinia/nuxt": "^0.11.2",
|
||||
"@tailwindcss/postcss": "^4.1.14",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@tanstack/vue-query": "^5.90.5",
|
||||
"@tiptap/extension-blockquote": "^3.7.2",
|
||||
"@tiptap/extension-bold": "^3.7.2",
|
||||
"@tiptap/extension-bubble-menu": "^3.7.2",
|
||||
"@tiptap/extension-heading": "^3.7.2",
|
||||
"@tiptap/extension-italic": "^3.7.2",
|
||||
"@tiptap/extension-paragraph": "^3.7.2",
|
||||
"@tiptap/extension-placeholder": "^3.7.2",
|
||||
"@tiptap/extension-strike": "^3.7.2",
|
||||
"@tiptap/extension-text": "^3.7.2",
|
||||
"@tiptap/pm": "^3.7.2",
|
||||
"@tiptap/starter-kit": "^3.7.2",
|
||||
"@tiptap/vue-3": "^3.7.2",
|
||||
"@unovis/ts": "^1.6.1",
|
||||
"@unovis/vue": "^1.6.1",
|
||||
"@pinia/nuxt": "^0.11.3",
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tanstack/vue-query": "^5.92.9",
|
||||
"@tiptap/extension-blockquote": "^3.18.0",
|
||||
"@tiptap/extension-bold": "^3.18.0",
|
||||
"@tiptap/extension-bubble-menu": "^3.18.0",
|
||||
"@tiptap/extension-heading": "^3.18.0",
|
||||
"@tiptap/extension-italic": "^3.18.0",
|
||||
"@tiptap/extension-paragraph": "^3.18.0",
|
||||
"@tiptap/extension-placeholder": "^3.18.0",
|
||||
"@tiptap/extension-strike": "^3.18.0",
|
||||
"@tiptap/extension-text": "^3.18.0",
|
||||
"@tiptap/pm": "^3.18.0",
|
||||
"@tiptap/starter-kit": "^3.18.0",
|
||||
"@tiptap/vue-3": "^3.18.0",
|
||||
"@unovis/ts": "^1.6.2",
|
||||
"@unovis/vue": "^1.6.2",
|
||||
"@vee-validate/nuxt": "^4.15.1",
|
||||
"@vee-validate/rules": "^4.15.1",
|
||||
"axios": "^1.13.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.18",
|
||||
"dayjs": "^1.11.19",
|
||||
"filesize": "^10.1.6",
|
||||
"js-md5": "^0.8.3",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lodash-es": "^4.17.23",
|
||||
"lucide-vue-next": "^0.542.0",
|
||||
"markdown-it": "^14.1.0",
|
||||
"motion-v": "^1.7.2",
|
||||
"motion-v": "^1.10.2",
|
||||
"nanoid": "^5.1.6",
|
||||
"nuxt": "4.1.2",
|
||||
"nuxt": "4.2.2",
|
||||
"nuxt-lucide-icons": "1.0.5",
|
||||
"pinia": "^3.0.3",
|
||||
"pixi.js": "^8.14.0",
|
||||
"pinia": "^3.0.4",
|
||||
"pixi.js": "^8.15.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"reka-ui": "^2.5.1",
|
||||
"reka-ui": "^2.8.0",
|
||||
"shadcn-nuxt": "2.0.1",
|
||||
"spark-md5": "^3.0.2",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"tiptap-markdown": "^0.9.0",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"vaul-vue": "^0.4.1",
|
||||
"vue": "^3.5.22",
|
||||
"vue-router": "^4.6.3",
|
||||
"vue": "^3.5.27",
|
||||
"vue-router": "^4.6.4",
|
||||
"vue-sonner": "^1.3.2",
|
||||
"vue3-pixi": "1.0.0-beta.2"
|
||||
},
|
||||
"packageManager": "pnpm@9.11.0+sha512.0a203ffaed5a3f63242cd064c8fb5892366c103e328079318f78062f24ea8c9d50bc6a47aa3567cabefd824d170e78fa2745ed1f16b132e16436146b7688f19b",
|
||||
"resolutions": {
|
||||
"esbuild": "0.25.6"
|
||||
},
|
||||
@@ -68,7 +68,7 @@
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/qrcode": "^1.5.6",
|
||||
"@types/spark-md5": "^3.0.5",
|
||||
"@vueuse/core": "^13.9.0",
|
||||
"@vueuse/nuxt": "^13.9.0",
|
||||
|
||||
@@ -6,10 +6,9 @@ const { t } = useI18n()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rounded-xl p-5 bg-white/50 backdrop-blur-xl w-full lg:w-200 my-5 flex flex-col gap-5">
|
||||
<div class="text-xl font-normal">{{ t('about.title') }}</div>
|
||||
<BaseCard class="my-5 flex flex-col gap-5" :title="t('page.about.title')">
|
||||
<AboutBaseInfo />
|
||||
<AboutChartView />
|
||||
<AboutVersionView />
|
||||
</div>
|
||||
</BaseCard>
|
||||
</template>
|
||||
|
||||
@@ -19,8 +19,7 @@ if (!isDev) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rounded-xl p-5 bg-white/50 backdrop-blur-xl w-full lg:w-200 my-5 flex flex-col gap-5">
|
||||
<h1>Dev</h1>
|
||||
<BaseCard class="my-5 flex flex-col gap-5" title="dev">
|
||||
<div class="flex flex-row gap-5 items-center">
|
||||
<Button
|
||||
@click="
|
||||
@@ -74,6 +73,6 @@ if (!isDev) {
|
||||
</div>
|
||||
</VeeForm>
|
||||
<div>测试dayjs语言包渲染:{{ dayjs().add(1, 'day').fromNow() }}</div>
|
||||
</div>
|
||||
</BaseCard>
|
||||
</template>
|
||||
<style scoped></style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-5 py-5 items-center w-full h-full">
|
||||
<component :is="renderComponent" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-5 py-5 items-center w-full h-full">
|
||||
<component :is="renderComponent" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -11,15 +11,15 @@ import { isString } from 'lodash-es'
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const type = computed(() => route?.query?.type)
|
||||
onMounted(() => {
|
||||
if (!isString(type.value) || type.value?.length === 0) {
|
||||
router.push({ query: { type: 'file' }, replace: true })
|
||||
}
|
||||
watchEffect(() => {
|
||||
if (!isString(type.value) || type.value?.length === 0) {
|
||||
router.push({ query: { type: 'file' }, replace: true })
|
||||
}
|
||||
})
|
||||
|
||||
const renderList = [
|
||||
{ key: 'file', component: FileUploadView },
|
||||
{ key: 'text', component: TextUploadView },
|
||||
{ key: 'file', component: FileUploadView },
|
||||
{ key: 'text', component: TextUploadView },
|
||||
]
|
||||
const renderComponent = computed(() => renderList.find(item => item.key === type.value)?.component)
|
||||
</script>
|
||||
const renderComponent = computed(() => renderList.find((item) => item.key === type.value)?.component)
|
||||
</script>
|
||||
|
||||
@@ -23,7 +23,7 @@ const { data, isLoading, error } = useQuery({
|
||||
}>(`/api/share/${id.value}`)
|
||||
return data?.data
|
||||
},
|
||||
retry: false
|
||||
retry: false,
|
||||
})
|
||||
|
||||
const isExpired = computed(() => {
|
||||
@@ -33,13 +33,12 @@ const isExpired = computed(() => {
|
||||
|
||||
const componentMap = {
|
||||
file: FileShareView,
|
||||
text: TextShareView
|
||||
text: TextShareView,
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rounded-xl p-5 bg-white/50 backdrop-blur-xl w-full lg:w-200 my-5 overflow-hidden">
|
||||
<BaseCard class="my-5 overflow-hidden">
|
||||
<div v-if="isLoading" class="flex flex-col gap-5 items-center">
|
||||
<Skeleton class="w-20 h-5 rounded-full" />
|
||||
<Skeleton class="w-16 h-16 rounded-xl" />
|
||||
@@ -52,15 +51,19 @@ const componentMap = {
|
||||
<template v-else>
|
||||
<div v-if="isExpired || !data" class="flex flex-col gap-5 items-center">
|
||||
<LucideAlertCircle :size="48" class="text-orange-500 rounded-full bg-orange-500/30 p-2" />
|
||||
<div class="text-xl ">此链接已过期。</div>
|
||||
<Button @click="() => {
|
||||
router.push('/')
|
||||
}">返回首页</Button>
|
||||
<div class="text-xl">此链接已过期。</div>
|
||||
<Button
|
||||
@click="
|
||||
() => {
|
||||
router.push('/')
|
||||
}
|
||||
"
|
||||
>返回首页</Button
|
||||
>
|
||||
</div>
|
||||
<template v-else>
|
||||
<component :is="componentMap[data?.type as keyof typeof componentMap] || 'div'" :data="data" />
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
</BaseCard>
|
||||
</template>
|
||||
|
||||
9
go.work
Normal file
9
go.work
Normal file
@@ -0,0 +1,9 @@
|
||||
go 1.25.5
|
||||
|
||||
use (
|
||||
./backend
|
||||
./pkg/models
|
||||
./pkg/services
|
||||
./pkg/utils
|
||||
./worker
|
||||
)
|
||||
104
go.work.sum
Normal file
104
go.work.sum
Normal file
@@ -0,0 +1,104 @@
|
||||
github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM=
|
||||
github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
|
||||
github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||
github.com/bmatcuk/doublestar/v4 v4.9.2 h1:b0mc6WyRSYLjzofB2v/0cuDUZ+MqoGyH3r0dVij35GI=
|
||||
github.com/bmatcuk/doublestar/v4 v4.9.2/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||
github.com/casbin/casbin/v2 v2.105.0 h1:dLj5P6pLApBRat9SADGiLxLZjiDPvA1bsPkyV4PGx6I=
|
||||
github.com/casbin/casbin/v2 v2.105.0/go.mod h1:Ee33aqGrmES+GNL17L0h9X28wXuo829wnNUnS0edAco=
|
||||
github.com/casbin/casbin/v2 v2.135.0 h1:6BLkMQiGotYyS5yYeWgW19vxqugUlvHFkFiLnLR/bxk=
|
||||
github.com/casbin/casbin/v2 v2.135.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
|
||||
github.com/casbin/govaluate v1.3.0 h1:VA0eSY0M2lA86dYd5kPPuNZMUD9QkWnOCnavGrw9myc=
|
||||
github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
|
||||
github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaDG0=
|
||||
github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
|
||||
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
|
||||
github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg=
|
||||
github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
|
||||
github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
|
||||
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o=
|
||||
github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
|
||||
github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg=
|
||||
github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
|
||||
github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
|
||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/telemetry v0.0.0-20250807160809-1a19826ec488 h1:3doPGa+Gg4snce233aCWnbZVFsyFMo/dR40KK/6skyE=
|
||||
golang.org/x/telemetry v0.0.0-20250807160809-1a19826ec488/go.mod h1:fGb/2+tgXXjhjHsTNdVEEMZNWA0quBnfrO+AfoDSAKw=
|
||||
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
|
||||
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
|
||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
|
||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM=
|
||||
google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
23
package.json
23
package.json
@@ -4,12 +4,23 @@
|
||||
"private": true,
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"dev": "npm-run-all -p dev:front dev:backend dev:worker",
|
||||
"dev": "concurrently -n front,backend,worker -c blue,green,yellow 'pnpm:dev:front' 'pnpm:dev:backend' 'pnpm:dev:worker'",
|
||||
"dev:front": "cd front && pnpm run dev",
|
||||
"dev:backend": "cd backend && air",
|
||||
"dev:worker": "cd worker && air",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"prepare": "husky"
|
||||
"lint": "concurrently -n front,backend,worker -c blue,green,yellow 'pnpm:lint:front' 'pnpm:lint:backend' 'pnpm:lint:worker'",
|
||||
"lint:front": "cd front && pnpm nuxt typecheck",
|
||||
"lint:backend": "cd backend && golangci-lint run",
|
||||
"lint:worker": "cd worker && golangci-lint run",
|
||||
"test:backend": "cd backend && go test -v ./test/...",
|
||||
"test:worker": "cd worker && go test -v ./test/...",
|
||||
"test": "concurrently -n backend,worker -c green,yellow 'pnpm:test:backend' 'pnpm:test:worker'",
|
||||
"prepare": "husky",
|
||||
"up-deps": "concurrently -n backend,worker,models,utils -c green,yellow,blue,magenta 'pnpm:up-deps:backend' 'pnpm:up-deps:worker' 'pnpm:up-deps:models' 'pnpm:up-deps:utils'",
|
||||
"up-deps:backend": "cd backend && go get -u",
|
||||
"up-deps:worker": "cd worker && go get -u",
|
||||
"up-deps:models": "cd pkg/models && go get -u",
|
||||
"up-deps:utils": "cd pkg/utils && go get -u"
|
||||
},
|
||||
"workspaces": [
|
||||
"front"
|
||||
@@ -27,10 +38,10 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"concurrently": "^9.2.1",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^15.5.2",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.6.2"
|
||||
"prettier": "^3.8.1"
|
||||
},
|
||||
"packageManager": "pnpm@9.11.0+sha512.0a203ffaed5a3f63242cd064c8fb5892366c103e328079318f78062f24ea8c9d50bc6a47aa3567cabefd824d170e78fa2745ed1f16b132e16436146b7688f19b"
|
||||
"packageManager": "pnpm@10.30.3+sha512.c961d1e0a2d8e354ecaa5166b822516668b7f44cb5bd95122d590dd81922f606f5473b6d23ec4a5be05e7fcd18e8488d47d978bbe981872f1145d06e9a740017"
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"backend/internal/utils"
|
||||
"encoding/json"
|
||||
"pkg/utils"
|
||||
|
||||
"dario.cat/mergo"
|
||||
"github.com/redis/go-redis/v9"
|
||||
@@ -2,7 +2,7 @@ package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"worker/internal/utils"
|
||||
"pkg/utils"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
13
pkg/models/go.mod
Normal file
13
pkg/models/go.mod
Normal file
@@ -0,0 +1,13 @@
|
||||
module pkg/models
|
||||
|
||||
go 1.25.5
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.2
|
||||
github.com/redis/go-redis/v9 v9.17.3
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
)
|
||||
10
pkg/models/go.sum
Normal file
10
pkg/models/go.sum
Normal file
@@ -0,0 +1,10 @@
|
||||
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/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
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/redis/go-redis/v9 v9.17.3 h1:fN29NdNrE17KttK5Ndf20buqfDZwGNgoUr9qjl1DQx4=
|
||||
github.com/redis/go-redis/v9 v9.17.3/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
||||
@@ -1,10 +1,11 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"backend/internal/utils"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"pkg/utils"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"backend/internal/utils"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"pkg/utils"
|
||||
|
||||
"dario.cat/mergo"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
@@ -1,9 +1,10 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"backend/internal/utils"
|
||||
"encoding/json"
|
||||
|
||||
"pkg/utils"
|
||||
|
||||
"dario.cat/mergo"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
@@ -4,7 +4,8 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
"worker/internal/utils"
|
||||
|
||||
"pkg/utils"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
22
pkg/services/file.go
Normal file
22
pkg/services/file.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"pkg/utils"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
)
|
||||
|
||||
func SetFileRemoveTask(fileId string, expire time.Duration) error {
|
||||
client := utils.GetQueueClient()
|
||||
json, err := json.Marshal(map[string]any{
|
||||
"file_id": fileId,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = client.Enqueue(asynq.NewTask("file:remove", json), asynq.ProcessIn(time.Duration(expire)*time.Second))
|
||||
return err
|
||||
}
|
||||
17
pkg/services/go.mod
Normal file
17
pkg/services/go.mod
Normal file
@@ -0,0 +1,17 @@
|
||||
module pkg/services
|
||||
|
||||
go 1.25.5
|
||||
|
||||
require github.com/hibiken/asynq v0.25.1
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/redis/go-redis/v9 v9.17.3 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
)
|
||||
18
pkg/services/go.sum
Normal file
18
pkg/services/go.sum
Normal file
@@ -0,0 +1,18 @@
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/hibiken/asynq v0.25.1 h1:phj028N0nm15n8O2ims+IvJ2gz4k2auvermngh9JhTw=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/redis/go-redis/v9 v9.17.3 h1:fN29NdNrE17KttK5Ndf20buqfDZwGNgoUr9qjl1DQx4=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
99
pkg/utils/env.go
Normal file
99
pkg/utils/env.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var (
|
||||
v *viper.Viper
|
||||
envOnce sync.Once
|
||||
)
|
||||
|
||||
func InitEnv(props EnvOption) {
|
||||
if v != nil {
|
||||
return
|
||||
}
|
||||
envOnce.Do(func() {
|
||||
v = viper.New()
|
||||
for _, viperConfigType := range props.ConfigType {
|
||||
v.SetConfigType(viperConfigType)
|
||||
}
|
||||
if props.ConfigData != nil {
|
||||
v.ReadConfig(props.ConfigData)
|
||||
return
|
||||
}
|
||||
for _, name := range props.ConfigName {
|
||||
v.SetConfigName(name)
|
||||
}
|
||||
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
for _, path := range props.ConfigPath {
|
||||
v.AddConfigPath(path)
|
||||
}
|
||||
v.AutomaticEnv()
|
||||
v.WatchConfig()
|
||||
err := v.ReadInConfig()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type Option interface {
|
||||
applyTo(*EnvOption)
|
||||
}
|
||||
|
||||
type EnvOption struct {
|
||||
DefaultValue string
|
||||
ConfigPath []string
|
||||
ConfigName []string
|
||||
ConfigType []string
|
||||
ConfigData io.Reader // 测试环境使用
|
||||
}
|
||||
|
||||
type WithDefaultValue string
|
||||
|
||||
func (o WithDefaultValue) applyTo(props *EnvOption) {
|
||||
props.DefaultValue = string(o)
|
||||
}
|
||||
|
||||
func getEnvOptions(options ...Option) EnvOption {
|
||||
props := EnvOption{
|
||||
DefaultValue: "",
|
||||
ConfigPath: []string{".", "../"},
|
||||
ConfigName: []string{"config"},
|
||||
ConfigType: []string{"yaml"},
|
||||
}
|
||||
for _, option := range options {
|
||||
option.applyTo(&props)
|
||||
}
|
||||
return props
|
||||
}
|
||||
|
||||
func GetEnv(key string, options ...Option) string {
|
||||
props := getEnvOptions(options...)
|
||||
InitEnv(props)
|
||||
value := v.GetString(key)
|
||||
|
||||
if value == "" && props.DefaultValue != "" {
|
||||
return props.DefaultValue
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func GetEnvWithDefault(key string, defaultValue string) string {
|
||||
return GetEnv(key, WithDefaultValue(defaultValue))
|
||||
}
|
||||
|
||||
func GetEnvMapString(key string) map[string]string {
|
||||
props := getEnvOptions()
|
||||
InitEnv(props)
|
||||
return v.GetStringMapString(key)
|
||||
}
|
||||
|
||||
func SetEnv(key string, value string) {
|
||||
v.Set(key, value)
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
)
|
||||
|
||||
func GetFileId(fileHash string, fileSize int64) string {
|
||||
@@ -14,11 +16,11 @@ func GetFileId(fileHash string, fileSize int64) string {
|
||||
}
|
||||
|
||||
func GetFileMd5(file io.Reader) (string, error) {
|
||||
|
||||
const bufferSize = 1024 * 1000 // 1MB
|
||||
|
||||
hash := md5.New()
|
||||
for buf, reader := make([]byte, bufferSize), bufio.NewReader(file); ; {
|
||||
buf := make([]byte, bufferSize)
|
||||
reader := bufio.NewReader(file)
|
||||
for {
|
||||
n, err := reader.Read(buf)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
@@ -26,26 +28,30 @@ func GetFileMd5(file io.Reader) (string, error) {
|
||||
}
|
||||
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
|
||||
uploadPath := GetEnv("upload.path")
|
||||
if uploadPath == "" {
|
||||
basepath, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
uploadPath = filepath.Join(basepath, "uploads")
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
func CopyFile(src, dst string) error {
|
||||
sourceFile, err := os.Open(src)
|
||||
if err != nil {
|
||||
35
pkg/utils/go.mod
Normal file
35
pkg/utils/go.mod
Normal file
@@ -0,0 +1,35 @@
|
||||
module pkg/utils
|
||||
|
||||
go 1.25.5
|
||||
|
||||
require (
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/hibiken/asynq v0.25.1
|
||||
github.com/redis/go-redis/v9 v9.17.3
|
||||
github.com/spf13/viper v1.21.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
)
|
||||
|
||||
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.5.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // 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/cast v1.10.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // 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
|
||||
)
|
||||
60
pkg/utils/go.sum
Normal file
60
pkg/utils/go.sum
Normal file
@@ -0,0 +1,60 @@
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
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/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
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.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hibiken/asynq v0.25.1 h1:phj028N0nm15n8O2ims+IvJ2gz4k2auvermngh9JhTw=
|
||||
github.com/hibiken/asynq v0.25.1/go.mod h1:pazWNOLBu0FEynQRBvHA26qdIKRSmfdIfUm4HdsLmXg=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
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.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/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
|
||||
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
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=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
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/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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -2,12 +2,16 @@ package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
var rdb *redis.Client = InitRedis()
|
||||
var ctx = context.Background()
|
||||
var (
|
||||
rdb *redis.Client
|
||||
ctx = context.Background()
|
||||
onceRedis sync.Once
|
||||
)
|
||||
|
||||
func InitRedis() *redis.Client {
|
||||
opt, err := redis.ParseURL(GetEnv("redis.url"))
|
||||
@@ -18,5 +22,10 @@ func InitRedis() *redis.Client {
|
||||
}
|
||||
|
||||
func GetRedisClient() (*redis.Client, context.Context) {
|
||||
onceRedis.Do(func() {
|
||||
if rdb == nil {
|
||||
rdb = InitRedis()
|
||||
}
|
||||
})
|
||||
return rdb, ctx
|
||||
}
|
||||
39
pkg/utils/test/env_test.go
Normal file
39
pkg/utils/test/env_test.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"pkg/utils"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestInitEnvAndGetEnv(t *testing.T) {
|
||||
jsonData := `
|
||||
{
|
||||
"test": {
|
||||
"value": "foobar",
|
||||
"empty": null
|
||||
}
|
||||
}
|
||||
`
|
||||
props := utils.EnvOption{
|
||||
ConfigData: bytes.NewBufferString(jsonData),
|
||||
ConfigType: []string{"json"},
|
||||
}
|
||||
utils.InitEnv(props)
|
||||
|
||||
// GetEnv应能拿到值
|
||||
val := utils.GetEnv("test.value")
|
||||
assert.Equal(t, "foobar", val)
|
||||
|
||||
// GetEnv拿不到值时应该返回空字符串
|
||||
emptyVal := utils.GetEnv("test.empty")
|
||||
assert.Equal(t, "", emptyVal)
|
||||
|
||||
// GetEnv拿不到值且有默认值时应该返回默认值
|
||||
notExistKey := "test.not_exist"
|
||||
valWithDefault := utils.GetEnvWithDefault(notExistKey, "defaultbar")
|
||||
assert.Equal(t, "defaultbar", valWithDefault)
|
||||
}
|
||||
6515
pnpm-lock.yaml
generated
6515
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user