28 Commits

Author SHA1 Message Date
30f9abbd0d fix: v0.1.2
All checks were successful
/ build ushare (push) Successful in 47s
/ clean (push) Successful in 1s
2025-05-11 10:42:44 +08:00
b2e3649a10 fix: v0.1.2
All checks were successful
/ build ushare (push) Successful in 10s
/ clean (push) Successful in 0s
2025-05-11 10:38:10 +08:00
205e436712 fix: v0.1.2
Some checks failed
/ build ushare (push) Failing after 6s
/ clean (push) Successful in 0s
2025-05-11 10:35:09 +08:00
467cc95bc7 fix: v0.1.2
Some checks failed
/ build ushare (push) Failing after 5s
/ clean (push) Successful in 0s
2025-05-11 10:25:26 +08:00
baa3098253 fix: v0.1.2
All checks were successful
/ build ushare (push) Successful in 3s
/ clean (push) Successful in 1s
2025-05-11 10:15:31 +08:00
9eed7ecaca fix: v0.1.2
All checks were successful
/ build ushare (push) Successful in 1m13s
/ clean (push) Successful in 0s
2025-05-11 10:10:57 +08:00
6a99e9ba6c fix: v0.1.2
All checks were successful
/ build ushare (push) Successful in 3s
/ clean (push) Successful in 0s
2025-05-10 11:54:08 +08:00
9d96ea1f70 fix: v0.1.2
Some checks failed
/ build ushare (push) Failing after 14s
/ clean (push) Successful in 0s
2025-05-10 11:51:28 +08:00
0dd72d0f53 fix: v0.1.2
All checks were successful
/ build and run u-api (push) Successful in 8s
/ clean (push) Successful in 0s
2025-05-10 11:35:29 +08:00
58564f9d5e fix: v0.1.2
Some checks failed
/ build and run u-api (push) Failing after 2s
/ clean (push) Successful in 0s
2025-05-10 11:13:24 +08:00
5c06d03f04 fix: v0.1.2
Some checks failed
/ build and run u-api (push) Failing after 2s
/ clean (push) Successful in 0s
2025-05-10 11:07:46 +08:00
b207b64466 fix: v0.1.2
Some checks failed
/ build and run u-api (push) Failing after 3s
/ clean (push) Successful in 0s
2025-05-10 11:06:22 +08:00
139401768f fix: v0.1.2
Some checks failed
/ build and run u-api (push) Failing after 3s
/ clean (push) Successful in 0s
2025-05-10 11:04:45 +08:00
6da6688677 fix: v0.1.2
All checks were successful
/ build and run u-api (push) Successful in 2s
/ clean (push) Successful in 1s
2025-05-10 10:51:13 +08:00
6e5bf040dc fix: v0.1.2
Some checks failed
/ build and run u-api (push) Failing after 2s
/ clean (push) Successful in 0s
2025-05-09 23:06:21 +08:00
1a8827a67a fix: v0.1.2
Some checks failed
/ build and run u-api (push) Failing after 2s
/ clean (push) Successful in 0s
2025-05-09 23:03:46 +08:00
761b1d3b27 fix: v0.1.2
Some checks failed
/ build and run u-api (push) Failing after 3s
/ clean (push) Successful in 0s
2025-05-09 22:52:32 +08:00
fb66b2c359 fix: v0.1.2
Some checks failed
/ build and run u-api (push) Failing after 3s
/ clean (push) Successful in 0s
2025-05-09 22:45:32 +08:00
ad1b096a3b fix: v0.1.2
Some checks failed
/ build and run u-api (push) Failing after 3s
/ clean (push) Successful in 0s
2025-05-09 22:44:31 +08:00
2d992e36ec fix: v0.1.2
Some checks failed
/ build and run u-api (push) Failing after 2s
/ clean (push) Successful in 0s
2025-05-09 22:11:54 +08:00
91f3da635d fix: v0.1.2
Some checks failed
/ build and run u-api (push) Failing after 2s
/ clean (push) Successful in 0s
2025-05-09 22:10:18 +08:00
93c4339039 init: 0.1.2
Some checks failed
/ build and run u-api (push) Failing after 8s
/ clean (push) Successful in 0s
feat: login page && auth required
todo: file dir cleanup
2025-05-09 17:49:12 +08:00
e5cf2c1367 cache 2025-05-09 11:22:27 +08:00
0e984c46b4 init: 0.1.1
feat: login page && auth required
todo: 断点上传
2025-05-08 18:02:49 +08:00
440aa96ad6 init: 0.1.0
feat: 基本功能有了
  1. 分片上传
  2. code 回显
  3. 下载
todo: 断点上传
2025-05-06 18:04:20 +08:00
cfd4e8cb6d init: 0.0.8
feat: 实现了分片上传
todo:
  1. 上传完成后 code 回显
  2. 清理不用的临时 code file
  3. 断点续传
  4. 多 chunk 并发上传
2025-04-30 18:00:29 +08:00
b8fda4322f init: 0.0.7
next: upload
2025-04-28 17:59:07 +08:00
0cfd617922 init: 0.0.6
next: upload
2025-04-28 14:59:22 +08:00
50 changed files with 3188 additions and 378 deletions

2
.dockerignore Normal file
View File

@ -0,0 +1,2 @@
node_modules
dist

View File

@ -0,0 +1,59 @@
run-name: build ushare
on:
push:
tags:
- 'v*'
jobs:
build ushare:
runs-on: tencent-sg
steps:
- name: prepare enviroment
uses: actions/checkout@v4
- name: prints date
run: date '+%Y-%m-%dT%H:%M:%S'
- name: print operator
run: whoami
- name: print tag name
run: echo "Tag name = ${{ gitea.ref_name }}"
- name: build prepare config
run: |
cat << EOF > .docker.config.json
${{ secrets.DOCKER_CONFIG }}
EOF
- name: print work dir and files
run: pwd & ls -alsh .
- name: build image by docker build
run: docker build -t gitea.loveuer.com/loveuer/build/ushare:${{ gitea.ref_name }} .
- name: login repository
run: echo ${{ secrets.DOCKER_REPOSITORY_PASSWORD }} | docker login --username loveuer --password-stdin gitea.loveuer.com/loveuer
- name: push image to repository
run: docker push gitea.loveuer.com/loveuer/build/ushare:${{ gitea.ref_name }}
# - name: build by kaniko in docker
# run: |
# docker run --rm -v $(pwd):/workspace \
# -v $(pwd)/.docker.config.json:/kaniko/.docker/config.json:ro \
# alpine:latest \
# ls -alsh /workspace
# gcr.io/kaniko-project/executor:latest \
# --dockerfile=/workspace/Dockerfile \
# --context=/workspace \
# --destination=gitea.loveuer.com/loveuer/build/u-api:${{ gitea.ref_name }} \
# --single-snapshot
clean:
if: always()
runs-on: tencent-sg
steps:
- name: clean docker config
run: |
rm -rf .docker.config.json

35
Dockerfile Normal file
View File

@ -0,0 +1,35 @@
# 第一阶段:构建前端
FROM node:20-alpine AS frontend-builder
RUN npm install -g pnpm --registry=https://registry.npmmirror.com
COPY frontend /app/frontend
WORKDIR /app/frontend
RUN pnpm install --registry=https://registry.npmmirror.com
RUN pnpm run build
# 第二阶段:构建 Golang 后端
FROM golang:alpine AS backend-builder
WORKDIR /app
ENV CGO_ENABLED=0
ENV GOOS=linux
ENV GOPROXY=https://goproxy.cn
COPY go.mod /app/go.mod
COPY go.sum /app/go.sum
RUN go mod download
COPY main.go /app/main.go
COPY internal /app/internal
RUN go build -ldflags '-s -w' -o ushare .
# 第三阶段:生成最终镜像
FROM nginx:alpine
COPY --from=frontend-builder /app/frontend/dist /usr/share/nginx/html
COPY --from=backend-builder /app/ushare /usr/local/bin/ushare
# 配置 Nginx
RUN rm /etc/nginx/conf.d/default.conf
COPY deployment/nginx.conf /etc/nginx/conf.d
# 开放端口
EXPOSE 80
# 启动服务
CMD ["sh", "-c", "ushare & nginx -g 'daemon off;'"]

22
deployment/nginx.conf Normal file
View File

@ -0,0 +1,22 @@
server {
listen 80;
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://localhost:9119;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /ushare {
proxy_pass http://localhost:9119;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

View File

@ -1,13 +1,23 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <!-- Emoji 图标方案 -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <link rel="icon"
<title>Vite + React + TS</title> href="data:image/svg+xml,
</head> <svg xmlns='http://www.w3.org/2000/svg'
<body> viewBox='0 0 100 100'>
<div id="root"></div> <text y='.9em'
<script type="module" src="/src/main.tsx"></script> font-size='90'
</body> style='font-family: Arial, sans-serif'>
</html> 🗃️
</text>
</svg>">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>UShare</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -10,10 +10,11 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.1.4",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"tailwindcss": "^4.1.4" "react-jss": "^10.10.0",
"react-router-dom": "^7.5.3",
"zustand": "^5.0.3"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.22.0", "@eslint/js": "^9.22.0",

545
frontend/pnpm-lock.yaml generated
View File

@ -8,18 +8,21 @@ importers:
.: .:
dependencies: dependencies:
'@tailwindcss/vite':
specifier: ^4.1.4
version: 4.1.4(vite@6.3.3(jiti@2.4.2)(lightningcss@1.29.2))
react: react:
specifier: ^19.0.0 specifier: ^19.0.0
version: 19.1.0 version: 19.1.0
react-dom: react-dom:
specifier: ^19.0.0 specifier: ^19.0.0
version: 19.1.0(react@19.1.0) version: 19.1.0(react@19.1.0)
tailwindcss: react-jss:
specifier: ^4.1.4 specifier: ^10.10.0
version: 4.1.4 version: 10.10.0(react@19.1.0)
react-router-dom:
specifier: ^7.5.3
version: 7.5.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
zustand:
specifier: ^5.0.3
version: 5.0.3(@types/react@19.1.2)(react@19.1.0)
devDependencies: devDependencies:
'@eslint/js': '@eslint/js':
specifier: ^9.22.0 specifier: ^9.22.0
@ -128,6 +131,10 @@ packages:
peerDependencies: peerDependencies:
'@babel/core': ^7.0.0-0 '@babel/core': ^7.0.0-0
'@babel/runtime@7.27.0':
resolution: {integrity: sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==}
engines: {node: '>=6.9.0'}
'@babel/template@7.27.0': '@babel/template@7.27.0':
resolution: {integrity: sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==} resolution: {integrity: sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@ -140,6 +147,12 @@ packages:
resolution: {integrity: sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==} resolution: {integrity: sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@emotion/is-prop-valid@0.7.3':
resolution: {integrity: sha512-uxJqm/sqwXw3YPA5GXX365OBcJGFtxUVkB6WyezqFHlNe9jqUWH5ur2O2M8dGBz61kn1g3ZBlzUunFQXQIClhA==}
'@emotion/memoize@0.7.1':
resolution: {integrity: sha512-Qv4LTqO11jepd5Qmlp3M1YEjBumoTHcHFdgPTQ+sFlIL5myi/7xu/POwP7IRu6odBdmLXdtIs1D6TuW6kbwbbg==}
'@esbuild/aix-ppc64@0.25.3': '@esbuild/aix-ppc64@0.25.3':
resolution: {integrity: sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==} resolution: {integrity: sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
@ -489,100 +502,6 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@tailwindcss/node@4.1.4':
resolution: {integrity: sha512-MT5118zaiO6x6hNA04OWInuAiP1YISXql8Z+/Y8iisV5nuhM8VXlyhRuqc2PEviPszcXI66W44bCIk500Oolhw==}
'@tailwindcss/oxide-android-arm64@4.1.4':
resolution: {integrity: sha512-xMMAe/SaCN/vHfQYui3fqaBDEXMu22BVwQ33veLc8ep+DNy7CWN52L+TTG9y1K397w9nkzv+Mw+mZWISiqhmlA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [android]
'@tailwindcss/oxide-darwin-arm64@4.1.4':
resolution: {integrity: sha512-JGRj0SYFuDuAGilWFBlshcexev2hOKfNkoX+0QTksKYq2zgF9VY/vVMq9m8IObYnLna0Xlg+ytCi2FN2rOL0Sg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@tailwindcss/oxide-darwin-x64@4.1.4':
resolution: {integrity: sha512-sdDeLNvs3cYeWsEJ4H1DvjOzaGios4QbBTNLVLVs0XQ0V95bffT3+scptzYGPMjm7xv4+qMhCDrkHwhnUySEzA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@tailwindcss/oxide-freebsd-x64@4.1.4':
resolution: {integrity: sha512-VHxAqxqdghM83HslPhRsNhHo91McsxRJaEnShJOMu8mHmEj9Ig7ToHJtDukkuLWLzLboh2XSjq/0zO6wgvykNA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [freebsd]
'@tailwindcss/oxide-linux-arm-gnueabihf@4.1.4':
resolution: {integrity: sha512-OTU/m/eV4gQKxy9r5acuesqaymyeSCnsx1cFto/I1WhPmi5HDxX1nkzb8KYBiwkHIGg7CTfo/AcGzoXAJBxLfg==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
'@tailwindcss/oxide-linux-arm64-gnu@4.1.4':
resolution: {integrity: sha512-hKlLNvbmUC6z5g/J4H+Zx7f7w15whSVImokLPmP6ff1QqTVE+TxUM9PGuNsjHvkvlHUtGTdDnOvGNSEUiXI1Ww==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-arm64-musl@4.1.4':
resolution: {integrity: sha512-X3As2xhtgPTY/m5edUtddmZ8rCruvBvtxYLMw9OsZdH01L2gS2icsHRwxdU0dMItNfVmrBezueXZCHxVeeb7Aw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@tailwindcss/oxide-linux-x64-gnu@4.1.4':
resolution: {integrity: sha512-2VG4DqhGaDSmYIu6C4ua2vSLXnJsb/C9liej7TuSO04NK+JJJgJucDUgmX6sn7Gw3Cs5ZJ9ZLrnI0QRDOjLfNQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-x64-musl@4.1.4':
resolution: {integrity: sha512-v+mxVgH2kmur/X5Mdrz9m7TsoVjbdYQT0b4Z+dr+I4RvreCNXyCFELZL/DO0M1RsidZTrm6O1eMnV6zlgEzTMQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@tailwindcss/oxide-wasm32-wasi@4.1.4':
resolution: {integrity: sha512-2TLe9ir+9esCf6Wm+lLWTMbgklIjiF0pbmDnwmhR9MksVOq+e8aP3TSsXySnBDDvTTVd/vKu1aNttEGj3P6l8Q==}
engines: {node: '>=14.0.0'}
cpu: [wasm32]
bundledDependencies:
- '@napi-rs/wasm-runtime'
- '@emnapi/core'
- '@emnapi/runtime'
- '@tybys/wasm-util'
- '@emnapi/wasi-threads'
- tslib
'@tailwindcss/oxide-win32-arm64-msvc@4.1.4':
resolution: {integrity: sha512-VlnhfilPlO0ltxW9/BgfLI5547PYzqBMPIzRrk4W7uupgCt8z6Trw/tAj6QUtF2om+1MH281Pg+HHUJoLesmng==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@tailwindcss/oxide-win32-x64-msvc@4.1.4':
resolution: {integrity: sha512-+7S63t5zhYjslUGb8NcgLpFXD+Kq1F/zt5Xv5qTv7HaFTG/DHyHD9GA6ieNAxhgyA4IcKa/zy7Xx4Oad2/wuhw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@tailwindcss/oxide@4.1.4':
resolution: {integrity: sha512-p5wOpXyOJx7mKh5MXh5oKk+kqcz8T+bA3z/5VWWeQwFrmuBItGwz8Y2CHk/sJ+dNb9B0nYFfn0rj/cKHZyjahQ==}
engines: {node: '>= 10'}
'@tailwindcss/vite@4.1.4':
resolution: {integrity: sha512-4UQeMrONbvrsXKXXp/uxmdEN5JIJ9RkH7YVzs6AMxC/KC1+Np7WZBaNIco7TEjlkthqxZbt8pU/ipD+hKjm80A==}
peerDependencies:
vite: ^5.2.0 || ^6
'@types/babel__core@7.20.5': '@types/babel__core@7.20.5':
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
@ -724,10 +643,20 @@ packages:
convert-source-map@2.0.0: convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
cookie@1.0.2:
resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
engines: {node: '>=18'}
cross-spawn@7.0.6: cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
css-jss@10.10.0:
resolution: {integrity: sha512-YyMIS/LsSKEGXEaVJdjonWe18p4vXLo8CMA4FrW/kcaEyqdIGKCFXao31gbJddXEdIxSXFFURWrenBJPlKTgAA==}
css-vendor@2.0.8:
resolution: {integrity: sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==}
csstype@3.1.3: csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
@ -750,10 +679,6 @@ packages:
electron-to-chromium@1.5.143: electron-to-chromium@1.5.143:
resolution: {integrity: sha512-QqklJMOFBMqe46k8iIOwA9l2hz57V2OKMmP5eSWcUvwx+mASAsbU+wkF1pHjn9ZVSBPrsYWr4/W/95y5SwYg2g==} resolution: {integrity: sha512-QqklJMOFBMqe46k8iIOwA9l2hz57V2OKMmP5eSWcUvwx+mASAsbU+wkF1pHjn9ZVSBPrsYWr4/W/95y5SwYg2g==}
enhanced-resolve@5.18.1:
resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==}
engines: {node: '>=10.13.0'}
esbuild@0.25.3: esbuild@0.25.3:
resolution: {integrity: sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==} resolution: {integrity: sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==}
engines: {node: '>=18'} engines: {node: '>=18'}
@ -892,9 +817,6 @@ packages:
resolution: {integrity: sha512-iInW14XItCXET01CQFqudPOWP2jYMl7T+QRQT+UNcR/iQncN/F0UNpgd76iFkBPgNQb4+X3LV9tLJYzwh+Gl3A==} resolution: {integrity: sha512-iInW14XItCXET01CQFqudPOWP2jYMl7T+QRQT+UNcR/iQncN/F0UNpgd76iFkBPgNQb4+X3LV9tLJYzwh+Gl3A==}
engines: {node: '>=18'} engines: {node: '>=18'}
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
graphemer@1.4.0: graphemer@1.4.0:
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
@ -902,6 +824,12 @@ packages:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
hoist-non-react-statics@3.3.2:
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
hyphenate-style-name@1.1.0:
resolution: {integrity: sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==}
ignore@5.3.2: ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'} engines: {node: '>= 4'}
@ -922,6 +850,9 @@ packages:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
is-in-browser@1.1.3:
resolution: {integrity: sha512-FeXIBgG/CPGd/WUxuEyvgGTEfwiG9Z4EKGxjNMRqviiIIfsmgrpnHLffEDdwUHqNva1VEW91o3xBT/m8Elgl9g==}
is-number@7.0.0: is-number@7.0.0:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'} engines: {node: '>=0.12.0'}
@ -959,6 +890,48 @@ packages:
engines: {node: '>=6'} engines: {node: '>=6'}
hasBin: true hasBin: true
jss-plugin-camel-case@10.10.0:
resolution: {integrity: sha512-z+HETfj5IYgFxh1wJnUAU8jByI48ED+v0fuTuhKrPR+pRBYS2EDwbusU8aFOpCdYhtRc9zhN+PJ7iNE8pAWyPw==}
jss-plugin-compose@10.10.0:
resolution: {integrity: sha512-F5kgtWpI2XfZ3Z8eP78tZEYFdgTIbpA/TMuX3a8vwrNolYtN1N4qJR/Ob0LAsqIwCMLojtxN7c7Oo/+Vz6THow==}
jss-plugin-default-unit@10.10.0:
resolution: {integrity: sha512-SvpajxIECi4JDUbGLefvNckmI+c2VWmP43qnEy/0eiwzRUsafg5DVSIWSzZe4d2vFX1u9nRDP46WCFV/PXVBGQ==}
jss-plugin-expand@10.10.0:
resolution: {integrity: sha512-ymT62W2OyDxBxr7A6JR87vVX9vTq2ep5jZLIdUSusfBIEENLdkkc0lL/Xaq8W9s3opUq7R0sZQpzRWELrfVYzA==}
jss-plugin-extend@10.10.0:
resolution: {integrity: sha512-sKYrcMfr4xxigmIwqTjxNcHwXJIfvhvjTNxF+Tbc1NmNdyspGW47Ey6sGH8BcQ4FFQhLXctpWCQSpDwdNmXSwg==}
jss-plugin-global@10.10.0:
resolution: {integrity: sha512-icXEYbMufiNuWfuazLeN+BNJO16Ge88OcXU5ZDC2vLqElmMybA31Wi7lZ3lf+vgufRocvPj8443irhYRgWxP+A==}
jss-plugin-nested@10.10.0:
resolution: {integrity: sha512-9R4JHxxGgiZhurDo3q7LdIiDEgtA1bTGzAbhSPyIOWb7ZubrjQe8acwhEQ6OEKydzpl8XHMtTnEwHXCARLYqYA==}
jss-plugin-props-sort@10.10.0:
resolution: {integrity: sha512-5VNJvQJbnq/vRfje6uZLe/FyaOpzP/IH1LP+0fr88QamVrGJa0hpRRyAa0ea4U/3LcorJfBFVyC4yN2QC73lJg==}
jss-plugin-rule-value-function@10.10.0:
resolution: {integrity: sha512-uEFJFgaCtkXeIPgki8ICw3Y7VMkL9GEan6SqmT9tqpwM+/t+hxfMUdU4wQ0MtOiMNWhwnckBV0IebrKcZM9C0g==}
jss-plugin-rule-value-observable@10.10.0:
resolution: {integrity: sha512-ZLMaYrR3QE+vD7nl3oNXuj79VZl9Kp8/u6A1IbTPDcuOu8b56cFdWRZNZ0vNr8jHewooEeq2doy8Oxtymr2ZPA==}
jss-plugin-template@10.10.0:
resolution: {integrity: sha512-ocXZBIOJOA+jISPdsgkTs8wwpK6UbsvtZK5JI7VUggTD6LWKbtoxUzadd2TpfF+lEtlhUmMsCkTRNkITdPKa6w==}
jss-plugin-vendor-prefixer@10.10.0:
resolution: {integrity: sha512-UY/41WumgjW8r1qMCO8l1ARg7NHnfRVWRhZ2E2m0DMYsr2DD91qIXLyNhiX83hHswR7Wm4D+oDYNC1zWCJWtqg==}
jss-preset-default@10.10.0:
resolution: {integrity: sha512-GL175Wt2FGhjE+f+Y3aWh+JioL06/QWFgZp53CbNNq6ZkVU0TDplD8Bxm9KnkotAYn3FlplNqoW5CjyLXcoJ7Q==}
jss@10.10.0:
resolution: {integrity: sha512-cqsOTS7jqPsPMjtKYDUpdFC0AbhYFLTcuGRqymgmdJIeQ8cH7+AgX7YSgQy79wXloZq2VvATYxUOUQEvS1V/Zw==}
keyv@4.5.4: keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
@ -1041,6 +1014,10 @@ packages:
lodash.merge@4.6.2: lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
loose-envify@1.4.0:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
lru-cache@5.1.1: lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
@ -1073,6 +1050,10 @@ packages:
node-releases@2.0.19: node-releases@2.0.19:
resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
optionator@0.9.4: optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
@ -1116,6 +1097,9 @@ packages:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
punycode@2.3.1: punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -1123,19 +1107,50 @@ packages:
queue-microtask@1.2.3: queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
react-display-name@0.2.5:
resolution: {integrity: sha512-I+vcaK9t4+kypiSgaiVWAipqHRXYmZIuAiS8vzFvXHHXVigg/sMKwlRgLy6LH2i3rmP+0Vzfl5lFsFRwF1r3pg==}
react-dom@19.1.0: react-dom@19.1.0:
resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==} resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==}
peerDependencies: peerDependencies:
react: ^19.1.0 react: ^19.1.0
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
react-jss@10.10.0:
resolution: {integrity: sha512-WLiq84UYWqNBF6579/uprcIUnM1TSywYq6AIjKTTTG5ziJl9Uy+pwuvpN3apuyVwflMbD60PraeTKT7uWH9XEQ==}
peerDependencies:
react: '>=16.8.6'
react-refresh@0.17.0: react-refresh@0.17.0:
resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
react-router-dom@7.5.3:
resolution: {integrity: sha512-cK0jSaTyW4jV9SRKAItMIQfWZ/D6WEZafgHuuCb9g+SjhLolY78qc+De4w/Cz9ybjvLzShAmaIMEXt8iF1Cm+A==}
engines: {node: '>=20.0.0'}
peerDependencies:
react: '>=18'
react-dom: '>=18'
react-router@7.5.3:
resolution: {integrity: sha512-3iUDM4/fZCQ89SXlDa+Ph3MevBrozBAI655OAfWQlTm9nBR0IKlrmNwFow5lPHttbwvITZfkeeeZFP6zt3F7pw==}
engines: {node: '>=20.0.0'}
peerDependencies:
react: '>=18'
react-dom: '>=18'
peerDependenciesMeta:
react-dom:
optional: true
react@19.1.0: react@19.1.0:
resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
regenerator-runtime@0.14.1:
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
resolve-from@4.0.0: resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'} engines: {node: '>=4'}
@ -1164,6 +1179,12 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
hasBin: true hasBin: true
set-cookie-parser@2.7.1:
resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==}
shallow-equal@1.2.1:
resolution: {integrity: sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==}
shebang-command@2.0.0: shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -1184,12 +1205,18 @@ packages:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'} engines: {node: '>=8'}
tailwindcss@4.1.4: symbol-observable@1.2.0:
resolution: {integrity: sha512-1ZIUqtPITFbv/DxRmDr5/agPqJwF69d24m9qmM1939TJehgY539CtzeZRjbLt5G6fSy/7YqqYsfvoTEw9xUI2A==} resolution: {integrity: sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==}
engines: {node: '>=0.10.0'}
tapable@2.2.1: theming@3.3.0:
resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} resolution: {integrity: sha512-u6l4qTJRDaWZsqa8JugaNt7Xd8PPl9+gonZaIe28vAhqgHMIG/DOyFPqiKN/gQLQYj05tHv+YQdNILL4zoiAVA==}
engines: {node: '>=6'} engines: {node: '>=8'}
peerDependencies:
react: '>=16.3'
tiny-warning@1.0.3:
resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==}
tinyglobby@0.2.13: tinyglobby@0.2.13:
resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==} resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==}
@ -1205,6 +1232,9 @@ packages:
peerDependencies: peerDependencies:
typescript: '>=4.8.4' typescript: '>=4.8.4'
turbo-stream@2.4.0:
resolution: {integrity: sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==}
type-check@0.4.0: type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
@ -1286,6 +1316,24 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'} engines: {node: '>=10'}
zustand@5.0.3:
resolution: {integrity: sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==}
engines: {node: '>=12.20.0'}
peerDependencies:
'@types/react': '>=18.0.0'
immer: '>=9.0.6'
react: '>=18.0.0'
use-sync-external-store: '>=1.2.0'
peerDependenciesMeta:
'@types/react':
optional: true
immer:
optional: true
react:
optional: true
use-sync-external-store:
optional: true
snapshots: snapshots:
'@ampproject/remapping@2.3.0': '@ampproject/remapping@2.3.0':
@ -1380,6 +1428,10 @@ snapshots:
'@babel/core': 7.26.10 '@babel/core': 7.26.10
'@babel/helper-plugin-utils': 7.26.5 '@babel/helper-plugin-utils': 7.26.5
'@babel/runtime@7.27.0':
dependencies:
regenerator-runtime: 0.14.1
'@babel/template@7.27.0': '@babel/template@7.27.0':
dependencies: dependencies:
'@babel/code-frame': 7.26.2 '@babel/code-frame': 7.26.2
@ -1403,6 +1455,12 @@ snapshots:
'@babel/helper-string-parser': 7.25.9 '@babel/helper-string-parser': 7.25.9
'@babel/helper-validator-identifier': 7.25.9 '@babel/helper-validator-identifier': 7.25.9
'@emotion/is-prop-valid@0.7.3':
dependencies:
'@emotion/memoize': 0.7.1
'@emotion/memoize@0.7.1': {}
'@esbuild/aix-ppc64@0.25.3': '@esbuild/aix-ppc64@0.25.3':
optional: true optional: true
@ -1624,71 +1682,6 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.40.0': '@rollup/rollup-win32-x64-msvc@4.40.0':
optional: true optional: true
'@tailwindcss/node@4.1.4':
dependencies:
enhanced-resolve: 5.18.1
jiti: 2.4.2
lightningcss: 1.29.2
tailwindcss: 4.1.4
'@tailwindcss/oxide-android-arm64@4.1.4':
optional: true
'@tailwindcss/oxide-darwin-arm64@4.1.4':
optional: true
'@tailwindcss/oxide-darwin-x64@4.1.4':
optional: true
'@tailwindcss/oxide-freebsd-x64@4.1.4':
optional: true
'@tailwindcss/oxide-linux-arm-gnueabihf@4.1.4':
optional: true
'@tailwindcss/oxide-linux-arm64-gnu@4.1.4':
optional: true
'@tailwindcss/oxide-linux-arm64-musl@4.1.4':
optional: true
'@tailwindcss/oxide-linux-x64-gnu@4.1.4':
optional: true
'@tailwindcss/oxide-linux-x64-musl@4.1.4':
optional: true
'@tailwindcss/oxide-wasm32-wasi@4.1.4':
optional: true
'@tailwindcss/oxide-win32-arm64-msvc@4.1.4':
optional: true
'@tailwindcss/oxide-win32-x64-msvc@4.1.4':
optional: true
'@tailwindcss/oxide@4.1.4':
optionalDependencies:
'@tailwindcss/oxide-android-arm64': 4.1.4
'@tailwindcss/oxide-darwin-arm64': 4.1.4
'@tailwindcss/oxide-darwin-x64': 4.1.4
'@tailwindcss/oxide-freebsd-x64': 4.1.4
'@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.4
'@tailwindcss/oxide-linux-arm64-gnu': 4.1.4
'@tailwindcss/oxide-linux-arm64-musl': 4.1.4
'@tailwindcss/oxide-linux-x64-gnu': 4.1.4
'@tailwindcss/oxide-linux-x64-musl': 4.1.4
'@tailwindcss/oxide-wasm32-wasi': 4.1.4
'@tailwindcss/oxide-win32-arm64-msvc': 4.1.4
'@tailwindcss/oxide-win32-x64-msvc': 4.1.4
'@tailwindcss/vite@4.1.4(vite@6.3.3(jiti@2.4.2)(lightningcss@1.29.2))':
dependencies:
'@tailwindcss/node': 4.1.4
'@tailwindcss/oxide': 4.1.4
tailwindcss: 4.1.4
vite: 6.3.3(jiti@2.4.2)(lightningcss@1.29.2)
'@types/babel__core@7.20.5': '@types/babel__core@7.20.5':
dependencies: dependencies:
'@babel/parser': 7.27.0 '@babel/parser': 7.27.0
@ -1870,12 +1863,25 @@ snapshots:
convert-source-map@2.0.0: {} convert-source-map@2.0.0: {}
cookie@1.0.2: {}
cross-spawn@7.0.6: cross-spawn@7.0.6:
dependencies: dependencies:
path-key: 3.1.1 path-key: 3.1.1
shebang-command: 2.0.0 shebang-command: 2.0.0
which: 2.0.2 which: 2.0.2
css-jss@10.10.0:
dependencies:
'@babel/runtime': 7.27.0
jss: 10.10.0
jss-preset-default: 10.10.0
css-vendor@2.0.8:
dependencies:
'@babel/runtime': 7.27.0
is-in-browser: 1.1.3
csstype@3.1.3: {} csstype@3.1.3: {}
debug@4.4.0: debug@4.4.0:
@ -1884,15 +1890,11 @@ snapshots:
deep-is@0.1.4: {} deep-is@0.1.4: {}
detect-libc@2.0.4: {} detect-libc@2.0.4:
optional: true
electron-to-chromium@1.5.143: {} electron-to-chromium@1.5.143: {}
enhanced-resolve@5.18.1:
dependencies:
graceful-fs: 4.2.11
tapable: 2.2.1
esbuild@0.25.3: esbuild@0.25.3:
optionalDependencies: optionalDependencies:
'@esbuild/aix-ppc64': 0.25.3 '@esbuild/aix-ppc64': 0.25.3
@ -2063,12 +2065,16 @@ snapshots:
globals@16.0.0: {} globals@16.0.0: {}
graceful-fs@4.2.11: {}
graphemer@1.4.0: {} graphemer@1.4.0: {}
has-flag@4.0.0: {} has-flag@4.0.0: {}
hoist-non-react-statics@3.3.2:
dependencies:
react-is: 16.13.1
hyphenate-style-name@1.1.0: {}
ignore@5.3.2: {} ignore@5.3.2: {}
import-fresh@3.3.1: import-fresh@3.3.1:
@ -2084,11 +2090,14 @@ snapshots:
dependencies: dependencies:
is-extglob: 2.1.1 is-extglob: 2.1.1
is-in-browser@1.1.3: {}
is-number@7.0.0: {} is-number@7.0.0: {}
isexe@2.0.0: {} isexe@2.0.0: {}
jiti@2.4.2: {} jiti@2.4.2:
optional: true
js-tokens@4.0.0: {} js-tokens@4.0.0: {}
@ -2106,6 +2115,98 @@ snapshots:
json5@2.2.3: {} json5@2.2.3: {}
jss-plugin-camel-case@10.10.0:
dependencies:
'@babel/runtime': 7.27.0
hyphenate-style-name: 1.1.0
jss: 10.10.0
jss-plugin-compose@10.10.0:
dependencies:
'@babel/runtime': 7.27.0
jss: 10.10.0
tiny-warning: 1.0.3
jss-plugin-default-unit@10.10.0:
dependencies:
'@babel/runtime': 7.27.0
jss: 10.10.0
jss-plugin-expand@10.10.0:
dependencies:
'@babel/runtime': 7.27.0
jss: 10.10.0
jss-plugin-extend@10.10.0:
dependencies:
'@babel/runtime': 7.27.0
jss: 10.10.0
tiny-warning: 1.0.3
jss-plugin-global@10.10.0:
dependencies:
'@babel/runtime': 7.27.0
jss: 10.10.0
jss-plugin-nested@10.10.0:
dependencies:
'@babel/runtime': 7.27.0
jss: 10.10.0
tiny-warning: 1.0.3
jss-plugin-props-sort@10.10.0:
dependencies:
'@babel/runtime': 7.27.0
jss: 10.10.0
jss-plugin-rule-value-function@10.10.0:
dependencies:
'@babel/runtime': 7.27.0
jss: 10.10.0
tiny-warning: 1.0.3
jss-plugin-rule-value-observable@10.10.0:
dependencies:
'@babel/runtime': 7.27.0
jss: 10.10.0
symbol-observable: 1.2.0
jss-plugin-template@10.10.0:
dependencies:
'@babel/runtime': 7.27.0
jss: 10.10.0
tiny-warning: 1.0.3
jss-plugin-vendor-prefixer@10.10.0:
dependencies:
'@babel/runtime': 7.27.0
css-vendor: 2.0.8
jss: 10.10.0
jss-preset-default@10.10.0:
dependencies:
'@babel/runtime': 7.27.0
jss: 10.10.0
jss-plugin-camel-case: 10.10.0
jss-plugin-compose: 10.10.0
jss-plugin-default-unit: 10.10.0
jss-plugin-expand: 10.10.0
jss-plugin-extend: 10.10.0
jss-plugin-global: 10.10.0
jss-plugin-nested: 10.10.0
jss-plugin-props-sort: 10.10.0
jss-plugin-rule-value-function: 10.10.0
jss-plugin-rule-value-observable: 10.10.0
jss-plugin-template: 10.10.0
jss-plugin-vendor-prefixer: 10.10.0
jss@10.10.0:
dependencies:
'@babel/runtime': 7.27.0
csstype: 3.1.3
is-in-browser: 1.1.3
tiny-warning: 1.0.3
keyv@4.5.4: keyv@4.5.4:
dependencies: dependencies:
json-buffer: 3.0.1 json-buffer: 3.0.1
@ -2159,6 +2260,7 @@ snapshots:
lightningcss-linux-x64-musl: 1.29.2 lightningcss-linux-x64-musl: 1.29.2
lightningcss-win32-arm64-msvc: 1.29.2 lightningcss-win32-arm64-msvc: 1.29.2
lightningcss-win32-x64-msvc: 1.29.2 lightningcss-win32-x64-msvc: 1.29.2
optional: true
locate-path@6.0.0: locate-path@6.0.0:
dependencies: dependencies:
@ -2166,6 +2268,10 @@ snapshots:
lodash.merge@4.6.2: {} lodash.merge@4.6.2: {}
loose-envify@1.4.0:
dependencies:
js-tokens: 4.0.0
lru-cache@5.1.1: lru-cache@5.1.1:
dependencies: dependencies:
yallist: 3.1.1 yallist: 3.1.1
@ -2193,6 +2299,8 @@ snapshots:
node-releases@2.0.19: {} node-releases@2.0.19: {}
object-assign@4.1.1: {}
optionator@0.9.4: optionator@0.9.4:
dependencies: dependencies:
deep-is: 0.1.4 deep-is: 0.1.4
@ -2232,19 +2340,61 @@ snapshots:
prelude-ls@1.2.1: {} prelude-ls@1.2.1: {}
prop-types@15.8.1:
dependencies:
loose-envify: 1.4.0
object-assign: 4.1.1
react-is: 16.13.1
punycode@2.3.1: {} punycode@2.3.1: {}
queue-microtask@1.2.3: {} queue-microtask@1.2.3: {}
react-display-name@0.2.5: {}
react-dom@19.1.0(react@19.1.0): react-dom@19.1.0(react@19.1.0):
dependencies: dependencies:
react: 19.1.0 react: 19.1.0
scheduler: 0.26.0 scheduler: 0.26.0
react-is@16.13.1: {}
react-jss@10.10.0(react@19.1.0):
dependencies:
'@babel/runtime': 7.27.0
'@emotion/is-prop-valid': 0.7.3
css-jss: 10.10.0
hoist-non-react-statics: 3.3.2
is-in-browser: 1.1.3
jss: 10.10.0
jss-preset-default: 10.10.0
prop-types: 15.8.1
react: 19.1.0
shallow-equal: 1.2.1
theming: 3.3.0(react@19.1.0)
tiny-warning: 1.0.3
react-refresh@0.17.0: {} react-refresh@0.17.0: {}
react-router-dom@7.5.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
react-router: 7.5.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
react-router@7.5.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
cookie: 1.0.2
react: 19.1.0
set-cookie-parser: 2.7.1
turbo-stream: 2.4.0
optionalDependencies:
react-dom: 19.1.0(react@19.1.0)
react@19.1.0: {} react@19.1.0: {}
regenerator-runtime@0.14.1: {}
resolve-from@4.0.0: {} resolve-from@4.0.0: {}
reusify@1.1.0: {} reusify@1.1.0: {}
@ -2285,6 +2435,10 @@ snapshots:
semver@7.7.1: {} semver@7.7.1: {}
set-cookie-parser@2.7.1: {}
shallow-equal@1.2.1: {}
shebang-command@2.0.0: shebang-command@2.0.0:
dependencies: dependencies:
shebang-regex: 3.0.0 shebang-regex: 3.0.0
@ -2299,9 +2453,17 @@ snapshots:
dependencies: dependencies:
has-flag: 4.0.0 has-flag: 4.0.0
tailwindcss@4.1.4: {} symbol-observable@1.2.0: {}
tapable@2.2.1: {} theming@3.3.0(react@19.1.0):
dependencies:
hoist-non-react-statics: 3.3.2
prop-types: 15.8.1
react: 19.1.0
react-display-name: 0.2.5
tiny-warning: 1.0.3
tiny-warning@1.0.3: {}
tinyglobby@0.2.13: tinyglobby@0.2.13:
dependencies: dependencies:
@ -2316,6 +2478,8 @@ snapshots:
dependencies: dependencies:
typescript: 5.7.3 typescript: 5.7.3
turbo-stream@2.4.0: {}
type-check@0.4.0: type-check@0.4.0:
dependencies: dependencies:
prelude-ls: 1.2.1 prelude-ls: 1.2.1
@ -2364,3 +2528,8 @@ snapshots:
yallist@3.1.1: {} yallist@3.1.1: {}
yocto-queue@0.1.0: {} yocto-queue@0.1.0: {}
zustand@5.0.3(@types/react@19.1.2)(react@19.1.0):
optionalDependencies:
'@types/react': 19.1.2
react: 19.1.0

View File

View File

@ -1,5 +0,0 @@
import {FileSharing} from "./page/share.tsx";
export function App() {
return <FileSharing />;
}

32
frontend/src/api/auth.ts Normal file
View File

@ -0,0 +1,32 @@
import {useState} from "react";
import {message} from "../component/message/u-message.tsx";
export interface User {
id: number;
username: string;
login_at: number;
token: string;
}
export const useAuth = () => {
const [user, setUser] = useState<User | null>(null)
const login = async (username: string, password: string) => {
let res = await fetch("/api/uauth/login", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({username: username, password: password}),
})
if(res.status !== 200) {
message.error("账号或密码错误")
throw new Error(await res.text())
}
let jes = await res.json() as User;
setUser(jes)
}
return {user, login}
}

View File

@ -0,0 +1,83 @@
import { useState } from 'react';
interface UploadRes {
code: string
}
export const useFileUpload = () => {
const [progress, setProgress] = useState(0);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const uploadFile = async (file: File): Promise<string> => {
setLoading(true);
setError(null);
setProgress(0);
try {
console.log(`[D] api.Upload: upload file = ${file.name}, size = ${file.size}`, file);
const url = `/api/ushare/${file.name}`;
// 1. 初始化上传
const res1 = await fetch(url, {
method: "PUT",
headers: {"X-File-Size": file.size.toString()}
});
if (!res1.ok) {
console.log(`[D] upload: put file not ok, status = ${res1.status}, res = ${await res1.text()}`)
if (res1.status === 401) {
window.location.href = "/login?next=/share"
return ""
}
throw new Error("上传失败<1>");
}
const j1 = await res1.json() as UploadRes;
if (!j1.code) {
throw new Error("上传失败<2>");
}
// 2. 准备分片上传
const CHUNK_SIZE = 1024 * 1024;
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
const code = j1.code;
// 3. 上传分片
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
const start = chunkIndex * CHUNK_SIZE;
const end = Math.min(start + CHUNK_SIZE, file.size);
const chunk = file.slice(start, end);
const res = await fetch(`/api/ushare/${code}`, {
method: "POST",
headers: {
"Range": `bytes=${start}-${end - 1}`,
"Content-Type": "application/octet-stream"
},
body: chunk
});
if (!res.ok) {
const err = await res.text();
throw new Error(`上传失败<3>: ${err}`);
}
// 更新进度
// const currentProgress = Number(((chunkIndex + 1) / totalChunks * 100).toFixed(2)); // 小数
const currentProgress = Math.round(((chunkIndex + 1) / totalChunks) * 100); // 整数 0-100
setProgress(currentProgress);
}
return code;
} catch (err) {
throw err; // 将错误继续抛出以便组件处理
} finally {
setLoading(false);
}
};
return { uploadFile, progress, loading, error };
};

View File

@ -0,0 +1,102 @@
import React, {ReactNode} from 'react';
import {createUseStyles} from 'react-jss';
const useStyle = createUseStyles({
ubutton: {
backgroundColor: "#4CAF50",
color: "white",
padding: "10px 20px",
border: "none",
borderRadius: "5px",
cursor: "pointer",
transition: "background-color 0.3s",
position: 'relative',
overflow: 'hidden',
"&:hover": {
backgroundColor: "#45a049",
},
"&:disabled": {
backgroundColor: "#a5d6a7",
cursor: "not-allowed",
},
},
loadingContent: {
display: 'flex',
alignItems: 'center',
gap: '8px',
},
spinner: {
animation: '$spin 1s linear infinite',
border: '2px solid white',
borderTopColor: 'transparent',
borderRadius: '50%',
width: '16px',
height: '16px',
},
'@keyframes spin': {
'0%': {transform: 'rotate(0deg)'},
'100%': {transform: 'rotate(360deg)'},
},
progressBar: {
position: 'absolute',
bottom: 0,
left: 0,
height: '3px',
backgroundColor: 'rgba(255,255,255,0.5)',
width: '100%',
},
progressFill: {
height: '100%',
backgroundColor: 'white',
transition: 'width 0.3s ease',
},
});
type Props = {
onClick?: () => void;
children: ReactNode;
disabled?: boolean;
style?: React.CSSProperties;
loading?: boolean;
process?: number;
};
export const UButton: React.FC<Props> = ({
onClick,
children,
disabled = false,
style,
loading,
process
}) => {
const classes = useStyle();
return (
<button
style={style}
className={classes.ubutton}
disabled={disabled || loading}
onClick={onClick}
>
{/* 显示加载状态或默认内容 */}
{loading ? (
<div className={classes.loadingContent}>
<span className={classes.spinner}/>
{children}
{process && `(${process}%)`}
</div>
) : children}
{/* 显示进度条 */}
{process !== undefined && (
<div className={classes.progressBar}>
<div
className={classes.progressFill}
style={{width: `${Math.min(Math.max(process, 0), 100)}%`}}
/>
</div>
)}
</button>
);
};

View File

@ -0,0 +1,159 @@
import { useRef, useEffect } from 'react';
export const CloudBackground = () => {
const canvasRef = useRef<HTMLCanvasElement>(null);
// 完整可配置参数
const config = {
cloudNum: 8, // 云朵数量
maxSpeed: 3.0, // 最大水平速度
cloudSize: 100, // 基础云朵尺寸 (新增)
sizeVariation: 0.5, // 尺寸随机变化率 (0-1)
colorVariation: 20, // 色相变化范围
verticalOscillation: 0.5, // 垂直浮动幅度
shapeComplexity: 5, // 形状复杂度(组成圆形数量)
boundaryOffset: 3 // 边界偏移倍数
};
type Cloud = {
x: number;
y: number;
speed: number;
circles: CloudCircle[];
color: string;
maxRadius: number; // 记录云朵最大半径
};
type CloudCircle = {
offsetX: number;
offsetY: number;
radius: number;
};
useEffect(() => {
const canvas = canvasRef.current!;
const ctx = canvas.getContext('2d')!;
const resize = () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
};
resize();
// 生成云朵形状(基于配置参数)
const createCloudShape = () => {
const circles: CloudCircle[] = [];
const circleCount = 4 + Math.floor(Math.random() * config.shapeComplexity);
for(let i = 0; i < circleCount; i++) {
circles.push({
offsetX: (Math.random() - 0.5) * config.cloudSize * 1.5,
offsetY: (Math.random() - 0.5) * config.cloudSize * 0.8,
radius: config.cloudSize * (1 - config.sizeVariation + Math.random() * config.sizeVariation)
});
}
return circles;
};
let clouds: Cloud[] = [];
const createClouds = () => {
clouds = Array.from({ length: config.cloudNum }).map(() => {
const shape = createCloudShape();
return {
x: Math.random() * canvas.width,
y: canvas.height * (0.2 + Math.random() * 0.6),
speed: (Math.random() * 0.5 + 0.5) * config.maxSpeed,
circles: shape,
color: `hsla(210, 30%, 95%, ${0.8 + Math.random() * 0.2})`,
maxRadius: Math.max(...shape.map(c => c.radius)) // 计算最大半径
};
});
};
const drawCloud = (cloud: Cloud) => {
ctx.save();
ctx.beginPath();
cloud.circles.forEach(circle => {
ctx.moveTo(cloud.x + circle.offsetX, cloud.y + circle.offsetY);
ctx.arc(
cloud.x + circle.offsetX,
cloud.y + circle.offsetY,
circle.radius,
0,
Math.PI * 2
);
});
const gradient = ctx.createRadialGradient(
cloud.x, cloud.y, 0,
cloud.x, cloud.y, config.cloudSize * 2
);
gradient.addColorStop(0, cloud.color);
gradient.addColorStop(1, `hsla(210, 50%, 98%, 0.3)`);
ctx.fillStyle = gradient;
ctx.filter = `blur(${config.cloudSize * 0.2}px)`; // 模糊与尺寸关联
ctx.fill();
ctx.restore();
};
let animationFrameId: number;
const animate = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 天空渐变背景
const skyGradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
skyGradient.addColorStop(0, '#e6f3ff');
skyGradient.addColorStop(1, '#d1e8ff');
ctx.fillStyle = skyGradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
clouds.forEach(cloud => {
cloud.x += cloud.speed;
cloud.y += Math.sin(Date.now() / 1000 + cloud.x) * config.verticalOscillation;
// 基于实际最大半径的边界检测
const resetPosition = cloud.x > canvas.width + (cloud.maxRadius * config.boundaryOffset);
if (resetPosition) {
cloud.x = -cloud.maxRadius * config.boundaryOffset;
// 重置时重新生成形状
const newShape = createCloudShape();
cloud.circles = newShape;
cloud.maxRadius = Math.max(...newShape.map(c => c.radius));
}
drawCloud(cloud);
});
animationFrameId = requestAnimationFrame(animate);
};
createClouds();
animate();
window.addEventListener('resize', () => {
resize();
createClouds();
});
return () => {
window.removeEventListener('resize', resize);
cancelAnimationFrame(animationFrameId);
};
}, []);
return (
<canvas
ref={canvasRef}
style={{
position: 'fixed',
top: 0,
left: 0,
zIndex: -1,
width: '100%',
height: '100%'
}}
/>
);
};

View File

@ -0,0 +1,124 @@
import { useRef, useEffect } from 'react';
export const AnimatedBackground = () => {
const canvasRef = useRef<HTMLCanvasElement>(null);
// 粒子配置
const config = {
particleNum: 100,
maxSpeed: 1.5,
particleRadius: 2,
lineWidth: 1.5,
lineDistance: 100
};
type Particle = {
x: number;
y: number;
speedX: number;
speedY: number;
color: string;
radius: number;
};
useEffect(() => {
const canvas = canvasRef.current!;
const ctx = canvas.getContext('2d')!;
// 设置canvas尺寸
const resize = () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
};
resize();
// 创建粒子数组
let particles: Particle[] = [];
const createParticles = () => {
particles = Array.from({ length: config.particleNum }).map(() => ({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
speedX: (Math.random() - 0.5) * config.maxSpeed,
speedY: (Math.random() - 0.5) * config.maxSpeed,
color: `hsl(${Math.random() * 360}, 70%, 60%)`,
radius: Math.random() * config.particleRadius + 1
}));
};
// 绘制连线
const drawLine = (p1: Particle, p2: Particle) => {
const dx = p1.x - p2.x;
const dy = p1.y - p2.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < config.lineDistance) {
ctx.beginPath();
ctx.strokeStyle = p1.color;
ctx.lineWidth = config.lineWidth * (1 - dist / config.lineDistance);
ctx.moveTo(p1.x, p1.y);
ctx.lineTo(p2.x, p2.y);
ctx.stroke();
}
};
// 动画循环
let animationFrameId: number;
const animate = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 更新粒子位置
particles.forEach(particle => {
particle.x += particle.speedX;
particle.y += particle.speedY;
// 边界反弹
if (particle.x < 0 || particle.x > canvas.width) particle.speedX *= -1;
if (particle.y < 0 || particle.y > canvas.height) particle.speedY *= -1;
// 绘制粒子
ctx.beginPath();
ctx.fillStyle = particle.color;
ctx.arc(particle.x, particle.y, particle.radius, 0, Math.PI * 2);
ctx.fill();
});
// 绘制粒子间的连线
for (let i = 0; i < particles.length; i++) {
for (let j = i + 1; j < particles.length; j++) {
drawLine(particles[i], particles[j]);
}
}
animationFrameId = requestAnimationFrame(animate);
};
// 初始化
createParticles();
animate();
// 窗口resize处理
window.addEventListener('resize', () => {
resize();
createParticles();
});
// 清理
return () => {
window.removeEventListener('resize', resize);
cancelAnimationFrame(animationFrameId);
};
}, []);
return (
<canvas
ref={canvasRef}
style={{
position: 'fixed',
top: 0,
left: 0,
zIndex: -1,
backgroundColor: '#1a1a1a'
}}
/>
);
};

View File

@ -0,0 +1,126 @@
import {createRoot} from "react-dom/client";
import {useState} from "react";
import {createUseStyles} from "react-jss";
const useStyle = createUseStyles({
container: {
position: 'fixed',
width: '100%',
display: 'flex',
zIndex: 10000,
top: '20px',
justifyContent: "center",
alignItems: "center",
flexDirection: "column",
},
message: {
width: '100%',
maxWidth: '300px',
background: '#fafafa', // 浅灰背景与其他状态统一
borderRadius: '8px',
padding: '10px 10px 10px 20px', // 统一左侧20px留白
borderLeft: '4px solid #e0e0e0', // 加粗为4px与其他状态一致
color: '#757575', // 中性灰文字
boxShadow: '0 2px 6px rgba(224, 224, 224, 0.15)', // 灰色投影
transition: 'all 0.3s ease-in-out', // 补全时间单位
marginBottom: '20px',
'&.success': {
color: '#2e7d32',
backgroundColor: '#f0f9eb',
borderLeft: '4px solid #4CAF50',
paddingLeft: '20px',
boxShadow: '0 2px 6px rgba(76, 175, 80, 0.15)'
},
'&.warning': {
color: '#faad14', // 警告文字色
backgroundColor: '#fffbe6', // 浅黄色背景
borderLeft: '4px solid #ffc53d', // 琥珀色左侧标识
paddingLeft: '20px',
boxShadow: '0 2px 6px rgba(255, 197, 61, 0.15)' // 金色投影
},
'&.error': {
color: '#f5222d', // 错误文字色
backgroundColor: '#fff1f0', // 浅红色背景
borderLeft: '4px solid #ff4d4f', // 品红色左侧标识
paddingLeft: '20px',
boxShadow: '0 2px 6px rgba(255, 77, 79, 0.15)' // 红色投影
}
}
})
let el = document.querySelector("#u-message")
if (!el) {
el = document.createElement('div')
el.className = 'u-message'
el.id = 'u-message'
document.body.append(el)
}
export interface Message {
id: number
content: string
duration: number
type: 'info' | 'success' | 'warning' | 'error'
}
export interface MessageApi {
info: (content: string, duration?: number) => void;
warning: (content: string, duration?: number) => void;
success: (content: string, duration?: number) => void;
error: (content: string, duration?: number) => void;
}
const default_duration = 3000
let add: (msg: Message) => void;
const MessageContainer: React.FC = () => {
const classes = useStyle()
const [msgs, setMsgs] = useState<Message[]>([]);
const remove = (id: number) => {
setMsgs(prevMsgs => prevMsgs.filter(v => v.id !== id));
}
add = (msg: Message) => {
const id = Date.now();
setMsgs(prevMsgs => {
const newMsgs = [{...msg, id}, ...prevMsgs];
// 直接限制数组长度为5移除最旧的消息最后一项
if (newMsgs.length > 5) {
newMsgs.pop();
}
return newMsgs;
});
setTimeout(() => {
remove(id);
}, msg.duration ?? default_duration);
}
return <div className={classes.container}>
{msgs.map(m => <div key={m.id} className={`${classes.message} ${m.type}`}>{m.content}</div>)}
</div>
}
createRoot(el).render(<MessageContainer/>)
export const message: MessageApi = {
info: function (content: string, duration?: number): void {
add({content: content, duration: duration, type: "info"} as Message)
},
warning: function (content: string, duration?: number): void {
add({content: content, duration: duration, type: "warning"} as Message)
},
success: function (content: string, duration?: number): void {
add({content: content, duration: duration, type: "success"} as Message)
},
error: function (content: string, duration?: number): void {
add({content: content, duration: duration, type: "error"} as Message)
}
}

View File

@ -1,10 +1,19 @@
import { StrictMode } from 'react' import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import './index.css' import './index.css'
import {App} from './App.tsx' import {createBrowserRouter, RouterProvider} from "react-router-dom";
import {Login} from "./page/login.tsx";
import {FileSharing} from "./page/share.tsx";
createRoot(document.getElementById('root')!).render( const container = document.getElementById('root')
<StrictMode> const root = createRoot(container!)
<App /> const router = createBrowserRouter([
</StrictMode>, {path: "/login", element: <Login />},
{path: "*", element: <FileSharing />},
])
root.render(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>,
) )

View File

@ -0,0 +1,221 @@
import {createUseStyles} from "react-jss";
import {UButton} from "../../component/button/u-button.tsx";
import React, {useState} from "react";
import {useStore} from "../../store/share.ts";
import {message} from "../../component/message/u-message.tsx";
import {useFileUpload} from "../../api/upload.ts";
const useUploadStyle = createUseStyles({
container: {
backgroundColor: "#e3f2fd",
display: "flex",
justifyContent: "center",
alignItems: "center",
},
form: {
backgroundColor: "#C8E6C9",
boxShadow: "inset 0 0 15px rgba(56, 142, 60, 0.15)",
padding: "30px",
borderRadius: "15px",
width: "70%",
margin: "20px 60px 20px 0",
/*todo margin 不用 px*/
},
title: {
color: "#2c9678"
},
file: {
display: 'none',
},
preview: {
marginTop: '10px',
display: 'flex',
},
name: {
color: "#2c9678",
marginLeft: '10px'
},
clean: {
borderRadius: '50%',
cursor: 'pointer',
'&:hover': {}
}
})
const useShowStyle = createUseStyles({
container: {
backgroundColor: "#e3f2fd",
display: "flex",
justifyContent: "center",
alignItems: "center",
position: "relative", // 为关闭按钮提供定位基准
},
title: {
color: "#2c9678",
marginTop: 0,
display: "flex",
justifyContent: "space-between",
alignItems: "center",
},
form: {
backgroundColor: "#C8E6C9",
boxShadow: "inset 0 0 15px rgba(56, 142, 60, 0.15)",
padding: "30px",
borderRadius: "15px",
width: "70%",
margin: "20px 60px 20px 0",
position: "relative",
},
closeButton: {
position: "absolute",
top: "10px",
right: "10px",
background: "transparent",
color: "white",
border: "none",
borderRadius: "50%",
width: "24px",
height: "24px",
cursor: "pointer",
"&:hover": {
// background: "#cc0000",
boxShadow: "20px 20px 60px #fff, -20px -20px 60px #fff",
// boxShadow: "20px 20px 60px #eee",
},
},
codeWrapper: {
backgroundColor: "rgba(255,255,255,0.8)",
padding: "0 15px",
borderRadius: "8px",
margin: "15px 0",
overflowX: "auto",
},
pre: {
display: 'flex',
flexDirection: 'row',
color: 'black',
alignItems: 'center',
height: '24px',
"& > code": {
marginLeft: "0",
}
},
copyButton: {
marginLeft: 'auto',
background: "#2c9678",
color: "white",
border: "none",
padding: "8px 16px",
borderRadius: "4px",
cursor: "pointer",
transition: "background 0.3s",
"&:hover": {
background: "#1f6d5a",
},
},
});
export const PanelLeft = () => {
const [code, set_code] = useState("")
if (code) {
return <PanelLeftShow code={code} set_code={set_code} />
}
return <PanelLeftUpload set_code={set_code}/>
};
const PanelLeftUpload: React.FC<{ set_code: (code:string) => void }> = ({set_code}) => {
const style = useUploadStyle()
const {file, setFile} = useStore()
const {uploadFile, progress, loading} = useFileUpload();
function onFileSelect() {
// @ts-ignore
document.querySelector('#real-file-input').click();
}
function onFileChange(e: React.ChangeEvent<HTMLInputElement>) {
console.log('[D] onFileChange: e =', e)
setFile(e.currentTarget.files ? e.currentTarget.files[0] : null)
}
async function onFileUpload() {
if (!file) {
return
}
const code = await uploadFile(file)
set_code(code)
}
function onFileClean() {
setFile(null)
}
return <div className={style.container}>
<div className={style.form}>
<h2 className={style.title}></h2>
{
!file && !loading &&
<UButton onClick={onFileSelect}></UButton>
}
{
file && !loading &&
<UButton onClick={onFileUpload}></UButton>
}
{
loading &&
<UButton process={progress} loading={loading}></UButton>
}
<input type="file" className={style.file} id="real-file-input" onChange={onFileChange}/>
{
file &&
<div className={style.preview}>
<div className={style.clean} onClick={onFileClean}>×</div>
<div className={style.name}>{file.name}</div>
</div>
}
</div>
</div>
}
const PanelLeftShow: React.FC<{ code: string; set_code: (code: string) => void }> = ({ code, set_code }) => {
const classes = useShowStyle();
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(code);
message.success("复制成功!");
} catch (err) {
message.warning("复制失败,请手动选择文本复制");
}
};
return (
<div className={classes.container}>
<div className={classes.form}>
<button
className={classes.closeButton}
onClick={() => set_code('')}
aria-label="关闭"
>
×
</button>
<h2 className={classes.title}>
!
</h2>
<div className={classes.codeWrapper}>
<pre className={classes.pre}>
<code>{code}</code>
<button className={classes.copyButton} onClick={handleCopy}>
</button>
</pre>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,30 @@
import {createUseStyles} from "react-jss";
const useStyle = createUseStyles({
container: {
backgroundColor: 'lightgray',
position: "relative",
overflow: "hidden",
},
left: {
backgroundColor: "#e3f2fd",
position: "absolute",
width: "100%",
height: "100%",
clipPath: 'polygon(0 0, 100% 0, 0 100%)'
},
right: {
backgroundColor: "#e8f5e9",
position: "absolute",
width: "100%",
height: "100%",
clipPath: 'polygon(100% 0, 100% 100%, 0 100%)'
},
})
export const PanelMid = () => {
const style = useStyle()
return <div className={style.container}>
<div className={style.left}></div>
<div className={style.right}></div>
</div>
};

View File

@ -0,0 +1,68 @@
import {createUseStyles} from "react-jss";
import {UButton} from "../../component/button/u-button.tsx";
import {useStore} from "../../store/share.ts";
const useStyle = createUseStyles({
container: {
backgroundColor: "#e8f5e9",
display: "flex",
justifyContent: "center",
alignItems: "center",
},
form: {
backgroundColor: "#BBDEFB",
boxShadow: "inset 0 0 15px rgba(33, 150, 243, 0.15)",
padding: "30px",
borderRadius: "15px",
width: "70%",
margin: "20px 0 20px 60px",
/*todo margin 不用 px*/
},
title: {
color: '#1661ab', // 靛青
},
code: {
padding: '11px',
margin: '20px 0',
width: '200px',
border: '2px solid #ddd',
borderRadius: '5px',
'&:active': {
border: '2px solid #1661ab',
}
}
})
export const PanelRight = () => {
const style = useStyle()
const {code, setCode} = useStore()
function onCodeChange(e: React.ChangeEvent<HTMLInputElement>) {
setCode(e.currentTarget.value)
}
async function onFetchFile() {
const url = `/ushare/${code}`
console.log('[D] onFetchFile: url =', url)
const link = document.createElement('a');
link.href = url;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
return <div className={style.container}>
<div className={style.form}>
<h2 className={style.title}></h2>
<div>
<input
type="text"
className={style.code}
placeholder="输入下载码"
value={code}
onChange={onCodeChange}
/>
<UButton style={{marginLeft: '10px'}} onClick={onFetchFile}></UButton>
</div>
</div>
</div>
};

183
frontend/src/page/login.tsx Normal file
View File

@ -0,0 +1,183 @@
import React, { useState } from "react";
import { createUseStyles } from "react-jss";
import { CloudBackground } from "../component/fluid/cloud.tsx";
import {useAuth} from "../api/auth.ts";
const useClass = createUseStyles({
container: {
overflow: 'hidden',
height: '100vh',
margin: 0,
padding: 0,
boxSizing: "border-box",
fontFamily: "'Segoe UI', Arial, sans-serif",
display: "flex",
justifyContent: "center",
alignItems: "center",
position: 'relative',
},
login_container: {
background: "rgba(255,255,255,.5)",
boxShadow: "0 2px 10px rgba(0, 0, 0, 0.1)",
width: "350px",
height: '100%',
position: 'absolute',
left: '70%',
display: "flex",
alignItems: "center",
justifyContent: "center",
},
form: {
height: '100%',
width: '100%',
display: "flex",
justifyContent: "center",
alignItems: "center",
flexDirection: 'column',
color: "#1a73e8",
padding: '40px',
},
input: {
width: '100%',
marginTop: '20px',
"& > input": {
width: "calc(100% - 30px)",
padding: "12px 15px",
border: "1px solid #ddd",
borderRadius: "6px",
fontSize: "16px",
transition: "border-color 0.3s",
"&:focus": {
outline: "none",
borderColor: "#1a73e8",
boxShadow: "0 0 0 2px rgba(26, 115, 232, 0.2)",
},
"&:hover": {
borderColor: "#1a73e8",
}
},
},
button: {
marginTop: '20px',
width: '100%',
"& > button": {
width: "100%",
padding: "12px",
background: "#1a73e8",
color: "white",
border: "none",
borderRadius: "6px",
fontSize: "16px",
cursor: "pointer",
transition: "background 0.3s",
"&:hover": {
background: "#1557b0",
},
},
},
inputContainer: {
position: 'relative',
width: '100%',
marginTop: '20px',
},
inputField: {
width: "calc(100% - 52px)",
padding: "12px 35px 12px 15px",
border: "1px solid #ddd",
borderRadius: "6px",
fontSize: "16px",
transition: "border-color 0.3s",
"&:focus": {
outline: "none",
borderColor: "#1a73e8",
boxShadow: "0 0 0 2px rgba(26, 115, 232, 0.2)",
},
"&:hover": {
borderColor: "#1a73e8",
}
},
iconButton: {
position: 'absolute',
right: '10px',
top: '50%',
transform: 'translateY(-50%)',
background: 'transparent',
border: 'none',
cursor: 'pointer',
color: '#666',
"&:hover": {
color: '#333',
}
}
})
export const Login: React.FC = () => {
const classes = useClass()
const {login} = useAuth()
const [username, setUsername] = useState("")
const [password, setPassword] = useState("")
const [showPassword, setShowPassword] = useState(false)
const onLogin = async () => {
try {
await login(username, password)
window.location.href = "/"
} catch (_e) {
}
}
return <div className={classes.container}>
<CloudBackground/>
<div className={classes.login_container}>
<div className={classes.form}>
<h2>UShare</h2>
{/* 用户名输入框 */}
<div className={classes.inputContainer}>
<input
className={classes.inputField}
placeholder="请输入账号"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
{username && (
<button
className={classes.iconButton}
onClick={() => setUsername("")}
style={{ right: '10px', fontSize: '16px' }}
>
×
</button>
)}
</div>
{/* 密码输入框 */}
<div className={classes.inputContainer}>
<input
className={classes.inputField}
placeholder="请输入密码"
type={showPassword ? "text" : "password"}
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button
className={classes.iconButton}
onMouseDown={() => setShowPassword(true)}
onMouseUp={() => setShowPassword(false)}
onMouseLeave={() => setShowPassword(false)}
style={{ right: '10px', fontSize: '12px' }}
>
{showPassword ? "👁" : "👁"}
</button>
</div>
<div className={classes.button}>
<button onClick={onLogin}></button>
</div>
</div>
</div>
</div>
}

View File

@ -1,15 +0,0 @@
@import "tailwindcss";
.mid-left {
background-color: #e3f2fd; /* 柔和的浅蓝色 */
}
.mid-right {
background-color: #e8f5e9; /* 柔和的浅绿色 */
}
.share-left {
background-color: #e3f2fd; /* 柔和的浅蓝色 */
}
.share-right {
background-color: #e8f5e9;
}

View File

@ -1,130 +1,26 @@
import "./share.css"; import {createUseStyles} from 'react-jss'
// FileSharing.tsx import {PanelLeft} from "./component/panel-left.tsx";
import { useRef, useState } from 'react'; import {PanelRight} from "./component/panel-right.tsx";
import {PanelMid} from "./component/panel-mid.tsx";
type FileItemProps = { const useStyle = createUseStyles({
fileName: string; "@global": {
onClear: () => void; margin: 0,
}; padding: 0,
},
container: {
margin: 0,
height: "100vh",
display: "grid",
gridTemplateColumns: "40% 20% 40%",
},
})
const FileItem = ({ fileName, onClear }: FileItemProps) => ( export const FileSharing = () => {
<div className="flex items-center gap-2 my-2"> const style = useStyle()
<button return <div className={style.container}>
className="w-5 h-5 rounded-full flex items-center justify-center text-white hover:bg-red-400 transition-colors" <PanelLeft />
onClick={onClear} <PanelMid/>
> <PanelRight/>
×
</button>
<span className="text-gray-700">{fileName}</span>
</div> </div>
); };
const MiddlePanel = () => (
<div className="relative bg-gray-200 overflow-hidden">
<div
className="mid-left absolute w-full h-full bg-blue-100"
style={{ clipPath: 'polygon(0 0, 100% 0, 0 100%)' }}
/>
<div
className="mid-right absolute w-full h-full bg-green-100"
style={{ clipPath: 'polygon(100% 0, 100% 100%, 0 100%)' }}
/>
</div>
);
const UploadPanel = () => {
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleFileSelect = () => {
if (!selectedFile && fileInputRef.current) {
fileInputRef.current.click();
} else {
// Handle upload logic
if (selectedFile) {
const formData = new FormData();
formData.append('file', selectedFile);
// Replace with actual API call
fetch('https://your-upload-api.com/upload', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
alert(`Upload success! Code: ${data.code}`);
setSelectedFile(null);
})
.catch(() => alert('Upload failed'));
}
}
};
return (
<div className="share-left flex flex-col items-center justify-center p-5 bg-blue-50 h-full">
<div className="bg-blue-100 rounded-xl p-6 w-4/5 shadow-inner">
<h2 className="text-xl font-semibold mb-4 text-cyan-900"></h2>
<input
type="file"
ref={fileInputRef}
className="hidden"
onChange={(e) => e.target.files?.[0] && setSelectedFile(e.target.files[0])}
/>
<button
onClick={handleFileSelect}
className="bg-green-500 text-white px-4 py-2 rounded-lg hover:bg-green-600 transition-colors"
>
{selectedFile ? '确认上传' : '选择文件'}
</button>
{selectedFile && (
<FileItem
fileName={selectedFile.name}
onClear={() => setSelectedFile(null)}
/>
)}
</div>
</div>
);
};
const DownloadPanel = () => {
const [downloadCode, setDownloadCode] = useState('');
const handleDownload = () => {
if (!downloadCode) {
alert('请输入下载码');
return;
}
// Replace with actual download logic
window.location.href = `https://your-download-api.com/download?code=${downloadCode}`;
};
return (
<div className="share-right flex flex-col items-center justify-center p-5 bg-green-50 h-full">
<div className="bg-green-100 rounded-xl p-6 w-4/5 shadow-inner">
<h2 className="text-xl font-semibold mb-4 text-teal-900"></h2>
<input
type="text"
placeholder="输入下载码"
className="w-full px-3 py-2 border-2 border-gray-200 rounded-lg mb-4"
value={downloadCode}
onChange={(e) => setDownloadCode(e.target.value)}
/>
<button
onClick={handleDownload}
className="bg-green-500 text-white px-4 py-2 rounded-lg hover:bg-green-600 transition-colors w-full"
>
</button>
</div>
</div>
);
};
export const FileSharing = () => (
<div className="h-screen grid grid-cols-[40%_20%_40%]">
<UploadPanel />
<MiddlePanel />
<DownloadPanel />
</div>
);

View File

@ -0,0 +1,23 @@
import {create} from 'zustand'
type Store = {
file: File | null,
setFile: (f: File | null) => void
code: string,
setCode: (value:string) => void
}
export const useStore = create<Store>()((set) => ({
file: null,
setFile: (f: File | null = null) => {
set(state => {
return {...state, file: f}
})
},
code: '',
setCode: (value:string= '') => {
set(state => {
return {...state, code: value}
})
}
}))

View File

@ -3,5 +3,8 @@
"references": [ "references": [
{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" } { "path": "./tsconfig.node.json" }
] ],
"compilerOptions": {
"allowJs": true
}
} }

View File

@ -1,8 +1,19 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react(), tailwindcss()], plugins: [react()],
server: {
proxy: {
'/api': {
target: 'http://127.0.0.1:9119',
changeOrigin: true
},
'/ushare': {
target: 'http://127.0.0.1:9119',
changeOrigin: true
},
}
}
}) })

31
go.mod
View File

@ -2,7 +2,11 @@ module github.com/loveuer/ushare
go 1.24.2 go 1.24.2
require github.com/loveuer/nf v0.3.5 require (
github.com/loveuer/nf v0.3.5
github.com/pkg/errors v0.9.1
github.com/spf13/viper v1.20.1
)
require ( require (
dario.cat/mergo v1.0.0 // indirect dario.cat/mergo v1.0.0 // indirect
@ -12,24 +16,41 @@ require (
github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // indirect
github.com/emirpasic/gods v1.18.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect
github.com/fatih/color v1.17.0 // indirect github.com/fatih/color v1.17.0 // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/go-git/go-billy/v5 v5.5.0 // indirect
github.com/go-git/go-git/v5 v5.12.0 // indirect github.com/go-git/go-git/v5 v5.12.0 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jedib0t/go-pretty/v6 v6.6.7 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/matoous/go-nanoid/v2 v2.1.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sagikazarmark/locafero v0.7.0 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/skeema/knownhosts v1.2.2 // indirect github.com/skeema/knownhosts v1.2.2 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.12.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.25.0 // indirect go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/crypto v0.32.0 // indirect
golang.org/x/mod v0.17.0 // indirect golang.org/x/mod v0.17.0 // indirect
golang.org/x/net v0.27.0 // indirect golang.org/x/net v0.33.0 // indirect
golang.org/x/sync v0.7.0 // indirect golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.22.0 // indirect golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )

69
go.sum
View File

@ -24,6 +24,10 @@ github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
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.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE=
github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
@ -34,6 +38,8 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMj
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys=
github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
@ -42,6 +48,8 @@ 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/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jedib0t/go-pretty/v6 v6.6.7 h1:m+LbHpm0aIAPLzLbMfn8dc3Ht8MW7lsSO4MPItz/Uuo=
github.com/jedib0t/go-pretty/v6 v6.6.7/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@ -53,41 +61,69 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/loveuer/nf v0.3.5 h1:DlgTa6Rx8D3/VtH9e0fLGAxmPwSYd7b3nfBltSMuypE= github.com/loveuer/nf v0.3.5 h1:DlgTa6Rx8D3/VtH9e0fLGAxmPwSYd7b3nfBltSMuypE=
github.com/loveuer/nf v0.3.5/go.mod h1:IAq0K1c/mlNQzLBvUzAD1LCWiVlt2GqTMPdDjej3Ryo= github.com/loveuer/nf v0.3.5/go.mod h1:IAq0K1c/mlNQzLBvUzAD1LCWiVlt2GqTMPdDjej3Ryo=
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.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 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/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A=
github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
@ -99,13 +135,14 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -120,15 +157,17 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@ -136,8 +175,10 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=

View File

@ -5,6 +5,7 @@ import (
"github.com/loveuer/nf" "github.com/loveuer/nf"
"github.com/loveuer/nf/nft/log" "github.com/loveuer/nf/nft/log"
"github.com/loveuer/nf/nft/tool" "github.com/loveuer/nf/nft/tool"
"github.com/loveuer/ushare/internal/handler"
"github.com/loveuer/ushare/internal/opt" "github.com/loveuer/ushare/internal/opt"
"net" "net"
"net/http" "net/http"
@ -17,6 +18,11 @@ func Start(ctx context.Context) <-chan struct{} {
return c.SendStatus(http.StatusOK) return c.SendStatus(http.StatusOK)
}) })
app.Get("/ushare/:code", handler.Fetch())
app.Put("/api/ushare/:filename", handler.AuthVerify(), handler.ShareNew()) // 获取上传 code, 分片大小
app.Post("/api/ushare/:code", handler.ShareUpload()) // 分片上传接口
app.Post("/api/uauth/login", handler.AuthLogin())
ready := make(chan struct{}) ready := make(chan struct{})
ln, err := net.Listen("tcp", opt.Cfg.Address) ln, err := net.Listen("tcp", opt.Cfg.Address)
if err != nil { if err != nil {

184
internal/controller/meta.go Normal file
View File

@ -0,0 +1,184 @@
package controller
import (
"context"
"fmt"
"github.com/loveuer/nf/nft/log"
"github.com/loveuer/ushare/internal/model"
"github.com/loveuer/ushare/internal/opt"
gonanoid "github.com/matoous/go-nanoid/v2"
"github.com/spf13/viper"
"io"
"os"
"path/filepath"
"strings"
"sync"
"time"
)
type metaInfo struct {
f *os.File
name string
create time.Time
last time.Time
size int64
cursor int64
user string
}
func (m *metaInfo) generateMeta(code string) error {
content := fmt.Sprintf("filename=%s\ncreated_at=%d\nsize=%d\nuploader=%s",
m.name, m.create.UnixMilli(), m.size, m.user,
)
return os.WriteFile(opt.MetaPath(code), []byte(content), 0644)
}
type meta struct {
sync.Mutex
ctx context.Context
m map[string]*metaInfo
}
var (
MetaManager = &meta{m: make(map[string]*metaInfo)}
)
const letters = "1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ"
func (m *meta) New(size int64, filename, ip string) (string, error) {
now := time.Now()
code, err := gonanoid.Generate(letters, opt.CodeLength)
if err != nil {
return "", err
}
f, err := os.Create(opt.FilePath(code))
if err != nil {
return "", err
}
if err = f.Truncate(size); err != nil {
f.Close()
return "", err
}
m.Lock()
defer m.Unlock()
m.m[code] = &metaInfo{f: f, name: filename, last: now, size: size, cursor: 0, create: now, user: ip}
return code, nil
}
func (m *meta) Write(code string, start, end int64, reader io.Reader) (total, cursor int64, err error) {
m.Lock()
defer m.Unlock()
if _, ok := m.m[code]; !ok {
return 0, 0, fmt.Errorf("code not exist")
}
w, err := io.CopyN(m.m[code].f, reader, end-start+1)
if err != nil {
return 0, 0, err
}
m.m[code].cursor += w
m.m[code].last = time.Now()
total = m.m[code].size
cursor = m.m[code].cursor
if m.m[code].cursor == m.m[code].size {
defer delete(m.m, code)
if err = m.m[code].generateMeta(code); err != nil {
return 0, 0, err
}
}
return total, cursor, nil
}
func (m *meta) Start(ctx context.Context) {
ticker := time.NewTicker(time.Minute)
m.ctx = ctx
// 清理 2 分钟内没有继续上传的 part
go func() {
for {
select {
case <-ctx.Done():
return
case now := <-ticker.C:
for code, info := range m.m {
if now.Sub(info.last) > 2*time.Minute {
m.Lock()
if err := info.f.Close(); err != nil {
log.Warn("handler.Meta: [timer] close file failed, file = %s, err = %s", opt.FilePath(code), err.Error())
}
if err := os.RemoveAll(opt.FilePath(code)); err != nil {
log.Warn("handler.Meta: [timer] remove file failed, file = %s, err = %s", opt.FilePath(code), err.Error())
}
delete(m.m, code)
m.Unlock()
log.Warn("MetaController: code timeout removed, code = %s", code)
}
}
}
}
}()
// 清理一天前的文件
go func() {
ticker := time.NewTicker(5 * time.Minute)
for {
select {
case <-ctx.Done():
return
case now := <-ticker.C:
_ = filepath.Walk(opt.Cfg.DataPath, func(path string, info os.FileInfo, err error) error {
if info.IsDir() {
return nil
}
name := filepath.Base(info.Name())
if !strings.HasPrefix(name, ".meta.") {
return nil
}
viper.SetConfigFile(path)
viper.SetConfigType("env")
if err = viper.ReadInConfig(); err != nil {
// todo log
return nil
}
mi := new(model.Meta)
if err = viper.Unmarshal(mi); err != nil {
// todo log
return nil
}
code := strings.TrimPrefix(name, ".meta.")
if now.Sub(time.UnixMilli(mi.CreatedAt)) > 24*time.Hour {
log.Debug("controller.meta: file out of date, code = %s, user_key = %s", code, mi.Uploader)
os.RemoveAll(opt.FilePath(code))
os.RemoveAll(path)
m.Lock()
delete(m.m, code)
m.Unlock()
}
return nil
})
}
}
}()
}

View File

@ -0,0 +1,86 @@
package controller
import (
"context"
"github.com/loveuer/ushare/internal/model"
"github.com/loveuer/ushare/internal/opt"
"github.com/loveuer/ushare/internal/pkg/tool"
"github.com/pkg/errors"
"sync"
"time"
)
type userManager struct {
sync.Mutex
ctx context.Context
um map[string]*model.User
}
func (um *userManager) Login(username string, password string) (*model.User, error) {
var (
now = time.Now()
)
if username != "admin" {
return nil, errors.New("账号或密码错误")
}
if !tool.ComparePassword(password, opt.Cfg.Auth) {
return nil, errors.New("账号或密码错误")
}
op := &model.User{
Id: 1,
Username: username,
LoginAt: now.Unix(),
Token: tool.RandomString(32),
}
um.Lock()
defer um.Unlock()
um.um[op.Token] = op
return op, nil
}
func (um *userManager) Verify(token string) (*model.User, error) {
um.Lock()
defer um.Unlock()
op, ok := um.um[token]
if !ok {
return nil, errors.New("未登录或凭证已失效, 请重新登录")
}
return op, nil
}
func (um *userManager) Start(ctx context.Context) {
um.ctx = ctx
go func() {
ticker := time.NewTicker(time.Minute)
for {
select {
case <-um.ctx.Done():
return
case now := <-ticker.C:
um.Lock()
for _, op := range um.um {
if now.Sub(time.UnixMilli(op.LoginAt)) > 8*time.Hour {
delete(um.um, op.Token)
}
}
um.Unlock()
}
}
}()
}
var (
UserManager = &userManager{
um: make(map[string]*model.User),
}
)

70
internal/handler/auth.go Normal file
View File

@ -0,0 +1,70 @@
package handler
import (
"fmt"
"github.com/loveuer/nf"
"github.com/loveuer/ushare/internal/controller"
"github.com/loveuer/ushare/internal/model"
"github.com/loveuer/ushare/internal/opt"
"net/http"
)
func AuthVerify() nf.HandlerFunc {
tokenFn := func(c *nf.Ctx) (token string) {
if token = c.Get("Authorization"); token != "" {
return
}
token = c.Cookies("ushare")
return
}
return func(c *nf.Ctx) error {
if opt.Cfg.Auth == "" {
return c.Next()
}
token := tokenFn(c)
if token == "" {
return c.Status(http.StatusUnauthorized).JSON(map[string]string{"error": "unauthorized"})
}
op, err := controller.UserManager.Verify(token)
if err != nil {
return c.Status(http.StatusUnauthorized).JSON(map[string]string{"error": "unauthorized", "msg": err.Error()})
}
c.Locals("user", op)
return c.Next()
}
}
func AuthLogin() nf.HandlerFunc {
return func(c *nf.Ctx) error {
type Req struct {
Username string `json:"username"`
Password string `json:"password"`
}
var (
err error
req Req
op *model.User
)
if err = c.BodyParser(&req); err != nil {
return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": "错误的用户名或密码<1>"})
}
if op, err = controller.UserManager.Login(req.Username, req.Password); err != nil {
return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": err.Error()})
}
header := fmt.Sprintf("ushare=%s; Path=/; Max-Age=%d", op.Token, 8*3600)
c.SetHeader("Set-Cookie", header)
return c.Status(http.StatusOK).JSON(map[string]any{"data": op})
}
}

118
internal/handler/share.go Normal file
View File

@ -0,0 +1,118 @@
package handler
import (
"fmt"
"github.com/loveuer/nf"
"github.com/loveuer/nf/nft/log"
"github.com/loveuer/ushare/internal/controller"
"github.com/loveuer/ushare/internal/model"
"github.com/loveuer/ushare/internal/opt"
"github.com/pkg/errors"
"github.com/spf13/cast"
"github.com/spf13/viper"
"net/http"
"os"
"regexp"
"strings"
)
func Fetch() nf.HandlerFunc {
return func(c *nf.Ctx) error {
code := c.Param("code")
log.Debug("handler.Fetch: code = %s", code)
info := new(model.Meta)
_, err := os.Stat(opt.MetaPath(code))
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return c.Status(http.StatusNotFound).JSON(map[string]string{"msg": "文件不存在"})
}
return c.SendStatus(http.StatusInternalServerError)
}
viper.SetConfigFile(opt.MetaPath(code))
viper.SetConfigType("env")
if err = viper.ReadInConfig(); err != nil {
return c.SendStatus(http.StatusInternalServerError)
}
if err = viper.Unmarshal(info); err != nil {
return c.SendStatus(http.StatusInternalServerError)
}
c.SetHeader("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, info.Filename))
http.ServeFile(c.Writer, c.Request, opt.FilePath(code))
return nil
}
}
func ShareNew() nf.HandlerFunc {
return func(c *nf.Ctx) error {
filename := strings.TrimSpace(c.Param("filename"))
if filename == "" {
return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": "filename required"})
}
size, err := cast.ToInt64E(c.Get(opt.HeaderSize))
if err != nil {
return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": "miss header: " + opt.HeaderSize})
}
code, err := controller.MetaManager.New(size, filename, c.IP())
if err != nil {
return c.Status(http.StatusInternalServerError).JSON(map[string]string{"msg": ""})
}
return c.Status(http.StatusOK).JSON(map[string]string{"code": code})
}
}
func ShareUpload() nf.HandlerFunc {
rangeValidator := regexp.MustCompile(`^bytes=\d+-\d+$`)
return func(c *nf.Ctx) error {
code := strings.TrimSpace(c.Param("code"))
if len(code) != opt.CodeLength {
return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": "invalid file code"})
}
log.Debug("handler.ShareUpload: code = %s", code)
ranger := strings.TrimSpace(c.Get("Range"))
if ranger == "" {
return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": "miss header: Range"})
}
log.Debug("handler.ShareUpload: code = %s, ranger = %s", code, ranger)
if !rangeValidator.MatchString(ranger) {
log.Warn("handler.ShareUpload: invalid range, ranger = %s", ranger)
return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": "Range invalid(1)"})
}
strs := strings.Split(strings.TrimPrefix(ranger, "bytes="), "-")
if len(strs) != 2 {
return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": "Range invalid(2)"})
}
start, err := cast.ToInt64E(strs[0])
if err != nil {
return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": "Range invalid(3)"})
}
end, err := cast.ToInt64E(strs[1])
if err != nil {
return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": "Range invalid(4)"})
}
log.Debug("handler.ShareUpload: code = %s, start = %d, end = %d", code, start, end)
total, cursor, err := controller.MetaManager.Write(code, start, end, c.Request.Body)
if err != nil {
log.Error("handler.ShareUpload: write error: %s", err)
return c.Status(http.StatusInternalServerError).JSON(map[string]string{"msg": ""})
}
return c.Status(http.StatusOK).JSON(map[string]any{"size": total, "cursor": cursor})
}
}

8
internal/model/meta.go Normal file
View File

@ -0,0 +1,8 @@
package model
type Meta struct {
Filename string `json:"filename" mapstructure:"filename"`
CreatedAt int64 `json:"created_at" mapstructure:"created_at"`
Size int64 `json:"size" mapstructure:"size"`
Uploader string `json:"uploader" mapstructure:"uploader"`
}

10
internal/model/user.go Normal file
View File

@ -0,0 +1,10 @@
package model
type User struct {
Id int `json:"id"`
Username string `json:"username"`
Key string `json:"key"`
Password string `json:"-"`
LoginAt int64 `json:"login_at"`
Token string `json:"token"`
}

View File

@ -1,10 +1,25 @@
package opt package opt
import (
"context"
"github.com/loveuer/nf/nft/log"
"github.com/loveuer/ushare/internal/pkg/tool"
)
type config struct { type config struct {
Debug bool Debug bool
Address string Address string
DataPath string
Auth string
} }
var ( var (
Cfg = &config{} Cfg = &config{}
) )
func Init(_ context.Context) {
if Cfg.Auth != "" {
Cfg.Auth = tool.NewPassword(Cfg.Auth)
log.Debug("opt.Init: encrypted password = %s", Cfg.Auth)
}
}

17
internal/opt/var.go Normal file
View File

@ -0,0 +1,17 @@
package opt
import "path/filepath"
const (
Meta = ".meta."
HeaderSize = "X-File-Size"
CodeLength = 8
)
func FilePath(code string) string {
return filepath.Join(Cfg.DataPath, code)
}
func MetaPath(code string) string {
return filepath.Join(Cfg.DataPath, Meta+code)
}

51
internal/pkg/db/client.go Normal file
View File

@ -0,0 +1,51 @@
package db
import (
"au99999/internal/opt"
"au99999/pkg/tool"
"context"
"gorm.io/gorm"
)
var Default *Client
type DBType string
const (
DBTypeSqlite = "sqlite"
DBTypeMysql = "mysql"
DBTypePostgres = "postgres"
)
type Client struct {
ctx context.Context
cli *gorm.DB
dbType DBType
}
func (c *Client) Type() DBType {
return c.dbType
}
func (c *Client) Session(ctxs ...context.Context) *gorm.DB {
var ctx context.Context
if len(ctxs) > 0 && ctxs[0] != nil {
ctx = ctxs[0]
} else {
ctx = tool.Timeout(30)
}
session := c.cli.Session(&gorm.Session{Context: ctx})
if opt.Cfg.Debug {
session = session.Debug()
}
return session
}
func (c *Client) Close() {
d, _ := c.cli.DB()
d.Close()
}

65
internal/pkg/db/new.go Normal file
View File

@ -0,0 +1,65 @@
package db
import (
"context"
"fmt"
"strings"
"github.com/glebarez/sqlite"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
const tip = `example:
for sqlite -> sqlite::<filepath>
sqlite::data.sqlite
sqlite::/data/data.db
for mysql -> mysql::<gorm_dsn>
mysql::user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local
for postgres -> postgres::<gorm_dsn>
postgres::host=localhost user=gorm password=gorm dbname=gorm port=9920 sslmode=disable TimeZone=Asia/Shanghai
`
func New(ctx context.Context, uri string) (*Client, error) {
parts := strings.SplitN(uri, "::", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("db.Init: db uri invalid\n%s", tip)
}
c := &Client{}
var (
err error
dsn = parts[1]
)
switch parts[0] {
case "sqlite":
c.dbType = DBTypeSqlite
c.cli, err = gorm.Open(sqlite.Open(dsn))
case "mysql":
c.dbType = DBTypeMysql
c.cli, err = gorm.Open(mysql.Open(dsn))
case "postgres":
c.dbType = DBTypePostgres
c.cli, err = gorm.Open(postgres.Open(dsn))
default:
return nil, fmt.Errorf("db type only support: [sqlite, mysql, postgres], unsupported db type: %s", parts[0])
}
if err != nil {
return nil, fmt.Errorf("db.Init: open %s with dsn:%s, err: %w", parts[0], dsn, err)
}
return c, nil
}
func Init(ctx context.Context, uri string) (err error) {
if Default, err = New(ctx, uri); err != nil {
return err
}
return nil
}

38
internal/pkg/tool/ctx.go Normal file
View File

@ -0,0 +1,38 @@
package tool
import (
"context"
"time"
)
func Timeout(seconds ...int) (ctx context.Context) {
var (
duration time.Duration
)
if len(seconds) > 0 && seconds[0] > 0 {
duration = time.Duration(seconds[0]) * time.Second
} else {
duration = time.Duration(30) * time.Second
}
ctx, _ = context.WithTimeout(context.Background(), duration)
return
}
func TimeoutCtx(ctx context.Context, seconds ...int) context.Context {
var (
duration time.Duration
)
if len(seconds) > 0 && seconds[0] > 0 {
duration = time.Duration(seconds[0]) * time.Second
} else {
duration = time.Duration(30) * time.Second
}
nctx, _ := context.WithTimeout(ctx, duration)
return nctx
}

View File

@ -0,0 +1,50 @@
package tool
import "fmt"
func HumanDuration(nano int64) string {
duration := float64(nano)
unit := "ns"
if duration >= 1000 {
duration /= 1000
unit = "us"
}
if duration >= 1000 {
duration /= 1000
unit = "ms"
}
if duration >= 1000 {
duration /= 1000
unit = " s"
}
return fmt.Sprintf("%6.2f%s", duration, unit)
}
func HumanSize(size int64) string {
const (
_ = iota
KB = 1 << (10 * iota) // 1 KB = 1024 bytes
MB // 1 MB = 1024 KB
GB // 1 GB = 1024 MB
TB // 1 TB = 1024 GB
PB // 1 PB = 1024 TB
)
switch {
case size >= PB:
return fmt.Sprintf("%.2f PB", float64(size)/PB)
case size >= TB:
return fmt.Sprintf("%.2f TB", float64(size)/TB)
case size >= GB:
return fmt.Sprintf("%.2f GB", float64(size)/GB)
case size >= MB:
return fmt.Sprintf("%.2f MB", float64(size)/MB)
case size >= KB:
return fmt.Sprintf("%.2f KB", float64(size)/KB)
default:
return fmt.Sprintf("%d bytes", size)
}
}

View File

@ -0,0 +1,68 @@
package tool
import "math"
func Map[T, R any](vals []T, fn func(item T, index int) R) []R {
var result = make([]R, len(vals))
for idx, v := range vals {
result[idx] = fn(v, idx)
}
return result
}
func Chunk[T any](vals []T, size int) [][]T {
if size <= 0 {
panic("Second parameter must be greater than 0")
}
chunksNum := len(vals) / size
if len(vals)%size != 0 {
chunksNum += 1
}
result := make([][]T, 0, chunksNum)
for i := 0; i < chunksNum; i++ {
last := (i + 1) * size
if last > len(vals) {
last = len(vals)
}
result = append(result, vals[i*size:last:last])
}
return result
}
// 对 vals 取样 x 个
func Sample[T any](vals []T, x int) []T {
if x < 0 {
panic("Second parameter can't be negative")
}
n := len(vals)
if n == 0 {
return []T{}
}
if x >= n {
return vals
}
// 处理x=1的特殊情况
if x == 1 {
return []T{vals[(n-1)/2]}
}
// 计算采样步长并生成结果数组
step := float64(n-1) / float64(x-1)
result := make([]T, x)
for i := 0; i < x; i++ {
// 计算采样位置并四舍五入
pos := float64(i) * step
index := int(math.Round(pos))
result[i] = vals[index]
}
return result
}

11
internal/pkg/tool/must.go Normal file
View File

@ -0,0 +1,11 @@
package tool
import "github.com/loveuer/nf/nft/log"
func Must(errs ...error) {
for _, err := range errs {
if err != nil {
log.Panic(err.Error())
}
}
}

View File

@ -0,0 +1,84 @@
package tool
import (
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"github.com/loveuer/nf/nft/log"
"golang.org/x/crypto/pbkdf2"
"regexp"
"strconv"
"strings"
)
const (
EncryptHeader string = "pbkdf2:sha256" // 用户密码加密
)
func NewPassword(password string) string {
return EncryptPassword(password, RandomString(8), int(RandomInt(50000)+100000))
}
func ComparePassword(in, db string) bool {
strs := strings.Split(db, "$")
if len(strs) != 3 {
log.Error("password in db invalid: %s", db)
return false
}
encs := strings.Split(strs[0], ":")
if len(encs) != 3 {
log.Error("password in db invalid: %s", db)
return false
}
encIteration, err := strconv.Atoi(encs[2])
if err != nil {
log.Error("password in db invalid: %s, convert iter err: %s", db, err)
return false
}
return EncryptPassword(in, strs[1], encIteration) == db
}
func EncryptPassword(password, salt string, iter int) string {
hash := pbkdf2.Key([]byte(password), []byte(salt), iter, 32, sha256.New)
encrypted := hex.EncodeToString(hash)
return fmt.Sprintf("%s:%d$%s$%s", EncryptHeader, iter, salt, encrypted)
}
func CheckPassword(password string) error {
if len(password) < 8 || len(password) > 32 {
return errors.New("密码长度不符合")
}
var (
err error
match bool
patternList = []string{`[0-9]+`, `[a-z]+`, `[A-Z]+`, `[!@#%]+`} //, `[~!@#$%^&*?_-]+`}
matchAccount = 0
tips = []string{"缺少数字", "缺少小写字母", "缺少大写字母", "缺少'!@#%'"}
locktips = make([]string, 0)
)
for idx, pattern := range patternList {
match, err = regexp.MatchString(pattern, password)
if err != nil {
log.Warn("regex match string err, reg_str: %s, err: %v", pattern, err)
return errors.New("密码强度不够")
}
if match {
matchAccount++
} else {
locktips = append(locktips, tips[idx])
}
}
if matchAccount < 3 {
return fmt.Errorf("密码强度不够, 可能 %s", strings.Join(locktips, ", "))
}
return nil
}

View File

@ -0,0 +1,20 @@
package tool
import "testing"
func TestEncPassword(t *testing.T) {
password := "123456"
result := EncryptPassword(password, RandomString(8), 50000)
t.Logf("sum => %s", result)
}
func TestPassword(t *testing.T) {
p := "wahaha@123"
p = NewPassword(p)
t.Logf("password => %s", p)
result := ComparePassword("wahaha@123", p)
t.Logf("compare result => %v", result)
}

View File

@ -0,0 +1,54 @@
package tool
import (
"crypto/rand"
"math/big"
)
var (
letters = []byte("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
letterNum = []byte("0123456789")
letterLow = []byte("abcdefghijklmnopqrstuvwxyz")
letterCap = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
letterSyb = []byte("!@#$%^&*()_+-=")
)
func RandomInt(max int64) int64 {
num, _ := rand.Int(rand.Reader, big.NewInt(max))
return num.Int64()
}
func RandomString(length int) string {
result := make([]byte, length)
for i := 0; i < length; i++ {
num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
result[i] = letters[num.Int64()]
}
return string(result)
}
func RandomPassword(length int, withSymbol bool) string {
result := make([]byte, length)
kind := 3
if withSymbol {
kind++
}
for i := 0; i < length; i++ {
switch i % kind {
case 0:
num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letterNum))))
result[i] = letterNum[num.Int64()]
case 1:
num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letterLow))))
result[i] = letterLow[num.Int64()]
case 2:
num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letterCap))))
result[i] = letterCap[num.Int64()]
case 3:
num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letterSyb))))
result[i] = letterSyb[num.Int64()]
}
}
return string(result)
}

124
internal/pkg/tool/table.go Normal file
View File

@ -0,0 +1,124 @@
package tool
import (
"encoding/json"
"fmt"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/loveuer/nf/nft/log"
"io"
"os"
"reflect"
"strings"
)
func TablePrinter(data any, writers ...io.Writer) {
var w io.Writer = os.Stdout
if len(writers) > 0 && writers[0] != nil {
w = writers[0]
}
t := table.NewWriter()
structPrinter(t, "", data)
_, _ = fmt.Fprintln(w, t.Render())
}
func structPrinter(w table.Writer, prefix string, item any) {
Start:
rv := reflect.ValueOf(item)
if rv.IsZero() {
return
}
for rv.Type().Kind() == reflect.Pointer {
rv = rv.Elem()
}
switch rv.Type().Kind() {
case reflect.Invalid,
reflect.Uintptr,
reflect.Chan,
reflect.Func,
reflect.UnsafePointer:
case reflect.Bool,
reflect.Int,
reflect.Int8,
reflect.Int16,
reflect.Int32,
reflect.Int64,
reflect.Uint,
reflect.Uint8,
reflect.Uint16,
reflect.Uint32,
reflect.Uint64,
reflect.Float32,
reflect.Float64,
reflect.Complex64,
reflect.Complex128,
reflect.Interface:
w.AppendRow(table.Row{strings.TrimPrefix(prefix, "."), rv.Interface()})
case reflect.String:
val := rv.String()
if len(val) <= 160 {
w.AppendRow(table.Row{strings.TrimPrefix(prefix, "."), val})
return
}
w.AppendRow(table.Row{strings.TrimPrefix(prefix, "."), val[0:64] + "..." + val[len(val)-64:]})
case reflect.Array, reflect.Slice:
for i := 0; i < rv.Len(); i++ {
p := strings.Join([]string{prefix, fmt.Sprintf("[%d]", i)}, ".")
structPrinter(w, p, rv.Index(i).Interface())
}
case reflect.Map:
for _, k := range rv.MapKeys() {
structPrinter(w, fmt.Sprintf("%s.{%v}", prefix, k), rv.MapIndex(k).Interface())
}
case reflect.Pointer:
goto Start
case reflect.Struct:
for i := 0; i < rv.NumField(); i++ {
p := fmt.Sprintf("%s.%s", prefix, rv.Type().Field(i).Name)
field := rv.Field(i)
//log.Debug("TablePrinter: prefix: %s, field: %v", p, rv.Field(i))
if !field.CanInterface() {
return
}
structPrinter(w, p, field.Interface())
}
}
}
func TableMapPrinter(data []byte) {
m := make(map[string]any)
if err := json.Unmarshal(data, &m); err != nil {
log.Warn(err.Error())
return
}
t := table.NewWriter()
addRow(t, "", m)
fmt.Println(t.Render())
}
func addRow(w table.Writer, prefix string, m any) {
rv := reflect.ValueOf(m)
switch rv.Type().Kind() {
case reflect.Map:
for _, k := range rv.MapKeys() {
key := k.String()
if prefix != "" {
key = strings.Join([]string{prefix, k.String()}, ".")
}
addRow(w, key, rv.MapIndex(k).Interface())
}
case reflect.Slice, reflect.Array:
for i := 0; i < rv.Len(); i++ {
addRow(w, fmt.Sprintf("%s[%d]", prefix, i), rv.Index(i).Interface())
}
default:
w.AppendRow(table.Row{prefix, m})
}
}

View File

@ -0,0 +1,73 @@
package tool
import (
"fmt"
"math"
)
func Min[T ~int | ~uint | ~int8 | ~uint8 | ~int16 | ~uint16 | ~int32 | ~uint32 | ~int64 | ~uint64 | ~float32 | ~float64](a, b T) T {
if a <= b {
return a
}
return b
}
func Mins[T ~int | ~uint | ~int8 | ~uint8 | ~int16 | ~uint16 | ~int32 | ~uint32 | ~int64 | ~uint64 | ~float32 | ~float64](vals ...T) T {
var val T
if len(vals) == 0 {
return val
}
val = vals[0]
for _, item := range vals[1:] {
if item < val {
val = item
}
}
return val
}
func Max[T ~int | ~uint | ~int8 | ~uint8 | ~int16 | ~uint16 | ~int32 | ~uint32 | ~int64 | ~uint64 | ~float32 | ~float64](a, b T) T {
if a >= b {
return a
}
return b
}
func Maxs[T ~int | ~uint | ~int8 | ~uint8 | ~int16 | ~uint16 | ~int32 | ~uint32 | ~int64 | ~uint64 | ~float32 | ~float64](vals ...T) T {
var val T
if len(vals) == 0 {
return val
}
for _, item := range vals {
if item > val {
val = item
}
}
return val
}
func Sum[T ~int | ~uint | ~int8 | ~uint8 | ~int16 | ~uint16 | ~int32 | ~uint32 | ~int64 | ~uint64 | ~float32 | ~float64](vals ...T) T {
var sum T = 0
for i := range vals {
sum += vals[i]
}
return sum
}
func Percent(val, minVal, maxVal, minPercent, maxPercent float64) string {
return fmt.Sprintf(
"%d%%",
int(math.Round(
((val-minVal)/(maxVal-minVal)*(maxPercent-minPercent)+minPercent)*100,
)),
)
}

View File

@ -0,0 +1,70 @@
package tool
import "testing"
func TestPercent(t *testing.T) {
type args struct {
val float64
minVal float64
maxVal float64
minPercent float64
maxPercent float64
}
tests := []struct {
name string
args args
want string
}{
{
name: "case 1",
args: args{
val: 0.5,
minVal: 0,
maxVal: 1,
minPercent: 0,
maxPercent: 1,
},
want: "50%",
},
{
name: "case 2",
args: args{
val: 0.3,
minVal: 0.1,
maxVal: 0.6,
minPercent: 0,
maxPercent: 1,
},
want: "40%",
},
{
name: "case 3",
args: args{
val: 700,
minVal: 700,
maxVal: 766,
minPercent: 0.1,
maxPercent: 0.7,
},
want: "10%",
},
{
name: "case 4",
args: args{
val: 766,
minVal: 700,
maxVal: 766,
minPercent: 0.1,
maxPercent: 0.7,
},
want: "70%",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Percent(tt.args.val, tt.args.minVal, tt.args.maxVal, tt.args.minPercent, tt.args.maxPercent); got != tt.want {
t.Errorf("Percent() = %v, want %v", got, tt.want)
}
})
}
}

14
main.go
View File

@ -3,7 +3,9 @@ package main
import ( import (
"context" "context"
"flag" "flag"
"github.com/loveuer/nf/nft/log"
"github.com/loveuer/ushare/internal/api" "github.com/loveuer/ushare/internal/api"
"github.com/loveuer/ushare/internal/controller"
"github.com/loveuer/ushare/internal/opt" "github.com/loveuer/ushare/internal/opt"
"os/signal" "os/signal"
"syscall" "syscall"
@ -11,14 +13,24 @@ import (
func init() { func init() {
flag.BoolVar(&opt.Cfg.Debug, "debug", false, "debug mode") flag.BoolVar(&opt.Cfg.Debug, "debug", false, "debug mode")
flag.StringVar(&opt.Cfg.Address, "address", "0.0.0.0:80", "") flag.StringVar(&opt.Cfg.Address, "address", "0.0.0.0:9119", "")
flag.StringVar(&opt.Cfg.DataPath, "data", "/data", "")
flag.StringVar(&opt.Cfg.Auth, "auth", "", "auth required(admin, password)")
flag.Parse() flag.Parse()
if opt.Cfg.Debug {
log.SetLogLevel(log.LogLevelDebug)
log.Debug("start server with debug mode")
}
} }
func main() { func main() {
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer cancel() defer cancel()
opt.Init(ctx)
controller.UserManager.Start(ctx)
controller.MetaManager.Start(ctx)
api.Start(ctx) api.Start(ctx)
<-ctx.Done() <-ctx.Done()

166
page/login.html Normal file
View File

@ -0,0 +1,166 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login Page</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
body {
background-color: #f5f5f5;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.login-container {
width: 100%;
padding: 20px;
display: flex;
justify-content: center;
align-items: center;
}
.login-box {
background: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 400px;
}
h2 {
text-align: center;
color: #333;
margin-bottom: 30px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
color: #555;
}
input[type="text"],
input[type="password"] {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 16px;
transition: border-color 0.3s ease;
}
input[type="text"]:focus,
input[type="password"]:focus {
border-color: #4CAF50;
outline: none;
}
.form-options {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.remember-me {
display: flex;
align-items: center;
gap: 5px;
color: #666;
}
.forgot-password {
color: #4CAF50;
text-decoration: none;
font-size: 14px;
}
.forgot-password:hover {
text-decoration: underline;
}
.login-button {
width: 100%;
padding: 12px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 5px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.login-button:hover {
background-color: #45a049;
}
.signup-link {
text-align: center;
margin-top: 20px;
color: #666;
}
.signup-link a {
color: #4CAF50;
text-decoration: none;
}
.signup-link a:hover {
text-decoration: underline;
}
/* Responsive Design */
@media (max-width: 480px) {
.login-box {
padding: 20px;
}
.form-options {
flex-direction: column;
gap: 10px;
align-items: flex-start;
}
}
</style>
</head>
<body>
<div class="login-container">
<div class="login-box">
<h2>Welcome Back</h2>
<form class="login-form">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required>
</div>
<div class="form-options">
<label class="remember-me">
<input type="checkbox"> Remember me
</label>
<a href="#" class="forgot-password">Forgot Password?</a>
</div>
<button type="submit" class="login-button">Log In</button>
</form>
<p class="signup-link">Don't have an account? <a href="#">Sign up</a></p>
</div>
</div>
</body>
</html>