26.9:版本号统一 + CI 精简 + 前端产物重建
CI 测试 / go vet + go test (push) Successful in 49s

- 全项目版本号统一:v3.x 迭代号(26.9/26.9/26.9/26.9 及裸 v2/v3)→ 26.9,
  覆盖 Go 注释 / 文档 / openapi.yaml / README×4 / 前端源码(80+ 处)
- v31_test.go 更名 custom_code_test.go;TestV2AccessorDefaults → TestKVAccessorDefaults
- docs/api/00-overview.md 更新日志合并为单条 26.9 条目(修复错位拼接)
- .goreleaser.yaml 头部注释与实际一致(Pro 2.18.1 / GITEA_TOKEN / semver tag 要求)
- CI:release-image.yml → ci.yml,仅保留 vet+test 门禁;
  镜像发布移交 GoReleaser Pro(原 build-push 的 tag 校验与 26.9 版本方案冲突,历史 9 次失败)
- 前端重建:server/web/dist 与 web-embed 同步(docs 文案嵌入更新)
This commit is contained in:
2026-09-08 03:16:51 +08:00
parent 27432218c4
commit 84df9996cb
122 changed files with 247 additions and 12268 deletions
+42
View File
@@ -0,0 +1,42 @@
name: CI 测试
on:
push:
branches: [main]
tags: ["v*"]
pull_request:
workflow_dispatch:
# 镜像发布不再走 CIGoReleaser Pro(本地 goreleaser release --clean
# 负责多平台归档 + Gitea Release + ACR 双架构镜像(见 .goreleaser.yaml)。
# 本工作流只做推送/PR 前置测试门禁。
jobs:
test:
name: go vet + go test
runs-on: ubuntu-latest
container:
image: golang:1.27.1-alpine
timeout-minutes: 30
steps:
- name: 安装工具并检出
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
apk add --no-cache git curl bash >/dev/null
SCHEME="${GITHUB_SERVER_URL%%://*}"
SRV="${GITHUB_SERVER_URL#*://}"
echo "clone from ${SCHEME}://${SRV}"
git clone --depth=1 --branch "$GITHUB_REF_NAME" \
"${SCHEME}://oauth2:${GITHUB_TOKEN}@${SRV}/${GITHUB_REPOSITORY}.git" .
- name: go vet + go test
working-directory: server
env:
GOCACHE: /tmp/.gocache
GOMODCACHE: /tmp/.gomodcache
CGO_ENABLED: "0"
run: |
go vet ./...
go test ./... -count=1
-135
View File
@@ -1,135 +0,0 @@
name: Release 镜像
on:
push:
branches: [main]
tags: ["v*", "26.*", "27.*"]
workflow_dispatch:
env:
REGISTRY: registry.cn-hangzhou.aliyuncs.com
IMAGE: registry.cn-hangzhou.aliyuncs.com/skymirror/fileshare
jobs:
test:
name: 测试(推送前置门禁)
runs-on: ubuntu-latest
container:
image: golang:1.27.1-alpine
timeout-minutes: 30
steps:
- name: 安装工具并检出
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
apk add --no-cache git curl bash >/dev/null
SCHEME="${GITHUB_SERVER_URL%%://*}"
SRV="${GITHUB_SERVER_URL#*://}"
echo "clone from ${SCHEME}://${SRV}"
git clone --depth=1 --branch "$GITHUB_REF_NAME" \
"${SCHEME}://oauth2:${GITHUB_TOKEN}@${SRV}/${GITHUB_REPOSITORY}.git" .
- name: go vet + go test
working-directory: server
env:
GOCACHE: /tmp/.gocache
GOMODCACHE: /tmp/.gomodcache
CGO_ENABLED: "0"
run: |
go vet ./...
go test ./... -count=1
build-push:
name: 多架构构建并推送 ACR
needs: test
runs-on: ubuntu-latest
# dind 自带独立 daemon:不依赖 runner 宿主机 docker.sock 转发配置
container:
image: docker:27-dind
options: --privileged
env:
DOCKER_HOST: unix:///var/run/docker.sock
DOCKER_TLS_CERTDIR: ""
timeout-minutes: 120
steps:
- name: 启动 dind daemon 并检出
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
apk add --no-cache git bash >/dev/null
# dind daemon 配置国内 registry mirrordocker.io 直连不可达)
mkdir -p /etc/docker
printf '{"registry-mirrors":["https://docker.m.daocloud.io","https://docker.1ms.run"]}' \
> /etc/docker/daemon.json
dockerd --host=unix:///var/run/docker.sock >/tmp/dockerd.log 2>&1 &
for i in $(seq 1 30); do docker info >/dev/null 2>&1 && break; sleep 1; done
docker info --format 'dind 就绪: {{.ServerVersion}}'
SCHEME="${GITHUB_SERVER_URL%%://*}"
SRV="${GITHUB_SERVER_URL#*://}"
git clone --depth=1 --branch "$GITHUB_REF_NAME" \
"${SCHEME}://oauth2:${GITHUB_TOKEN}@${SRV}/${GITHUB_REPOSITORY}.git" .
- name: 安装 QEMU binfmt(跨架构)
run: |
docker run --rm --privileged tonistiigi/binfmt:latest --install all 2>/dev/null \
|| docker run --rm --privileged docker.m.daocloud.io/tonistiigi/binfmt:latest --install all
- name: 计算 tag(以 APP_VERSION 为唯一版本源)
id: meta
env:
REF: ${{ gitea.ref }}
run: |
VER=$(sed -n 's/.*APP_VERSION = "\(.*\)".*/\1/p' server/cmd/server/main.go | head -1)
[ -n "$VER" ] || { echo "无法从 main.go 解析 APP_VERSION" >&2; exit 1; }
echo "APP_VERSION=$VER"
if [[ "$REF" == refs/tags/* ]]; then
# tag 触发:要求 tag 名与 APP_VERSION 一致,防错版发布
TAG_VER="${REF#refs/tags/}"
TAG_VER="${TAG_VER#v}"
[ "$TAG_VER" = "$VER" ] || { echo "tag($TAG_VER) != APP_VERSION($VER),拒绝发布" >&2; exit 1; }
fi
echo "version=$VER" >> "$GITHUB_OUTPUT"
echo "tags=${IMAGE}:${VER} ${IMAGE}:latest" >> "$GITHUB_OUTPUT"
- name: 登录阿里云 ACR
env:
ACR_USER: ${{ secrets.ACR_USERNAME }}
ACR_PASS: ${{ secrets.ACR_PASSWORD }}
run: |
echo "ACR 用户: $ACR_USER (密码 ${#ACR_PASS} 位)"
printf '%s' "$ACR_PASS" | docker login "$REGISTRY" -u "$ACR_USER" --password-stdin
- name: buildx 多架构构建并推送
env:
TAGS: ${{ steps.meta.outputs.tags }}
run: |
# docker-container 驱动才支持多平台。基础镜像/APK/NPM 全部走国内源:
# runner 侧网络对 docker.io 存在 DNS 污染(证书误配 facebook 域),不可直连
docker buildx create --name multiarch --driver docker-container >/dev/null 2>&1 || true
docker buildx use multiarch
ARGS=""
for t in $TAGS; do ARGS="$ARGS -t $t"; done
# provenance/sbom 必须关:阿里云 ACR 不识别 OCI empty manifestattestation
# buildx 会把 docker login 的 ACR 凭据转发给 buildkitd(私有基础镜像可拉)
docker buildx build \
--builder multiarch \
--platform linux/amd64,linux/arm64 \
--provenance=false --sbom=false \
--build-arg NODE_IMAGE=registry.cn-hangzhou.aliyuncs.com/skymirror/node:20-alpine \
--build-arg GO_IMAGE=registry.cn-hangzhou.aliyuncs.com/skymirror/golang:1.27.1-alpine \
--build-arg RUNTIME_IMAGE=registry.cn-hangzhou.aliyuncs.com/skymirror/alpine:3.20 \
--build-arg NPM_REGISTRY=https://registry.npmmirror.com \
--build-arg APK_MIRROR=https://mirrors.aliyun.com \
--push \
-f deploy/Dockerfile \
$ARGS \
.
- name: 校验远程 manifest(双架构)
env:
VERSION: ${{ steps.meta.outputs.version }}
run: |
docker buildx imagetools inspect "${IMAGE}:${VERSION}" | grep -E "linux/amd64|linux/arm64"
echo "推送完成: ${IMAGE}:${VERSION} + ${IMAGE}:latest"
+17 -8
View File
@@ -1,13 +1,22 @@
# 文件快传 GoReleaser 配置(OSS v2.14.1+
# 文件快传 GoReleaser 配置(Pro 2.18.1,二进制在 ~/Code/Releaser/goreleaser
#
# 用法:
# goreleaser release --snapshot --clean # 本地试跑,产出 ./dist
# goreleaser release --clean # 正式发布(需 git tag + 远端可写
# goreleaser release --snapshot --clean --skip=publish # 本地试跑,产出 ./dist
# goreleaser release --clean # 正式发布(需 semver tag
#
# 注入凭据的环境变量(不入库):
# GORELEASER_GITEA_TOKEN Gitea Personal Access Tokenuploads + release
# GORELEASER_ACR_USER ACR 用户名
# GORELEASER_ACR_PASS ACR 密码
# tag 必须是语义化版本(goreleaser 强制),如 v26.9.0(→ 版本 26.9.0),
# 并与 server/cmd/server/main.go 的 APP_VERSION 保持一致。
#
# 发布所需环境变量(不入库):
# GITEA_TOKEN Gitea Personal Access Tokenrelease + 资产上传)
# BUILDX_CONFIG=/tmp/buildx-config 可选:buildx 状态目录重定向(受限环境)
# DOCKER_CONFIG=/tmp/docker-config 可选:docker 配置重定向(manifest 需写 ~/.docker 时)
# ACR 登录:docker login registry.cn-hangzhou.aliyuncs.com(镜像推送用本机 docker 凭证)
#
# 产物:
# - 归档:linux/darwin/windows × amd64/arm64tar.gz + zip)→ Gitea Release
# - 镜像:registry.cn-hangzhou.aliyuncs.com/skymirror/fileshare:{版本}-amd64/-arm64
# + 多架构 manifest {版本} 与 latest → 阿里云 ACR
version: 2
@@ -156,7 +165,7 @@ docker_manifests:
- 'registry.cn-hangzhou.aliyuncs.com/skymirror/fileshare:{{ .Version }}-amd64'
- 'registry.cn-hangzhou.aliyuncs.com/skymirror/fileshare:{{ .Version }}-arm64'
# Gitea 服务器(v2.14 OSSgitea_urls 在顶层为对象;token 走 GORELEASER_TOKEN env
# Gitea 服务器(token 走 GITEA_TOKEN env
gitea_urls:
api: https://git.skymirror.top/api/v1
+10 -10
View File
@@ -5,10 +5,10 @@
数据库**默认 SQLite 零依赖**modernc.org/sqlite 纯 Go 驱动,数据文件 `./data/fileshare.db`),
可选切换 Postgres`FCB_DB_DRIVER=postgres` + DSN);Redis 为**可选**增强(未配置时自动降级为进程内存缓存)。
存储引擎支持 **本地 / S3 / WebDAV**(运行时热切换,健康检查通过才生效;WebDAV 重点优化:流式、Range、重试、连接复用)。
v3.2 起支持**上下行带宽限速**`upload_rate` / `download_rate` 字节/秒,0=不限速,管理端改后立即生效)。
26.9 起支持**上下行带宽限速**`upload_rate` / `download_rate` 字节/秒,0=不限速,管理端改后立即生效)。
**v3.2 新增**:上下行带宽限速(见 [专题](docs/api/13-bandwidth.md))· 站点对外域名、自定义提取码(v3.1)·
本地/S3/WebDAV 存储引擎 v3 运行时热切换。
**26.9 新增**:上下行带宽限速(见 [专题](docs/api/13-bandwidth.md))· 站点对外域名、自定义提取码(26.9)·
本地/S3/WebDAV 存储引擎 26.9 运行时热切换。
| 目录 | 说明 |
|---|---|
@@ -21,7 +21,7 @@ v3.2 起支持**上下行带宽限速**`upload_rate` / `download_rate` 字节
## 默认 Logo 与自定义
- 页面导航 Logo:本地资源 `web/src/assets/brand/logo.svg`
- favicon / 备用 Logo:本地资源 `web/src/assets/brand/favicon.png`v2 起不再使用远程 URL 默认值)
- favicon / 备用 Logo:本地资源 `web/src/assets/brand/favicon.png`26.9 起不再使用远程 URL 默认值)
管理端自定义 Logo 三步:
@@ -82,7 +82,7 @@ npm run build # 产出 web/dist/,构建时按 deploy/Dockerfil
|---|---|
| API 操作文档(概述/认证/分享/分片/预签名/管理后台/审计/存储/配置/错误码/Logo/
**带宽限速** | 站内 `/docs`,源文件 [docs/api/](docs/api/) |
| ↳ **v3.2 带宽限速**`upload_rate` / `download_rate`,管理端改后立即生效) | [docs/api/13-bandwidth.md](docs/api/13-bandwidth.md) |
| ↳ **26.9 带宽限速**`upload_rate` / `download_rate`,管理端改后立即生效) | [docs/api/13-bandwidth.md](docs/api/13-bandwidth.md) |
| OpenAPI 3.0 规范 + Swagger UI | 站内 `/openapi`,源文件 `docs/openapi.yaml` |
| 后端设计契约 | [server/README.md](server/README.md) |
| 前端说明与渲染约定 | [web/README.md](web/README.md) |
@@ -94,7 +94,7 @@ npm run build # 产出 web/dist/,构建时按 deploy/Dockerfil
| 变量 | 必需 | 默认 | 说明 |
|---|---|---|---|
| `FCB_DB_DRIVER` | ❌ | `sqlite` | 数据库驱动:`sqlite` \| `postgres`v2 需求 ⑧) |
| `FCB_DB_DRIVER` | ❌ | `sqlite` | 数据库驱动:`sqlite` \| `postgres`26.9 需求 ⑧) |
| `FCB_DB_DSN` | 视驱动 | - | postgres:连接串(**必需**);sqlite:文件路径(可空,默认 `./data/fileshare.db` |
| `FCB_REDIS_ADDR` | ❌ | 空 | 为空时缓存降级为内存实现;支持 `redis://[:password@]host:port[/db]` / `rediss://` URL 形式 |
| `FCB_REDIS_DB` | ❌ | `0` | Redis 逻辑库号 0-15URL 显式 `/N` 时以 URL 为准) |
@@ -115,16 +115,16 @@ npm run build # 产出 web/dist/,构建时按 deploy/Dockerfil
### 运行时配置(settings KV,管理端可改)
站点信息(`site_name``logo_url``favicon_url``page_explain` 等)、v2 展示与通知
站点信息(`site_name``logo_url``favicon_url``page_explain` 等)、26.9 展示与通知
`background_url``footer_text``footer_beian``notify_enabled``notify_title/content`)、
上传策略(`openUpload``enableChunk``uploadSize``allowed_file_types``expireStyle`
`code_generate_type``max_save_seconds``storageLimit`,及 v2 上限键 `max_save_count`
`code_generate_type``max_save_seconds``storageLimit`,及 26.9 上限键 `max_save_count`
`max_file_size`)、限流(`uploadCount/uploadMinute``errorCount/errorMinute`
`loginCount/loginMinute`)、安全(`adminSessionExpire``admin_token`/`jwt_secret` 由系统管理)。
v3.2 带宽(`upload_rate` / `download_rate`,字节/秒,0=不限速)。
26.9 带宽(`upload_rate` / `download_rate`,字节/秒,0=不限速)。
完整键表与默认值见《[环境变量与配置项](docs/api/10-config.md)》;
v3.2 带宽限速专题《[带宽限速](docs/api/13-bandwidth.md)》;
26.9 带宽限速专题《[带宽限速](docs/api/13-bandwidth.md)》;
修改接口见《[管理后台 API](docs/api/07-admin.md)》(`PATCH /admin/config/update`,改密自动轮换 jwt_secret)。
## 审计日志
+4 -4
View File
@@ -1,12 +1,12 @@
# 文件快传 部署编排(deploy/)
Go 1.27.1Gin + GORM+ Vue 3 重写版(v3.1)的容器化部署。数据库**默认 SQLite,零外部依赖**
Go 1.27.1Gin + GORM+ Vue 3 重写版(26.9)的容器化部署。数据库**默认 SQLite,零外部依赖**
modernc.org/sqlite 纯 Go 驱动,无需 Postgres),可选切换 Postgres`--profile postgres`);
Redis 为可选增强(`--profile redis`),未配置 `FCB_REDIS_ADDR` 时服务端自动降级为进程内存缓存。
> v3.1 功能提示:管理后台可设「站点对外域名」(内网部署生成公网分享链接)、
> 26.9 功能提示:管理后台可设「站点对外域名」(内网部署生成公网分享链接)、
> 分享时支持自定义提取码(4-8 位字母数字);这些均为运行时配置,无需改部署。
> v3.2 新增:管理后台可设「上行/下行带宽」限速(字节/秒,0=不限速),立即生效。
> 26.9 新增:管理后台可设「上行/下行带宽」限速(字节/秒,0=不限速),立即生效。
> S3 预签名直传(客户端→S3)服务端无法介入限速,其余下载/上传路径均覆盖。详见《[带宽限速](../docs/api/13-bandwidth.md)》。
## compose 服务与 profile 一览
@@ -73,7 +73,7 @@ docker compose --profile minio up -d --build # .env: FCB_STORAGE_ENGINE=s3
docker compose --profile webdav up -d --build # .env: FCB_STORAGE_ENGINE=webdav
```
> **v3 起支持运行时热切换**:也可不改 `.env`,直接在管理后台「系统设置 → 存储引擎」
> **26.9 起支持运行时热切换**:也可不改 `.env`,直接在管理后台「系统设置 → 存储引擎」
> 三选并保存引擎参数后点切换(健康检查通过才生效,失败保持原引擎),或调用
> `POST /admin/storage/switch`——均无需重启容器。`.env` 的 `FCB_STORAGE_ENGINE`
> 仅作为首次启动(KV 为空时)的默认引擎。
+5 -3
View File
@@ -5,8 +5,10 @@
## 更新日志
- **v3.2**:上下行带宽限速(`upload_rate` / `download_rate`,详见《[带宽限速](13-bandwidth.md)》)
- v3.1站点对外域名(`site_domain`)、自定义提取码(5~8 位)本文档与 `server/internal/api/` 实际实现逐一对齐,
- **26.9**:上下行带宽限速(`upload_rate` / `download_rate`,详见《[带宽限速](13-bandwidth.md)》)
站点对外域名(`site_domain`)、自定义提取码(5~8 位)
本文档与 `server/internal/api/` 实际实现逐一对齐,
交互式规范见站内 `/openapi`(源文件 `docs/openapi.yaml`)。
## Base URL
@@ -98,7 +100,7 @@
|---|---|
| 站点 Logo 自定义 | [12-logo.md](12-logo.md) |
| 错误码 | [11-errors.md](11-errors.md) |
| 带宽限速(v3.2 | [13-bandwidth.md](13-bandwidth.md) |
| 带宽限速(26.9 | [13-bandwidth.md](13-bandwidth.md) |
## 时间与编码
+2 -2
View File
@@ -18,7 +18,7 @@
- `day`/`hour`/`minute`:按时间过期,`expired_count = -1`
- `count`:按次数过期,取件 `expire_value` 次后失效(`expired_count = expire_value`);
**v2 需求 ④**`max_save_count>0``expire_value` 不得超出该上限,超限 403。
**26.9 需求 ④**`max_save_count>0``expire_value` 不得超出该上限,超限 403。
- `forever`:永久(需站点允许;`max_save_seconds>0` 时其他方式受最长保存上限约束,超限 403)。
> 可选值与上限来自公开配置 `GET /api/v1/config``expireStyle`、`max_save_seconds`、
@@ -88,4 +88,4 @@ curl -s "http://localhost:8466/share/select?code=8XQ2M"
你好,文件快传
```
**v3.1 变更**:① 支持 JSON 提交(`Content-Type: application/json`,字段同名);② 空文本 400「分享内容不能为空」;③ 可选 `code` 自定义提取码(5-8 位字母数字,占用 400)。
**26.9 变更**:① 支持 JSON 提交(`Content-Type: application/json`,字段同名);② 空文本 400「分享内容不能为空」;③ 可选 `code` 自定义提取码(5-8 位字母数字,占用 400)。
+1 -1
View File
@@ -39,7 +39,7 @@ curl -s -X POST http://localhost:8466/share/file \
{ "code": 403, "msg": "大小超过限制,最大为10.00 MB" }
```
> 大小上限为动态策略(v2 需求 ④⑩):管理端改 `max_file_size`0=回落 `uploadSize`)后
> 大小上限为动态策略(26.9 需求 ④⑩):管理端改 `max_file_size`0=回落 `uploadSize`)后
> **下一次上传立即按新上限执行**,无需重启;上限值可经 `GET /api/v1/config` 的
> `max_file_size`/`maxFileSize` 字段读取。
+1 -1
View File
@@ -130,7 +130,7 @@ curl -s -X POST http://localhost:8466/chunk/upload/3f6b8c2a4d5e6f708192a3b4c5d6e
> 约束:单分片 ≤ `chunk_size`init 声明值)且 ≤ **32MiB 硬上限**init 时 `chunk_size>33554432` 直接 400「chunk_size 过大」);
> 总大小(init 按分片数上限、上传/合并按累计)受**动态策略上限**约束——`max_file_size>0` 时为其,
> 否则回落 `uploadSize`v2 需求 ④⑩,管理端改后立即生效,超限清理会话);首个分片做 magic bytes
> 否则回落 `uploadSize`26.9 需求 ④⑩,管理端改后立即生效,超限清理会话);首个分片做 magic bytes
> 防伪校验(403「文件内容校验失败:…」)。分片哈希由服务端计算;合并时与分片记录交叉校验,不一致报 400。
>
> **enableChunk 开关**:管理端关闭分片上传后,`/chunk/upload/*` 全部端点返回 403「分片上传未启用」(后端强制,前端仅隐藏入口)。
+1 -1
View File
@@ -82,7 +82,7 @@ curl -s -X POST http://localhost:8466/presign/upload/init \
{ "code": 403, "msg": "大小超过限制,最大为10.00 MB" }
```
> 大小上限为动态策略(v2 需求 ④⑩):管理端改 `max_file_size`0=回落 `uploadSize`)后
> 大小上限为动态策略(26.9 需求 ④⑩):管理端改 `max_file_size`0=回落 `uploadSize`)后
> 立即按新上限校验 init 声明的 `file_size`。
```json
+6 -6
View File
@@ -318,7 +318,7 @@ curl -s "http://localhost:8466/admin/file/preview?id=41&maxChars=100" -H "Author
返回运行时配置 KV(含默认值与管理端修改)。`admin_token` 恒返回空串(屏蔽);
`jwt_secret` 不下发;存储引擎为进程级单例,`_engine_hint` 提示引擎配置修改需重启。
v2 新增键(需求 ①②③④⑩)一并返回:`background_url``footer_text``footer_beian`
26.9 新增键(需求 ①②③④⑩)一并返回:`background_url``footer_text``footer_beian`
`notify_enabled``max_save_count``max_file_size` 等。
```json
@@ -351,7 +351,7 @@ v2 新增键(需求 ①②③④⑩)一并返回:`background_url`、`foote
}
```
## 存储引擎热切换:POST /admin/storage/switchv3
## 存储引擎热切换:POST /admin/storage/switch26.9
运行时切换存储引擎,**无需重启**:
@@ -374,14 +374,14 @@ curl -s -X POST http://localhost:8466/admin/storage/switch \
部分更新(JSON 对象,未提供的键不变;表单亦可)。**PATCH 为主名,POST 为兼容别名**。
- 仅接受管理端可见键(见上响应键集合),未知键忽略。
- 数值型键自动转型:`openUpload``enableChunk``uploadSize``storageLimit`、限流四组、`max_save_seconds``adminSessionExpire``showAdminAddr`v2 新增 `max_save_count``max_file_size``notify_enabled``opacity` 为浮点。
- **v3.1**`site_domain`(站点对外域名)可经本端点设置,非法格式 400(仅 http/https、主机+端口、不带路径)。
- **v3 引擎键**`storage_engine` 不经本端点修改(走 `POST /admin/storage/switch`);引擎参数键
- 数值型键自动转型:`openUpload``enableChunk``uploadSize``storageLimit`、限流四组、`max_save_seconds``adminSessionExpire``showAdminAddr`26.9 新增 `max_save_count``max_file_size``notify_enabled``opacity` 为浮点。
- **26.9**`site_domain`(站点对外域名)可经本端点设置,非法格式 400(仅 http/https、主机+端口、不带路径)。
- **26.9 引擎键**`storage_engine` 不经本端点修改(走 `POST /admin/storage/switch`);引擎参数键
`local_storage_path``webdav_url``webdav_root_path``webdav_username``webdav_password`
`s3_endpoint_url``s3_region_name``s3_bucket_name``s3_access_key_id``s3_secret_access_key`
`aws_session_token``s3_addressing_style`)可经本端点保存——保存后对应引擎实例缓存失效,
下次切换/构建生效;敏感键空串或 `******` 表示不修改。
- **v2 schema 校验**`settings.KVSchema`,越界一律 400,中文错误信息):
- **26.9 schema 校验**`settings.KVSchema`,越界一律 400,中文错误信息):
- 整型边界:`max_file_size` ≤ 10GiB10737418240)、`max_save_count` ≤ 100000、`max_save_seconds` ≤ 31536000365 天)、`notify_enabled` ∈ {0,1}、`uploadCount` 1~10000、`uploadMinute` 1~1440 等;
- 字符串长度:`background_url` ≤ 2048、`footer_text` ≤ 2000、`footer_beian` ≤ 128、`notify_title` ≤ 128、`notify_content` ≤ 2000 字符;
- 列表键 `expireStyle` / `allowed_file_types`:须为字符串数组(或逗号分隔串)且至少保留一项;
+2 -2
View File
@@ -14,11 +14,11 @@ FCB_STORAGE_ENGINE=webdav
非法值直接启动失败:`FCB_STORAGE_ENGINE 无效值 "xxx",仅支持 local|s3|webdav`
**v3 运行时热切换**:管理端 `POST /admin/storage/switch`(或后台设置页「存储引擎」卡)可在不重启的情况下切换引擎——
**26.9 运行时热切换**:管理端 `POST /admin/storage/switch`(或后台设置页「存储引擎」卡)可在不重启的情况下切换引擎——
先构建新引擎并健康检查,通过才生效;失败 503 保持原引擎。当前引擎持久化在 settings KV `storage_engine`(空=回落启动值)。
各引擎参数(存储目录/服务地址/存储桶/密钥)同样在后台设置页运行时可改;保存后对应引擎实例缓存失效,下次切换/构建生效。
## 文件归属引擎(v3
## 文件归属引擎(26.9
每条分享记录(`file_codes.engine`)与上传会话(`upload_chunks.engine` / `presign_upload_sessions.engine`
在创建时戳记当时的引擎名。下载、分片合并、删除按**归属引擎**操作——切换引擎后,旧引擎里的文件仍可正常下载与删除
+16 -16
View File
@@ -1,7 +1,7 @@
# 环境变量与配置项
配置分三层:**默认值 → `FCB_*` 环境变量 → 数据库 settings KV(管理端运行时修改)**。
v2 起进程必需的环境变量为空集:数据库默认 **SQLite**modernc.org/sqlite 纯 Go 驱动,零外部依赖,
26.9 起进程必需的环境变量为空集:数据库默认 **SQLite**modernc.org/sqlite 纯 Go 驱动,零外部依赖,
DSN 缺省落 `./data/fileshare.db`);`FCB_DB_DRIVER=postgres``FCB_DB_DSN` 必需(需求 ⑧)。
## 环境变量(进程级)
@@ -31,14 +31,14 @@ DSN 缺省落 `./data/fileshare.db`);`FCB_DB_DRIVER=postgres` 时 `FCB_DB_DS
## 配置项(settings KV,默认值对齐参考实现)
> 键名/类型/默认值/边界以 `server/internal/config/schema.go` 的 `KVSchema()` 为单一事实来源
> schema 同步测试保证与 defaults() 逐键一致);v2 新增键统一 snake_case。
> schema 同步测试保证与 defaults() 逐键一致);26.9 新增键统一 snake_case。
### 站点信息与展示(需求 ①②③)
| 键 | 类型/边界 | 默认 | 说明 |
|---|---|---|---|
| `site_name` / `name` | string | 文件快传 | 站点名称(`site_name` 优先) |
| `site_domain` | string,≤256 | 空 | **v3.1**:站点对外域名(`http(s)://host[:port]`,不带路径;裸主机自动补 `http://`)。配置后分享链接(结果卡/管理端复制)用该域名生成——内网部署也能把公网链接发出去;留空=用当前访问地址 |
| `site_domain` | string,≤256 | 空 | **26.9**:站点对外域名(`http(s)://host[:port]`,不带路径;裸主机自动补 `http://`)。配置后分享链接(结果卡/管理端复制)用该域名生成——内网部署也能把公网链接发出去;留空=用当前访问地址 |
| `description` | string | 开箱即用的文件快传系统 | 站点描述 |
| `page_explain` | string | (合规声明) | 页面说明文案 |
| `keywords` | string | 文件快传, 文件分享… | SEO 关键词 |
@@ -46,10 +46,10 @@ DSN 缺省落 `./data/fileshare.db`);`FCB_DB_DRIVER=postgres` 时 `FCB_DB_DS
| `favicon_url` | string | 空(前端回落本地打包 `/assets/favicon-*.png`,需求 ⑤) | favicon / 备用 Logo |
| `opacity` | float | 0.9 | 界面不透明度 |
| `background` | string | 空 | 背景图 URL(参考实现既有键,v1 兼容保留) |
| `background_url` | string,≤2048 字符 | 空 | **v2 需求 ①**:背景图 URL 或上传后地址(空=主题默认;取值时 legacy `background` 键兜底)。管理端保存时校验协议白名单:仅 `http(s)``data:image/*` 与站内相对路径(防 `javascript:` 注入,非法 400 |
| `footer_text` | string,≤2000 字符 | 空 | **v2 需求 ②**:页脚自定义内容(纯文本或受控 HTML 片段) |
| `footer_beian` | string,≤128 字符 | 空 | **v2 需求 ②**:备案号(如 `京ICP备2024xxxxxx号-1`),展示于页脚 |
| `notify_enabled` | int0/1 | 1 | **v2 需求 ③**:通知开关(1=前台右上角悬浮窗展示 / 0=关闭) |
| `background_url` | string,≤2048 字符 | 空 | **26.9 需求 ①**:背景图 URL 或上传后地址(空=主题默认;取值时 legacy `background` 键兜底)。管理端保存时校验协议白名单:仅 `http(s)``data:image/*` 与站内相对路径(防 `javascript:` 注入,非法 400 |
| `footer_text` | string,≤2000 字符 | 空 | **26.9 需求 ②**:页脚自定义内容(纯文本或受控 HTML 片段) |
| `footer_beian` | string,≤128 字符 | 空 | **26.9 需求 ②**:备案号(如 `京ICP备2024xxxxxx号-1`),展示于页脚 |
| `notify_enabled` | int0/1 | 1 | **26.9 需求 ③**:通知开关(1=前台右上角悬浮窗展示 / 0=关闭) |
| `notify_title` | string,≤128 字符 | 系统通知 | 通知标题 |
| `notify_content` | string,≤2000 字符 | 欢迎使用… | 通知正文(**服务端白名单净化**:仅保留纯文本与 `<a href>` 为 http(s)/站内相对/`#` 锚点的链接,其余标签与事件属性剥离,保存与读取双侧生效) |
| `showAdminAddr` | int(0/1 | 0 | 是否展示后台入口 |
@@ -59,8 +59,8 @@ DSN 缺省落 `./data/fileshare.db`);`FCB_DB_DRIVER=postgres` 时 `FCB_DB_DS
| 键 | 类型/边界 | 默认 | 说明 |
|---|---|---|---|
| `max_save_seconds` | int640~31536000 | 0 | 最长保存秒数上限(0=仅默认 7 天兜底;>0 时按时间过期超限 403「限制最长时间为 X,可换用其他方式」)。**v3**:管理界面以「小时/天」下拉单位编辑(≥1 天自动显示天),提交时前端换算为秒——canonical 单位保持秒,接口语义不变 |
| `max_save_count` | int0~100000 | 0 | **v2 新增**:单次分享最大可取(保存)次数上限(0=不限制;`expire_style=count``expire_value` 超上限时 403「限制次数最多为 N 次」) |
| `max_save_seconds` | int640~31536000 | 0 | 最长保存秒数上限(0=仅默认 7 天兜底;>0 时按时间过期超限 403「限制最长时间为 X,可换用其他方式」)。**26.9**:管理界面以「小时/天」下拉单位编辑(≥1 天自动显示天),提交时前端换算为秒——canonical 单位保持秒,接口语义不变 |
| `max_save_count` | int0~100000 | 0 | **26.9 新增**:单次分享最大可取(保存)次数上限(0=不限制;`expire_style=count``expire_value` 超上限时 403「限制次数最多为 N 次」) |
| `expireStyle` | []string | `["day","hour","minute","forever","count"]` | 允许的过期方式白名单(上传时不在白名单 400「过期时间类型错误」) |
### 存储策略(需求 ④⑩)
@@ -68,7 +68,7 @@ DSN 缺省落 `./data/fileshare.db`);`FCB_DB_DRIVER=postgres` 时 `FCB_DB_DS
| 键 | 类型/边界 | 默认 | 说明 |
|---|---|---|---|
| `uploadSize` | int641024~10GiB | 1048576010MB) | 单文件大小上限(字节),参考实现语义;`max_file_size=0` 时作为生效上限 |
| `max_file_size` | int640~10GiB | 0 | **v2 新增**:存储策略-单文件上限(字节),0=回落 `uploadSize`;超出 403(文案 humanSize 自适应 B/KB/MB/GB)。**v3**:管理界面以「MB/GB」下拉单位编辑(≥1 GiB 自动显示 GB),提交时前端换算为字节 |
| `max_file_size` | int640~10GiB | 0 | **26.9 新增**:存储策略-单文件上限(字节),0=回落 `uploadSize`;超出 403(文案 humanSize 自适应 B/KB/MB/GB)。**26.9**:管理界面以「MB/GB」下拉单位编辑(≥1 GiB 自动显示 GB),提交时前端换算为字节 |
| `allowed_file_types` | []string | `["*"]` | 允许类型白名单(扩展名/MIME 通配,`*` 不限制;非白名单 403「不允许上传该类型文件」) |
| `storageLimit` | int64,≥0 | 0 | 站点总容量(字节),0=不限制(超限 507) |
| `openUpload` | int0/1) | 1 | 游客上传开关(0 时上传接口要求管理员令牌 403) |
@@ -79,7 +79,7 @@ DSN 缺省落 `./data/fileshare.db`);`FCB_DB_DRIVER=postgres` 时 `FCB_DB_DS
| 键 | 类型/边界 | 默认 | 说明 |
|---|---|---|---|
| `uploadCount` / `uploadMinute` | int1~10000 / 1~1440 | 10 / 1 | 窗口内允许上传次数 / 窗口分钟(上传成功才计数,超限 423;管理端修改后运行时同步限流规则,立即生效) |
| `upload_rate` / `download_rate` | int640~1 GiB/s | 0 / 0 | **v3.2**:上下行带宽字节/秒,0=不限速;管理端改后立即生效(每请求动态读 KV)。详见《[带宽限速](13-bandwidth.md)》 |
| `upload_rate` / `download_rate` | int640~1 GiB/s | 0 / 0 | **26.9**:上下行带宽字节/秒,0=不限速;管理端改后立即生效(每请求动态读 KV)。详见《[带宽限速](13-bandwidth.md)》 |
| `errorCount` / `errorMinute` | int | 10 / 1 | 取件错误(失败计数)+ metadata 每次计数 |
| `loginCount` / `loginMinute` | int | 5 / 15 | 登录失败计数 |
@@ -91,9 +91,9 @@ DSN 缺省落 `./data/fileshare.db`);`FCB_DB_DRIVER=postgres` 时 `FCB_DB_DS
| `jwt_secret` | 空 | JWT 签名密钥(初始化/改密时自动生成轮换;不下发;`settings.SensitiveKeys` 双模式下一致屏蔽) |
| `adminSessionExpire` | 604800(7 天) | 管理员会话秒数(须 1~365 整天) |
### 存储引擎(v3 运行时可配 + 热切换)
### 存储引擎(26.9 运行时可配 + 热切换)
**`storage_engine`**v3 新增键):string`local|s3|webdav`,默认空=回落启动值 `FCB_STORAGE_ENGINE`
**`storage_engine`**26.9 新增键):string`local|s3|webdav`,默认空=回落启动值 `FCB_STORAGE_ENGINE`
运行时切换走 **`POST /admin/storage/switch`**(JWT 保护):构建新引擎 → 健康检查通过才生效;
失败返回 503「存储引擎切换失败,已保持原引擎: …」且不改 KV。成功后持久化 `storage_engine`,重启沿用。
`GET /api/v1/config` 公开下发 `storage_engine` 当前名(仅名称,任何引擎参数/凭据不下发)。
@@ -115,10 +115,10 @@ DSN 缺省落 `./data/fileshare.db`);`FCB_DB_DRIVER=postgres` 时 `FCB_DB_DS
| `webdav_root_path` | `filebox_storage` |
| `webdav_proxy` | 0 |
> 敏感键 `webdav_password` / `s3_secret_access_key` / `aws_session_token`v3 加入 `settings.SensitiveKeys`):
> 敏感键 `webdav_password` / `s3_secret_access_key` / `aws_session_token`26.9 加入 `settings.SensitiveKeys`):
> 管理端 GET 返回掩码 `******`PATCH 时空串或 `******` 表示不修改。直接写库 settings KV 后重启同样生效。
## 策略动态生效机制(v2 需求 ④⑩)
## 策略动态生效机制(26.9 需求 ④⑩)
上传页通过 `GET /api/v1/config``config` 字段读取**当前策略快照**并在范围内渲染选项;
上传链路(`/share/file``/chunk/*``/presign/*`**每次请求实时读取** settings KV 同一组值校验:
@@ -129,7 +129,7 @@ DSN 缺省落 `./data/fileshare.db`);`FCB_DB_DRIVER=postgres` 时 `FCB_DB_DS
## 公共配置接口
前端启动时经 `GET /api/v1/config` 获取站点公开配置(无需认证;v2 扩展需求 ①②③④⑩):
前端启动时经 `GET /api/v1/config` 获取站点公开配置(无需认证;26.9 扩展需求 ①②③④⑩):
```json
{
+2 -2
View File
@@ -1,13 +1,13 @@
# Logo 自定义
## 默认 Logo(内置,v2 需求 ⑤)
## 默认 Logo(内置,26.9 需求 ⑤)
| 项 | 默认值 | 用途 |
|---|---|---|
| 页面导航 Logo | 前端打包本地资源 `/assets/logo-*.svg`(源:`web/src/assets/brand/logo.svg` | 导航栏 `<img>``config.logo_url` 为空时回落使用 |
| favicon / 备用 Logo | 前端打包本地资源 `/assets/favicon-*.png`(源:`web/src/assets/brand/favicon.png` | `index.html` `<link rel="icon">` + 动态 favicon 回落 |
v2 起默认不再引用远程 URL`GET /api/v1/config``logo_url`/`favicon_url` 默认下发空串,
26.9 起默认不再引用远程 URL`GET /api/v1/config``logo_url`/`favicon_url` 默认下发空串,
前端 `displayLogoUrl`/`displayFaviconUrl` 判空后回落到打包的本地资源。
管理端仍可设置任意 URL 全站替换(三步如下)。
+3 -3
View File
@@ -1,6 +1,6 @@
# 带宽限速(v3.2
# 带宽限速(26.9
> v3.2 新增。管理端可独立设置**上行(上传)/ 下行(下载)带宽**,单位字节/秒(前端 UI 友好单位为 MB/s),
> 26.9 新增。管理端可独立设置**上行(上传)/ 下行(下载)带宽**,单位字节/秒(前端 UI 友好单位为 MB/s),
> 0=不限速。修改后**立即生效**(每请求动态读取最新 KV,不需重启容器/进程)。
## 适用对象
@@ -38,7 +38,7 @@ return n, err
保证长期速率严格 ≤ `rate`,瞬时由调用方 Read 块大小自然突发。**不按字节微睡眠**——避免单次 Read 在低速场景下被调度抖动放大。
> 早期版本按每字节 sleep 1/rate 秒,被 Mac 调度粒度(约 10µs)放大后实际速率只有目标值的 1/3;v3.2.1 修正为整块对齐 sleep。
> 早期版本按每字节 sleep 1/rate 秒,被 Mac 调度粒度(约 10µs)放大后实际速率只有目标值的 1/3;26.9 修正为整块对齐 sleep。
## 公开接口
+6 -6
View File
@@ -93,7 +93,7 @@ paths:
footer_text: { type: string, description: '需求 ② 页脚自定义内容' }
footer_beian: { type: string, description: '需求 ② 备案号' }
storage_engine: { type: string, description: 当前存储引擎名(仅名称,凭据不下发) }
site_domain: { type: string, description: 'v3.1 站点对外域名(空=用当前访问地址)' }
site_domain: { type: string, description: '26.9 站点对外域名(空=用当前访问地址)' }
notify_enabled: { type: integer, enum: [0, 1], description: '需求 ③ 通知开关(1=右上角悬浮窗)' }
notify_title: { type: string }
notify_content: { type: string }
@@ -166,8 +166,8 @@ paths:
loginMinute: { type: integer, default: 15 }
uploadCount: { type: integer, default: 10 }
uploadMinute: { type: integer, default: 1 }
upload_rate: { type: integer, format: int64, default: 0, description: 'v3.2 上行带宽字节/秒;0=不限速' }
download_rate: { type: integer, format: int64, default: 0, description: 'v3.2 下行带宽字节/秒;0=不限速' }
upload_rate: { type: integer, format: int64, default: 0, description: '26.9 上行带宽字节/秒;0=不限速' }
download_rate: { type: integer, format: int64, default: 0, description: '26.9 下行带宽字节/秒;0=不限速' }
allowed_file_types: { type: string, default: '*' }
openUpload: { type: boolean, default: true }
enableChunk: { type: boolean, default: false }
@@ -207,7 +207,7 @@ paths:
文本 ≤222KB(请求体全局上限 1MiBContent-Length>441KB 直接 403);上传类接口(成功计数 upload 限流,423 超限),经审计中间件落库。
保存策略(需求 ④):expire_style 须在 expireStyle 白名单(400);count 型受
max_save_count 约束(403「限制次数最多为 N 次」);时间型受 max_save_seconds 约束(403)。
v3.1:支持 JSON 提交(字段同名);空文本 400「分享内容不能为空」;
26.9:支持 JSON 提交(字段同名);空文本 400「分享内容不能为空」;
可选 code 自定义提取码(5-8 位字母或数字,占用 400「该提取码已被占用」)。
requestBody:
content:
@@ -1377,8 +1377,8 @@ paths:
storageLimit: { type: integer, format: int64 }
uploadCount: { type: integer }
uploadMinute: { type: integer }
upload_rate: { type: integer, format: int64, description: 'v3.2 上行带宽字节/秒;0=不限速' }
download_rate: { type: integer, format: int64, description: 'v3.2 下行带宽字节/秒;0=不限速' }
upload_rate: { type: integer, format: int64, description: '26.9 上行带宽字节/秒;0=不限速' }
download_rate: { type: integer, format: int64, description: '26.9 下行带宽字节/秒;0=不限速' }
admin_token: { type: string, example: '' }
_engine_hint:
type: object
+1 -1
View File
@@ -83,7 +83,7 @@
- 位置:`server/internal/api/helpers.go:114-125``validatePickupCode`4 位下限)
- 原状:`code_generate_type=number` 时仅 9 万空间(5 位数字),默认 `errorCount=10/分/IP` 下单 IP 需约 6 天扫完,分布式多 IP 可显著缩短;自定义码允许 4 位(36⁴≈168 万)。
- ✅ 修复:自定义提码最小长度 4 → **5 位**`pickupCodeMinLen=5`36⁵≈6000 万空间);测试 `v31_test.go``TestPickupCodeMinLen` 同步更新。
- ✅ 修复:自定义提码最小长度 4 → **5 位**`pickupCodeMinLen=5`36⁵≈6000 万空间);测试 `custom_code_test.go``TestPickupCodeMinLen` 同步更新。
### L4 `enableChunk` 开关后端不强制 ✅ 已修复
+3 -3
View File
@@ -11,7 +11,7 @@ server/
├── cmd/server/main.go # 入口:装配 配置→DB→缓存→设置→审计→中间件→路由
└── internal/
├── config/ # 配置:FCB_* 环境变量基线 + DB settings KV 运行时覆盖
│ └── schema.go # v2 新配置键 schema(键名/类型/默认值/边界)
│ └── schema.go # 26.9 新配置键 schema(键名/类型/默认值/边界)
├── model/ # GORM 模型 + 双方言 AutoMigrate
├── cache/ # 缓存统一接口:redis.go / memory.go 双实现
├── database/ # 双方言连接与迁移(sqlite 默认 / postgres 可选)
@@ -43,7 +43,7 @@ server/
- 双方言共用 GORM 抽象:AutoMigrate、settings KV、全部业务查询方言无关;
唯一原生 DDLmigrates 台账表)在 `database.createMigratesTable` 内部分支处理。
## v2 新增配置键(internal/config/schema.go 为单一事实来源)
## 26.9 新增配置键(internal/config/schema.go 为单一事实来源)
| 键 | 类型 | 默认 | 说明 |
|---|---|---|---|
@@ -57,7 +57,7 @@ server/
| `max_file_size` | int64 | `0` | 需求 ⑩:存储策略-单文件上限字节(0=回落 `uploadSize`,≤10GiB |
| `uploadSize` / `allowed_file_types` / `storageLimit` / `openUpload` | 既有 | - | 存储策略既有键(语义不变) |
| `uploadCount` / `uploadMinute` | 既有 | `10` / `1` | 上传频率限制(对齐参考 `ip_limit["upload"]` |
| `upload_rate` / `download_rate` | int64 | `0` / `0` | **v3.2 带宽限速**:上下行字节/秒(0=不限速,≤1 GiB/s)。管理端改后立即生效(每请求动态读 KV);上传侧 middleware 包裹 `Request.Body`,下载侧在 `serveFile` 包裹 `storage.ReadCloser`;S3 预签名直传(客户端→S3)服务端无法限速。算法为时间窗对齐 sleep(`middleware/bandwidth.go` |
| `upload_rate` / `download_rate` | int64 | `0` / `0` | **26.9 带宽限速**:上下行字节/秒(0=不限速,≤1 GiB/s)。管理端改后立即生效(每请求动态读 KV);上传侧 middleware 包裹 `Request.Body`,下载侧在 `serveFile` 包裹 `storage.ReadCloser`;S3 预签名直传(客户端→S3)服务端无法限速。算法为时间窗对齐 sleep(`middleware/bandwidth.go` |
管理端与文档(t2/t4)以 `config.KVSchema()``settings.KVSchema()` re-export)为元数据源;
schema 同步测试保证 `KVSchema()``defaults()` 逐键一致。
+3 -3
View File
@@ -26,7 +26,7 @@ import (
"fileshare/internal/storage"
)
// APP_VERSION 版本号,对齐参考仓库 VERSION。v3.2 起改为 var 以便
// APP_VERSION 版本号,对齐参考仓库 VERSION。26.9 起改为 var 以便
// goreleaser 通过 -ldflags -X main.APP_VERSION=… 注入发布版本。
//
// 编译时可选注入(goreleaser 触发),不注入则保持默认 26.9。
@@ -105,7 +105,7 @@ func main() {
middleware.LimitMeta: {Count: cfg.GetInt("errorCount"), Window: minutes(cfg.GetInt("errorMinute"))},
})
// 7. 存储引擎:注入配置 → 构造 → 健康预检(需求 ④;v3 包装为可热切换 Manager
// 7. 存储引擎:注入配置 → 构造 → 健康预检(需求 ④;26.9 包装为可热切换 Manager
storage.SetEngineOptions(buildEngineOptions(cfg))
bootStore, err := storage.NewEngine(ctx, cfg.Engine())
if err != nil {
@@ -117,7 +117,7 @@ func main() {
} else {
log.Printf("[boot] 存储引擎 %s 健康检查通过", cfg.Engine())
}
// v3:Manager 包装——保存/读取委托当前引擎;管理端可热切换
// 26.9:Manager 包装——保存/读取委托当前引擎;管理端可热切换
//(构建闭包在每次切换前用最新 KV 刷新 EngineOptions,参数改动即时生效)
store := storage.NewManager(cfg.Engine(), bootStore, func(name string) (storage.Storage, error) {
storage.SetEngineOptions(buildEngineOptions(cfg))
+1 -1
View File
@@ -13,6 +13,7 @@ require (
github.com/glebarez/sqlite v1.11.0
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/redis/go-redis/v9 v9.17.0
golang.org/x/crypto v0.56.0
gorm.io/driver/postgres v1.6.2
gorm.io/gorm v1.31.2
)
@@ -66,7 +67,6 @@ require (
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
golang.org/x/arch v0.20.0 // indirect
golang.org/x/crypto v0.56.0 // indirect
golang.org/x/net v0.58.0 // indirect
golang.org/x/sync v0.22.0 // indirect
golang.org/x/sys v0.47.0 // indirect
+15 -15
View File
@@ -78,7 +78,7 @@ func registerAdmin(r *gin.Engine, d *Deps) {
authed.PATCH("/settings/password", d.adminChangePassword)
authed.POST("/settings/password", d.adminChangePassword)
// v3 存储引擎:运行时热切换(健康检查通过才生效,失败保持原引擎)
// 26.9 存储引擎:运行时热切换(健康检查通过才生效,失败保持原引擎)
authed.POST("/storage/switch", d.adminStorageSwitch)
// 审计日志查询(需求 ③;logs 为 list 的别名)
@@ -404,7 +404,7 @@ func buildAdminFileItem(fc *model.FileCodes, now time.Time) gin.H {
"hasDownloadLimit": fc.ExpiredCount >= 0, "has_download_limit": fc.ExpiredCount >= 0,
"isPermanent": fc.ExpiredAt == nil && fc.ExpiredCount < 0, "is_permanent": fc.ExpiredAt == nil && fc.ExpiredCount < 0,
"remainingDownloads": remaining, "remaining_downloads": remaining,
"engine": fc.Engine, // v3:归属引擎(管理端展示/排查用)
"engine": fc.Engine, // 26.9:归属引擎(管理端展示/排查用)
}
if fc.FileHash != nil {
item["fileHash"] = *fc.FileHash
@@ -784,7 +784,7 @@ func (d *Deps) adminFilePreview(c *gin.Context) {
// ============ 配置 ============
// configKeys 管理端可见/可改的配置键(不含 jwt_secretadmin_token 屏蔽展示)。
// v2 新增键(需求 ①②③④⑩):背景图、页脚、通知开关、保存/存储策略、频率限制。
// 26.9 新增键(需求 ①②③④⑩):背景图、页脚、通知开关、保存/存储策略、频率限制。
var configKeys = []string{
"site_name", "name", "description", "page_explain", "keywords",
"notify_title", "notify_content", "notify_enabled", "logo_url", "favicon_url",
@@ -794,11 +794,11 @@ var configKeys = []string{
"code_generate_type", "enableChunk",
"uploadMinute", "uploadCount", "errorMinute", "errorCount",
"loginCount", "loginMinute",
"opacity", "background", "showAdminAddr", "robotsText", "site_domain", // v3.1:站点对外域名
"upload_rate", "download_rate", // v3.2:上下行带宽字节/秒(0=不限速)
"opacity", "background", "showAdminAddr", "robotsText", "site_domain", // 26.9:站点对外域名
"upload_rate", "download_rate", // 26.9:上下行带宽字节/秒(0=不限速)
"adminSessionExpire", "storage_path", "local_storage_path",
"file_storage",
// v3 存储引擎与引擎参数(热切换;凭据为敏感键,get 掩码/update 空跳过)
// 26.9 存储引擎与引擎参数(热切换;凭据为敏感键,get 掩码/update 空跳过)
"storage_engine",
"local_storage_path",
"webdav_url", "webdav_root_path", "webdav_username", "webdav_password",
@@ -807,7 +807,7 @@ var configKeys = []string{
}
// intConfigKeys 需按 schema 边界校验的整型键(adminConfigUpdate 归一化用)。
// v1 既有键保留原语义;v2 新增键(max_file_size/max_save_count/notify_enabled
// v1 既有键保留原语义;26.9 新增键(max_file_size/max_save_count/notify_enabled
// 的边界来自 settings.KVSchema(单一事实来源在 config/schema.go)。
var intConfigKeys = []string{
"openUpload", "enableChunk", "showAdminAddr", "storageLimit",
@@ -815,7 +815,7 @@ var intConfigKeys = []string{
"loginCount", "loginMinute", "max_save_seconds", "uploadSize",
"adminSessionExpire",
"max_save_count", "max_file_size", "notify_enabled",
"upload_rate", "download_rate", // v3.2
"upload_rate", "download_rate", // 26.9
}
// validateConfigValue 按 settings.KVSchema 校验单个配置值:
@@ -905,7 +905,7 @@ func toStrSlice(v any) []string {
}
// adminConfigGet 读取配置(对齐参考 get_configadmin_token 屏蔽、jwt_secret 不下发)。
// v3:引擎凭据类敏感键返回掩码占位(前端表单"留空=不修改");storage_engine 为当前热切换后的引擎。
// 26.9:引擎凭据类敏感键返回掩码占位(前端表单"留空=不修改");storage_engine 为当前热切换后的引擎。
func (d *Deps) adminConfigGet(c *gin.Context) {
cfg := d.Cfg
out := gin.H{}
@@ -925,7 +925,7 @@ func (d *Deps) adminConfigGet(c *gin.Context) {
}
// jwt_secret 永不下发
delete(out, "jwt_secret")
// 引擎运行时状态(v3:热切换即时生效,无需重启)
// 引擎运行时状态(26.9:热切换即时生效,无需重启)
out["_engine_hint"] = gin.H{
"storage_backend": d.Store.CurrentName(),
"engines": gin.H{"local": true, "s3": true, "webdav": true},
@@ -973,7 +973,7 @@ func (d *Deps) adminConfigUpdate(c *gin.Context) {
if !known {
continue
}
// 类型归一 + schema 边界校验(v2:数值/字符串长度/列表键统一走 KVSchema)
// 类型归一 + schema 边界校验(26.9:数值/字符串长度/列表键统一走 KVSchema)
isInt := false
for _, key := range intConfigKeys {
if key == k {
@@ -1067,7 +1067,7 @@ func (d *Deps) adminConfigUpdate(c *gin.Context) {
dbPatch["jwt_secret"] = settings.GenerateJWTSecret()
}
// v3.1site_domain 规范化(http(s)://host[:port];空=用当前地址)
// 26.9site_domain 规范化(http(s)://host[:port];空=用当前地址)
if raw, ok := dbPatch["site_domain"]; ok {
sv, _ := raw.(string)
normalized, err := normalizeSiteDomain(sv)
@@ -1078,7 +1078,7 @@ func (d *Deps) adminConfigUpdate(c *gin.Context) {
dbPatch["site_domain"] = normalized
}
// v3 引擎键处理:参数键与 storage_engine 分离。
// 26.9 引擎键处理:参数键与 storage_engine 分离。
// 1) storage_engine 只接受合法枚举;
// 2) 敏感凭据键空串/掩码=不修改(避免管理端表单回显把密钥抹掉);
// 3) 先持久化普通键+参数键 → Invalidate 对应引擎缓存 → 再尝试 Switch 新引擎;
@@ -1152,7 +1152,7 @@ func engineOfParamKeys(patch map[string]any) []string {
return out
}
// adminStorageSwitch v3 存储引擎热切换:{engine:"local"|"s3"|"webdav"}。
// adminStorageSwitch 26.9 存储引擎热切换:{engine:"local"|"s3"|"webdav"}。
// 成功:持久化 storage_engine KV 并返回当前引擎;失败:503 且原引擎不变。
func (d *Deps) adminStorageSwitch(c *gin.Context) {
var body struct {
@@ -1302,7 +1302,7 @@ func (d *Deps) fileByID(c *gin.Context, id int64) (*model.FileCodes, error) {
// deleteFileCode 删除分享记录与存储文件(文本分享无存储文件)。
func (d *Deps) deleteFileCode(c *gin.Context, fc *model.FileCodes) error {
if fc.Text == nil && fc.FilePath != nil && fc.UUIDFileName != nil {
// v3:删除走文件归属引擎(旧引擎里的文件也要能删掉)
// 26.9:删除走文件归属引擎(旧引擎里的文件也要能删掉)
if delStore, dErr := d.storeFor(fc.Engine); dErr == nil {
if err := delStore.DeleteFile(c.Request.Context(), fileSavePath(fc)); err != nil && !errors.Is(err, storage.ErrNotFound) {
return errInternal("存储文件删除失败: " + err.Error())
+9 -9
View File
@@ -99,7 +99,7 @@ func (d *Deps) chunkInit(c *gin.Context) {
// 服务端按分片数上限校验总大小(防分片声明绕过)
totalChunks := (req.FileSize + chunkSize - 1) / chunkSize
maxPossible := totalChunks * chunkSize
// v2 需求 ④⑩:动态策略校验(max_file_size0=回落 uploadSize
// 26.9 需求 ④⑩:动态策略校验(max_file_size0=回落 uploadSize
if err := d.CurrentUploadPolicy().CheckSize(maxPossible); err != nil {
auditUploadEntry(c, "", safeName, req.FileSize, 0)
auditRecordFailed(c, d.AuditSvc, "文件大小超过限制")
@@ -165,7 +165,7 @@ func (d *Deps) chunkInit(c *gin.Context) {
ChunkHash: req.FileHash,
FileName: safeName,
SavePath: savePath,
Engine: d.Store.CurrentName(), // v3:会话归属引擎(分片/合并全程走同一引擎)
Engine: d.Store.CurrentName(), // 26.9:会话归属引擎(分片/合并全程走同一引擎)
}
if err := d.DB.WithContext(ctx).Create(&session).Error; err != nil {
releaseStorage(ctx, d.DB, resToken)
@@ -389,7 +389,7 @@ func (d *Deps) saveOneChunk(c *gin.Context, ctx context.Context, session *model.
ChunkSize: session.ChunkSize,
FileName: session.FileName,
SavePath: session.SavePath,
Engine: session.Engine, // v3:继承会话引擎
Engine: session.Engine, // 26.9:继承会话引擎
}
if err := d.DB.WithContext(ctx).
Where("upload_id = ? AND chunk_index = ?", session.UploadID, idx).
@@ -444,7 +444,7 @@ func (d *Deps) chunkStatus(c *gin.Context) {
type chunkCompleteRequest struct {
ExpireValue int `json:"expire_value" form:"expire_value"`
ExpireStyle string `json:"expire_style" form:"expire_style"`
Code string `json:"code" form:"code"` // v3.1:自定义提取码(4-8 位字母数字,空=随机)
Code string `json:"code" form:"code"` // 26.9:自定义提取码(4-8 位字母数字,空=随机)
}
// chunkComplete 合并分片并创建分享(对齐参考 complete_upload)。
@@ -489,7 +489,7 @@ func (d *Deps) chunkComplete(c *gin.Context) {
respondError(c, err)
return
}
// v3.1:自定义提取码(合并前校验,失败快速返回)
// 26.9:自定义提取码(合并前校验,失败快速返回)
if err := validatePickupCode(req.Code); err != nil {
auditUploadEntry(c, uploadID, session.FileName, session.FileSize, 0)
auditRecordFailed(c, d.AuditSvc, "提取码非法")
@@ -539,7 +539,7 @@ func (d *Deps) chunkComplete(c *gin.Context) {
}
return rec.ChunkHash, nil
}
// v3:合并走会话归属引擎(会话创建时的引擎,即使中途热切换也不受影响)
// 26.9:合并走会话归属引擎(会话创建时的引擎,即使中途热切换也不受影响)
mergeStore, sErr := d.storeFor(session.Engine)
if sErr != nil {
auditUploadEntry(c, uploadID, session.FileName, session.FileSize, session.FileSize)
@@ -556,7 +556,7 @@ func (d *Deps) chunkComplete(c *gin.Context) {
return
}
// 创建分享记录(v3.1:支持自定义提取码)
// 创建分享记录(26.9:支持自定义提取码)
code, err := pickCustomCode(ctx, d.DB, d.Cfg, req.Code)
if err == nil {
fc := model.FileCodes{
@@ -568,7 +568,7 @@ func (d *Deps) chunkComplete(c *gin.Context) {
ExpiredAt: exp.ExpiredAt,
ExpiredCount: exp.ExpiredCount,
UsedCount: exp.UsedCount,
Engine: session.Engine, // v3:归属引擎戳
Engine: session.Engine, // 26.9:归属引擎戳
}
// 拆分路径与文件名(对齐参考:path=dirname(save_path), uuid=basename
dir, name := splitDirBase(session.SavePath)
@@ -578,7 +578,7 @@ func (d *Deps) chunkComplete(c *gin.Context) {
fc.Prefix = trimExt(name)
fc.Suffix = ext
err = d.DB.WithContext(ctx).Create(&fc).Error
err = mapCodeConflict(err) // v3.1
err = mapCodeConflict(err) // 26.9
}
if err == nil {
// 成功:清理分片与记录(走归属引擎)
@@ -1,6 +1,6 @@
package api
// v3.1:自定义提取码与站点域名单元测试。
// 26.9:自定义提取码与站点域名单元测试。
import (
"encoding/json"
@@ -13,7 +13,7 @@ import (
"github.com/gin-gonic/gin"
)
// postForm 以 urlencoded 表单调用 handlerv3.1 测试辅助)。
// postForm 以 urlencoded 表单调用 handler26.9 测试辅助)。
func postForm(d *Deps, path string, fields map[string]string) *httptest.ResponseRecorder {
form := url.Values{}
for k, v := range fields {
+8 -8
View File
@@ -125,7 +125,7 @@ func generateCode(style string) string {
return string(b)
}
// ============ 自定义提取码(v3.1,防撞库)============
// ============ 自定义提取码(26.9,防撞库)============
const (
// pickupCodeMinLen 最小长度:L3 由 4 提升至 5(4 位码空间仅 168 万,
@@ -172,7 +172,7 @@ func pickCustomCode(ctx context.Context, db *gorm.DB, cfg *config.Config, custom
return custom, nil
}
// mapCodeConflict v3.1:分享记录创建失败时,若是自定义码唯一索引冲突(并发兜底,
// mapCodeConflict 26.9:分享记录创建失败时,若是自定义码唯一索引冲突(并发兜底,
// pickCustomCode 的预查重未覆盖竞态),转为友好 400;其余错误原样返回。
func mapCodeConflict(err error) error {
if err == nil {
@@ -188,9 +188,9 @@ func mapCodeConflict(err error) error {
return err
}
// ============ 站点对外域名(v3.1============
// ============ 站点对外域名(26.9============
// SiteDomain 站点对外域名规范化(v3.1):空串合法(分享链接用当前访问地址)。
// SiteDomain 站点对外域名规范化(26.9):空串合法(分享链接用当前访问地址)。
// 接受 http(s)://host[:port] 或裸 host[:port](自动补 http://,内网场景)。
// 拒绝路径/查询/片段/用户信息/非 http(s) 协议/非法主机字符(防 javascript: 注入分享链接)。
var siteDomainHostRe = regexp.MustCompile(`^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*$`)
@@ -275,7 +275,7 @@ type expireResult struct {
// resolveExpire 校验 expire_style 白名单并计算过期信息。
// 对齐参考:max_save_seconds>0 时为最长保存上限(超限 403),否则默认 7 天上限;
// v2 需求 ④:style=count 时 expire_value 不得超出 max_save_count0=不限制,超限 403)。
// 26.9 需求 ④:style=count 时 expire_value 不得超出 max_save_count0=不限制,超限 403)。
func resolveExpire(cfg *config.Config, expireValue int, expireStyle string) (*expireResult, error) {
allowed := cfg.ExpireStyle()
okStyle := false
@@ -358,7 +358,7 @@ func formatDurationCN(d time.Duration) string {
// ============ 存储路径 / 容量预留 ============
// storeFor v3:按归属引擎取存储实例(空戳/未知名回落当前引擎,兼容历史数据)。
// storeFor 26.9:按归属引擎取存储实例(空戳/未知名回落当前引擎,兼容历史数据)。
func (d *Deps) storeFor(engine string) (storage.Storage, error) {
if engine == "" || !storage.ValidEngine(engine) {
return d.Store, nil
@@ -656,7 +656,7 @@ func readMultipartHeader(f multipart.File, n int64) []byte {
// ============ 杂项 ============
// humanSize 把字节数转成人类可读描述(B/KB/MB/GB 自适应;
// v2 需求④:max_file_size 支持子 MB 上限,固定 MB 格式会显示 0.00 MB)。
// 26.9 需求④:max_file_size 支持子 MB 上限,固定 MB 格式会显示 0.00 MB)。
func humanSize(n int64) string {
const kb, mb, gb = int64(1024), int64(1024 * 1024), int64(1024 * 1024 * 1024)
switch {
@@ -728,7 +728,7 @@ func bindJSONOrForm(c *gin.Context, obj any) error {
}
return nil
}
// v3.1.1 兼容归一化:旧前端 bundlefetch 字符串 body 默认 text/plain)发的
// 26.9 兼容归一化:旧前端 bundlefetch 字符串 body 默认 text/plain)发的
// 是 text/plain + urlencoded 格式。此类请求改写 Content-Type 后走表单绑定,
// 否则 ShouldBind 对 text/plain 不解析,非空字段全部丢失。
base := ct
+1 -1
View File
@@ -1,4 +1,4 @@
// policy.go — v2 上传策略统一读取与校验(需求 ④⑩)。
// policy.go — 26.9 上传策略统一读取与校验(需求 ④⑩)。
//
// 管理端在后台设置页修改策略(settings KVt1 schema)后,上传链路
// share/file、chunk、presign)每次请求实时读取当前策略并动态校验:
+6 -6
View File
@@ -55,7 +55,7 @@ func newPolicyTestDeps(t *testing.T) *Deps {
if err != nil {
t.Fatalf("storage.NewLocalStorage: %v", err)
}
// v3:包装为 Managerbuild 直接返回 local 实例,测试无需真实多引擎)
// 26.9:包装为 Managerbuild 直接返回 local 实例,测试无需真实多引擎)
storeMgr := storage.NewManager("local", store, func(string) (storage.Storage, error) {
return storage.NewLocalStorage(filepath.Join(dir, "storage"))
})
@@ -141,13 +141,13 @@ func respBody(t *testing.T, w *httptest.ResponseRecorder) (code int, data map[st
// pngMagic 最小合法 PNG 头(magic 校验可识别)。
var pngMagic = []byte{0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n'}
// ============ ① 公开 configv2 展示与策略字段下发 ============
// ============ ① 公开 config26.9 展示与策略字段下发 ============
// TestPublicConfigV2Fields 验证 /api/v1/config 下发背景/页脚/备案/通知与策略范围,
// 且响应不包含任何敏感键(admin_token/jwt_secret)。
func TestPublicConfigV2Fields(t *testing.T) {
d := newPolicyTestDeps(t)
// 管理端先设置 v2 展示字段
// 管理端先设置 26.9 展示字段
if w := patchConfig(d, map[string]any{
"background_url": "https://cdn.example.com/bg.png",
"footer_text": "自定义页脚内容",
@@ -204,9 +204,9 @@ func TestPublicConfigV2Fields(t *testing.T) {
}
}
// ============ ② 管理端 get/updatev2 键全链路 + 类型范围校验 ============
// ============ ② 管理端 get/update26.9 键全链路 + 类型范围校验 ============
// TestAdminConfigV2RoundTrip 验证 v2 新键 update → get → public 的往返,
// TestAdminConfigV2RoundTrip 验证 26.9 新键 update → get → public 的往返,
// 且 admin_token 屏蔽、jwt_secret 不下发。
func TestAdminConfigV2RoundTrip(t *testing.T) {
d := newPolicyTestDeps(t)
@@ -480,7 +480,7 @@ func TestPolicySnapshotMatchesConfig(t *testing.T) {
// 编译期保证 fmt 被使用(测试辅助函数中错误路径占位)。
var _ = fmt.Sprintf
// ============ v3 存储引擎热切换 ============
// ============ 26.9 存储引擎热切换 ============
// switchEngine 调用 POST /admin/storage/switch。
func switchEngine(d *Deps, engine string) *httptest.ResponseRecorder {
+14 -14
View File
@@ -51,7 +51,7 @@ type presignInitRequest struct {
FileSize int64 `json:"file_size" form:"file_size"`
ExpireValue int `json:"expire_value" form:"expire_value"`
ExpireStyle string `json:"expire_style" form:"expire_style"`
Code string `json:"code" form:"code"` // v3.1:自定义提取码(init 时校验,完成时落库)
Code string `json:"code" form:"code"` // 26.9:自定义提取码(init 时校验,完成时落库)
}
// presignInit 初始化预签名上传(对齐参考 presign_upload_init):
@@ -74,7 +74,7 @@ func (d *Deps) presignInit(c *gin.Context) {
response.Fail(c, http.StatusBadRequest, "文件名非法")
return
}
// v3.1:自定义提取码提前校验(init 时快速失败;完成请求须再次携带)
// 26.9:自定义提取码提前校验(init 时快速失败;完成请求须再次携带)
if err := validatePickupCode(req.Code); err != nil {
auditRecordFailed(c, d.AuditSvc, "提取码非法")
respondError(c, err)
@@ -86,7 +86,7 @@ func (d *Deps) presignInit(c *gin.Context) {
respondError(c, err)
return
}
// v2 需求 ④⑩:动态策略校验(max_file_size0=回落 uploadSize
// 26.9 需求 ④⑩:动态策略校验(max_file_size0=回落 uploadSize
if err := d.CurrentUploadPolicy().CheckSize(req.FileSize); err != nil {
auditUploadEntry(c, "", safeName, req.FileSize, 0)
auditRecordFailed(c, d.AuditSvc, "文件大小超过限制")
@@ -146,7 +146,7 @@ func (d *Deps) presignInit(c *gin.Context) {
FileSize: req.FileSize,
SavePath: savePath,
Mode: mode,
Engine: d.Store.CurrentName(), // v3:会话归属引擎
Engine: d.Store.CurrentName(), // 26.9:会话归属引擎
ExpireValue: req.ExpireValue,
ExpireStyle: req.ExpireStyle,
ExpiresAt: time.Now().Add(presignSessionExpires * time.Second),
@@ -195,7 +195,7 @@ func (d *Deps) presignProxy(c *gin.Context) {
respondError(c, err)
return
}
// v3.1:自定义提取码随代理上传表单携带(init 时已预校验)
// 26.9:自定义提取码随代理上传表单携带(init 时已预校验)
if err := validatePickupCode(c.PostForm("code")); err != nil {
auditUploadEntry(c, uploadID, session.FileName, session.FileSize, 0)
auditRecordFailed(c, d.AuditSvc, "提取码非法")
@@ -234,7 +234,7 @@ func (d *Deps) presignProxy(c *gin.Context) {
if err == nil {
defer func() { _ = f.Close() }()
if err = validateFileMagic(d.Cfg, session.FileName, fh.Header.Get("Content-Type"), readMultipartHeader(f, 64)); err == nil {
// v3:落盘走会话归属引擎
// 26.9:落盘走会话归属引擎
var ps storage.Storage
ps, sErr := d.storeFor(session.Engine)
if sErr != nil {
@@ -259,7 +259,7 @@ func (d *Deps) presignProxy(c *gin.Context) {
releaseStorage(ctx, d.DB, "presign:"+uploadID)
if err != nil {
if ps, sErr := d.storeFor(session.Engine); sErr == nil {
_ = ps.DeleteFile(ctx, session.SavePath) // v3:清理走归属引擎
_ = ps.DeleteFile(ctx, session.SavePath) // 26.9:清理走归属引擎
}
auditUploadEntry(c, uploadID, session.FileName, session.FileSize, 0)
auditRecordFailed(c, d.AuditSvc, "创建分享失败")
@@ -299,7 +299,7 @@ func (d *Deps) presignConfirm(c *gin.Context) {
return
}
// v3:直传文件存在性按会话归属引擎检查(直传可能落在旧引擎)
// 26.9:直传文件存在性按会话归属引擎检查(直传可能落在旧引擎)
psCheck, sErr := d.storeFor(session.Engine)
if sErr != nil {
auditUploadEntry(c, uploadID, session.FileName, session.FileSize, 0)
@@ -307,7 +307,7 @@ func (d *Deps) presignConfirm(c *gin.Context) {
respondError(c, mapStorageError(sErr))
return
}
// v3.1:自定义提取码随确认请求携带(query 或 JSON/form body,均可选)
// 26.9:自定义提取码随确认请求携带(query 或 JSON/form body,均可选)
customCode := c.Query("code")
if customCode == "" && c.Request.Body != nil && c.Request.ContentLength != 0 {
var fin struct {
@@ -378,7 +378,7 @@ func (d *Deps) presignConfirm(c *gin.Context) {
releaseStorage(ctx, d.DB, "presign:"+uploadID)
if err != nil {
if ps, sErr := d.storeFor(session.Engine); sErr == nil {
_ = ps.DeleteFile(ctx, session.SavePath) // v3:清理走归属引擎
_ = ps.DeleteFile(ctx, session.SavePath) // 26.9:清理走归属引擎
}
auditUploadEntry(c, uploadID, session.FileName, session.FileSize, 0)
auditRecordFailed(c, d.AuditSvc, "创建分享失败")
@@ -399,7 +399,7 @@ func (d *Deps) createRecordFromSession(c *gin.Context, session *model.PresignUpl
if err != nil {
return "", err
}
// v3.1:完成请求的自定义提取码兜底校验(init 已验,防只发完成请求绕过)
// 26.9:完成请求的自定义提取码兜底校验(init 已验,防只发完成请求绕过)
if err := validatePickupCode(customCode); err != nil {
return "", err
}
@@ -420,10 +420,10 @@ func (d *Deps) createRecordFromSession(c *gin.Context, session *model.PresignUpl
ExpiredAt: exp.ExpiredAt,
ExpiredCount: exp.ExpiredCount,
UsedCount: exp.UsedCount,
Engine: session.Engine, // v3:归属引擎戳
Engine: session.Engine, // 26.9:归属引擎戳
}
if err := d.DB.WithContext(ctx).Create(&fc).Error; err != nil {
return "", mapCodeConflict(err) // v3.1:并发占用自定义码 → 友好 400
return "", mapCodeConflict(err) // 26.9:并发占用自定义码 → 友好 400
}
return code, nil
}
@@ -474,7 +474,7 @@ func (d *Deps) presignCancel(c *gin.Context) {
return
}
if session.Mode == "direct" {
// v3:清理走会话归属引擎
// 26.9:清理走会话归属引擎
if ps, sErr := d.storeFor(session.Engine); sErr == nil {
if exists, eErr := ps.FileExists(ctx, session.SavePath); eErr == nil && exists {
_ = ps.DeleteFile(ctx, session.SavePath)
+4 -4
View File
@@ -21,7 +21,7 @@ type Deps struct {
Mgr *settings.Manager
AuditSvc *audit.Service
Limiter *middleware.RateLimiter
Store *storage.Manager // v3:可热切换引擎管理器(实现 Storage 接口)
Store *storage.Manager // 26.9:可热切换引擎管理器(实现 Storage 接口)
Version string
}
@@ -104,7 +104,7 @@ func (d *Deps) robotsText(c *gin.Context) {
c.Data(http.StatusOK, "text/plain; charset=utf-8", []byte(d.Cfg.GetString("robotsText")))
}
// publicConfig 公共配置(前端首页/上传页所需;v2 需求 ①②③④⑩ 扩展):
// publicConfig 公共配置(前端首页/上传页所需;26.9 需求 ①②③④⑩ 扩展):
// - 展示字段:站点信息、Logo/favicon、背景图、页脚文案/备案号、通知;
// - 策略范围(上传页动态渲染):大小上限、类型白名单、过期方式、保存
// 时间/次数上限、上传频率(仅范围,不含内部实现键)。
@@ -132,10 +132,10 @@ func (d *Deps) publicConfig(c *gin.Context) {
// 需求 ②:页脚自定义内容与备案号
"footer_text": cfg.FooterText(),
"footer_beian": cfg.FooterBeian(),
// v3:当前存储引擎名(仅名称,任何引擎参数/凭据不下发)
// 26.9:当前存储引擎名(仅名称,任何引擎参数/凭据不下发)
"storage_engine": d.Store.CurrentName(),
"site_domain": d.Cfg.SiteDomain(),
// v3.2:上下行带宽字节/秒(公开下发,0=不限速,方便管理端展示当前值)
// 26.9:上下行带宽字节/秒(公开下发,0=不限速,方便管理端展示当前值)
"upload_rate": d.Cfg.UploadRate(),
"download_rate": d.Cfg.DownloadRate(),
// 需求 ③:系统通知(开关 + 内容,前台右上角悬浮窗)
+11 -11
View File
@@ -90,7 +90,7 @@ func (d *Deps) shareText(c *gin.Context) {
if !requireUploadLimit(c, d.Limiter) {
return
}
// v3.1 修复:JSON/表单/ultipart 统一绑定(form+json 双标签——此前仅 PostForm 时,
// 26.9 修复:JSON/表单/ultipart 统一绑定(form+json 双标签——此前仅 PostForm 时,
// JSON 提交会静默存成空文本并 200,取件页空白)。
var body struct {
Text string `json:"text" form:"text"`
@@ -120,7 +120,7 @@ func (d *Deps) shareText(c *gin.Context) {
if expireStyle == "" {
expireStyle = "day"
}
// v3.1:自定义提取码格式校验(4-8 位字母数字,空=随机)
// 26.9:自定义提取码格式校验(4-8 位字母数字,空=随机)
if err := validatePickupCode(body.Code); err != nil {
respondError(c, err)
return
@@ -153,11 +153,11 @@ func (d *Deps) shareText(c *gin.Context) {
ExpiredAt: exp.ExpiredAt,
ExpiredCount: exp.ExpiredCount,
UsedCount: exp.UsedCount,
Engine: d.Store.CurrentName(), // v3:归属引擎戳(文本也记录,保持一致性)
Engine: d.Store.CurrentName(), // 26.9:归属引擎戳(文本也记录,保持一致性)
}
err = d.DB.WithContext(ctx).Create(&fc).Error
}
err = mapCodeConflict(err) // v3.1:自定义码唯一索引冲突 → 友好 400
err = mapCodeConflict(err) // 26.9:自定义码唯一索引冲突 → 友好 400
releaseStorage(ctx, d.DB, token)
if err != nil {
auditRecordFailed(c, d.AuditSvc, "文本分享创建失败")
@@ -188,7 +188,7 @@ func (d *Deps) shareFile(c *gin.Context) {
return
}
origName := fh.Filename
// v2 需求 ④⑩:动态策略校验(max_file_size0=回落 uploadSize
// 26.9 需求 ④⑩:动态策略校验(max_file_size0=回落 uploadSize
if err := d.CurrentUploadPolicy().CheckSize(fh.Size); err != nil {
auditUploadEntry(c, "", origName, fh.Size, 0)
auditRecordFailed(c, d.AuditSvc, "大小超过限制")
@@ -205,7 +205,7 @@ func (d *Deps) shareFile(c *gin.Context) {
return
}
// v3.1:自定义提取码(落盘前校验,失败快速返回不占容量预留)
// 26.9:自定义提取码(落盘前校验,失败快速返回不占容量预留)
if err := validatePickupCode(c.PostForm("code")); err != nil {
auditUploadEntry(c, "", origName, fh.Size, 0)
auditRecordFailed(c, d.AuditSvc, "提取码非法")
@@ -257,10 +257,10 @@ func (d *Deps) shareFile(c *gin.Context) {
ExpiredAt: exp.ExpiredAt,
ExpiredCount: exp.ExpiredCount,
UsedCount: exp.UsedCount,
Engine: d.Store.CurrentName(), // v3:归属引擎戳(下载按此取回)
Engine: d.Store.CurrentName(), // 26.9:归属引擎戳(下载按此取回)
}
if err = d.DB.WithContext(ctx).Create(&fc).Error; err != nil {
err = mapCodeConflict(err) // v3.1
err = mapCodeConflict(err) // 26.9
// 记录创建失败:清理已落盘文件
_ = d.Store.DeleteFile(ctx, savePath)
}
@@ -273,7 +273,7 @@ func (d *Deps) shareFile(c *gin.Context) {
auditUploadEntry(c, "", origName, fh.Size, 0)
auditRecordFailed(c, d.AuditSvc, "文件保存失败")
if be, ok := err.(*apiError); ok && be.Status == http.StatusBadRequest {
respondError(c, err) // v3.1:提取码冲突等业务 400 原样透出
respondError(c, err) // 26.9:提取码冲突等业务 400 原样透出
} else {
respondError(c, mapStorageError(err))
}
@@ -544,7 +544,7 @@ func (d *Deps) serveFile(c *gin.Context, fc *model.FileCodes) {
savePath := fileSavePath(fc)
name := fc.Prefix + fc.Suffix
// v3:按文件归属引擎取回(切换引擎后旧文件仍可下载);空戳=历史数据回落当前引擎
// 26.9:按文件归属引擎取回(切换引擎后旧文件仍可下载);空戳=历史数据回落当前引擎
store, err := d.storeFor(fc.Engine)
if err != nil {
auditUploadEntry(c, fc.Code, name, fc.Size, 0)
@@ -597,7 +597,7 @@ func (d *Deps) serveFile(c *gin.Context, fc *model.FileCodes) {
}
auditUploadEntry(c, fc.Code, name, total, 0)
c.Status(status)
// v3.2:下载带宽限速(storage.ReadCloser → 限速 reader → c.Writer
// 26.9:下载带宽限速(storage.ReadCloser → 限速 reader → c.Writer
dlReader := middleware.WrapReadCloser(dl, d.Cfg.DownloadRate())
n, _ := io.Copy(c.Writer, dlReader)
middleware.AuditSet(c, func(e *audit.Entry) {
+7 -7
View File
@@ -28,7 +28,7 @@ const (
)
// DefaultLogoURL / DefaultFaviconURL 默认 Logo 与 favicon(需求 ⑤):
// v2 起默认改用前端打包的本地资源(web/src/assets/brand/logo.svg + favicon.png
// 26.9 起默认改用前端打包的本地资源(web/src/assets/brand/logo.svg + favicon.png
// 经 Vite 产出 /assets/logo-*.svg 与 /assets/favicon-*.png)。此处留空,
// GET /api/v1/config 下发空值时前端 displayLogoUrl/displayFaviconUrl 回落到本地打包资源;
// 管理端仍可设置任意 URL 全站替换。
@@ -62,7 +62,7 @@ func defaults() map[string]any {
"file_storage": "local",
"storage_path": "",
"storageLimit": 0,
// v3:存储引擎运行时可配(热切换);空=沿用 Env.StorageEngine 启动值
// 26.9:存储引擎运行时可配(热切换);空=沿用 Env.StorageEngine 启动值
"storage_engine": "",
"site_domain": "",
"upload_rate": "0",
@@ -78,7 +78,7 @@ func defaults() map[string]any {
// 需求 ⑤:默认 Logo 与 favicon(空 = 前端使用打包的本地资源)
"logo_url": DefaultLogoURL,
"favicon_url": DefaultFaviconURL,
// 需求 ①:背景图(v2 新增 background_urlbackground 为参考实现既有键,保留兼容)
// 需求 ①:背景图(26.9 新增 background_urlbackground 为参考实现既有键,保留兼容)
"background": "",
"background_url": "",
// 需求 ②:页脚自定义内容与备案号
@@ -280,7 +280,7 @@ func (c *Config) GetBool(key string) bool {
}
// GetStringSlice 取字符串切片配置。
// UploadRate 上传带宽字节/秒(v3.20=不限速)。
// UploadRate 上传带宽字节/秒(26.90=不限速)。
func (c *Config) UploadRate() int {
v := c.GetInt(KeyUploadRate)
if v < 0 {
@@ -289,7 +289,7 @@ func (c *Config) UploadRate() int {
return v
}
// DownloadRate 下载带宽字节/秒(v3.20=不限速)。
// DownloadRate 下载带宽字节/秒(26.90=不限速)。
func (c *Config) DownloadRate() int {
v := c.GetInt(KeyDownloadRate)
if v < 0 {
@@ -298,7 +298,7 @@ func (c *Config) DownloadRate() int {
return v
}
// SiteDomain 站点对外域名(v3.1):空=分享链接用当前访问地址。
// SiteDomain 站点对外域名(26.9):空=分享链接用当前访问地址。
func (c *Config) SiteDomain() string {
return strings.TrimRight(strings.TrimSpace(c.GetString("site_domain")), "/")
}
@@ -421,7 +421,7 @@ func (c *Config) AdminSessionExpireSeconds() int {
}
// Engine 当前存储引擎。
// Engine 返回当前存储引擎名:KV storage_engine 优先(v3 运行时可改),
// Engine 返回当前存储引擎名:KV storage_engine 优先(26.9 运行时可改),
// 空(未设置/历史数据)回落启动值 Env.StorageEngineenv 校验过的 local|s3|webdav)。
// 枚举校验内联(避免 config→storage 反向依赖)。
func (c *Config) Engine() string {
+13 -13
View File
@@ -1,36 +1,36 @@
// Package config — schema.go 定义 v2 新增配置键(KVschema
// Package config — schema.go 定义 26.9 新增配置键(KVschema
// 键名常量、类型、默认值与取值边界。管理与 API 层(t2)按下表读写与校验,
// 文档(t4)按本表生成说明。键名除参考实现既有 camelCase 键外,
// v2 新增键统一 snake_case。
// 26.9 新增键统一 snake_case。
package config
// —— v2 新增/沿用键名常量(单一事实来源;settings 包会 re-export)——
// 命名规则:v2 新增键 snake_case;与参考实现对齐的既有键保持原拼写。
// —— 26.9 新增/沿用键名常量(单一事实来源;settings 包会 re-export)——
// 命名规则:26.9 新增键 snake_case;与参考实现对齐的既有键保持原拼写。
const (
// 需求 ①:背景图
KeyBackground = "background" // 参考实现既有键(v1 兼容保留)
KeyBackgroundURL = "background_url" // v2 新增:背景图 URL 或上传后的访问地址(空=默认主题)
KeyBackgroundURL = "background_url" // 26.9 新增:背景图 URL 或上传后的访问地址(空=默认主题)
// 需求 ②:页脚
KeyFooterText = "footer_text" // v2 新增:页脚自定义内容(纯文本或受控 HTML 片段)
KeyFooterBeian = "footer_beian" // v2 新增:备案号(如 京ICP备2024xxxxxx号-1
KeyFooterText = "footer_text" // 26.9 新增:页脚自定义内容(纯文本或受控 HTML 片段)
KeyFooterBeian = "footer_beian" // 26.9 新增:备案号(如 京ICP备2024xxxxxx号-1
// 需求 ③:系统通知
KeyNotifyEnabled = "notify_enabled" // v2 新增:通知开关,1 开启 / 0 关闭
KeyNotifyEnabled = "notify_enabled" // 26.9 新增:通知开关,1 开启 / 0 关闭
KeyNotifyTitle = "notify_title" // 既有键:通知标题
KeyNotifyContent = "notify_content" // 既有键:通知内容(允许 <a> 等受控 HTML
// 需求 ④:保存策略(上传页动态读取并在范围内选择)
KeyMaxSaveSeconds = "max_save_seconds" // 既有键:最长保存秒数,0=不限制(仅受默认 7 天兜底)
KeyMaxSaveCount = "max_save_count" // v2 新增:单次分享最大可取(保存)次数上限,0=不限制
KeyMaxSaveCount = "max_save_count" // 26.9 新增:单次分享最大可取(保存)次数上限,0=不限制
KeyExpireStyle = "expireStyle" // 既有键:允许的过期方式白名单
// 需求 ④:上传频率限制(既有键,对齐参考 ip_limit["upload"]
KeyUploadCount = "uploadCount" // 窗口内允许上传次数
KeyUploadMinute = "uploadMinute" // 频率窗口(分钟)
// 需求 ④⑩:存储策略(最大文件大小/允许类型/总容量)
KeyUploadSize = "uploadSize" // 既有键:单文件上限(字节),参考实现语义
KeyMaxFileSize = "max_file_size" // v2 新增:存储策略-单文件上限(字节),0=回落 uploadSize
KeyMaxFileSize = "max_file_size" // 26.9 新增:存储策略-单文件上限(字节),0=回落 uploadSize
KeyAllowedTypes = "allowed_file_types" // 既有键:允许类型白名单("*" 不限制)
KeyStorageLimit = "storageLimit" // 既有键:站点总容量(字节),0=不限制
KeyOpenUpload = "openUpload" // 既有键:游客上传开关
// v3:存储引擎运行时可配(热切换;file_storage 为参考既有键保留兼容)
// 26.9:存储引擎运行时可配(热切换;file_storage 为参考既有键保留兼容)
KeyStorageEngine = "storage_engine" // 当前存储引擎:local|s3|webdav
KeySiteDomain = "site_domain" // 站点对外域名(空=分享链接用当前地址)
KeyUploadRate = "upload_rate" // 上传带宽字节/秒(0=不限速)
@@ -66,7 +66,7 @@ type KVSchemaEntry struct {
Description string // 中文说明
}
// KVSchema v2 全量配置键 schema 表(含既有策略键,供管理端/文档/AI 校验)。
// KVSchema 26.9 全量配置键 schema 表(含既有策略键,供管理端/文档/AI 校验)。
// 注意:Default 与 config defaults() 逐一对应(schema_test 保证)。
func KVSchema() []KVSchemaEntry {
return []KVSchemaEntry{
@@ -92,7 +92,7 @@ func KVSchema() []KVSchemaEntry {
{KeyAllowedTypes, "[]string", []string{"*"}, -1, -1, "允许上传类型白名单(\"*\" 不限制)"},
{KeyStorageLimit, "int64", int64(0), 0, -1, "站点总容量(字节),0=不限制"},
{KeyOpenUpload, "int", 1, 0, 1, "游客上传开关:1 开 / 0 需管理员登录"},
// —— v3 存储引擎(热切换;引擎参数键沿用 defaults() 既有键,管理端经 config get/update 读写)——
// —— 26.9 存储引擎(热切换;引擎参数键沿用 defaults() 既有键,管理端经 config get/update 读写)——
{KeyStorageEngine, "string", "", 0, 16, "当前存储引擎:local|s3|webdav(热切换,健康检查通过才生效;空=回落启动值 FCB_STORAGE_ENGINE"},
{KeySiteDomain, "string", "", 0, 256, "站点对外域名(http(s)://host[:port],不带路径;空=分享链接用当前访问地址)"},
{KeyUploadRate, "int64", "0", 0, 1073741824, "上传带宽字节/秒(0=不限速;范围 0~1 GiB/s"},
+4 -4
View File
@@ -35,7 +35,7 @@ func TestKVSchemaNoDuplicates(t *testing.T) {
}
}
// TestV2NewKeysPresent v2 新增键必须在 schema 与 defaults 中同时存在。
// TestV2NewKeysPresent 26.9 新增键必须在 schema 与 defaults 中同时存在。
func TestV2NewKeysPresent(t *testing.T) {
def := defaults()
newKeys := []string{
@@ -44,13 +44,13 @@ func TestV2NewKeysPresent(t *testing.T) {
}
for _, k := range newKeys {
if _, ok := def[k]; !ok {
t.Fatalf("v2 新键 %q 缺少默认值", k)
t.Fatalf("26.9 新键 %q 缺少默认值", k)
}
}
}
// TestV2AccessorDefaults v2 便捷访问器默认语义。
func TestV2AccessorDefaults(t *testing.T) {
// TestKVAccessorDefaults 26.9 便捷访问器默认语义。
func TestKVAccessorDefaults(t *testing.T) {
t.Setenv("FCB_DB_DRIVER", "sqlite")
t.Setenv("FCB_DB_DSN", "")
c, err := New()
+1 -1
View File
@@ -1,6 +1,6 @@
package middleware
// v3.2:上传/下载带宽限速(字节/秒,0=不限速)。
// 26.9:上传/下载带宽限速(字节/秒,0=不限速)。
//
// 设计要点:
// - 令牌桶(token bucket):每 Read 计算自上次起累计可消费字节,
+3 -3
View File
@@ -25,7 +25,7 @@ type FileCodes struct {
FileHash *string `gorm:"size:64" json:"file_hash"` // SHA256
IsChunked bool `gorm:"default:false" json:"is_chunked"`
UploadID *string `gorm:"size:36" json:"upload_id"` // 分片上传会话 ID
Engine string `gorm:"size:16;default:''" json:"engine"` // 归属存储引擎(v3local|s3|webdav;空=历史数据按当前引擎取)
Engine string `gorm:"size:16;default:''" json:"engine"` // 归属存储引擎(26.9local|s3|webdav;空=历史数据按当前引擎取)
}
// TableName 表名。
@@ -55,7 +55,7 @@ type UploadChunk struct {
SavePath string `gorm:"size:512" json:"save_path"`
CreatedAt time.Time `json:"created_at"`
Completed bool `gorm:"default:false" json:"completed"`
Engine string `gorm:"size:16;default:''" json:"engine"` // 归属存储引擎(v3:分片与会话记录当时引擎,合并走同一引擎)
Engine string `gorm:"size:16;default:''" json:"engine"` // 归属存储引擎(26.9:分片与会话记录当时引擎,合并走同一引擎)
}
// TableName 表名。
@@ -84,7 +84,7 @@ type PresignUploadSession struct {
ExpireStyle string `gorm:"size:20;default:day" json:"expire_style"`
CreatedAt time.Time `json:"created_at"`
ExpiresAt time.Time `json:"expires_at"`
Engine string `gorm:"size:16;default:''" json:"engine"` // 归属存储引擎(v3:直传/代理完成走同一引擎取回)
Engine string `gorm:"size:16;default:''" json:"engine"` // 归属存储引擎(26.9:直传/代理完成走同一引擎取回)
}
// TableName 表名。
+4 -4
View File
@@ -1,4 +1,4 @@
// Package settings — schema.gov2 配置键 schema 常量与元数据表。
// Package settings — schema.go26.9 配置键 schema 常量与元数据表。
//
// 键名常量的单一事实来源在 internal/config/schema.godefaults() 需引用);
// 本文件 re-export 供 API/管理层使用,并提供「键名/类型/默认值」全量表,
@@ -35,7 +35,7 @@ const (
KeyAllowedTypes = config.KeyAllowedTypes
KeyStorageLimit = config.KeyStorageLimit
KeyOpenUpload = config.KeyOpenUpload
// v3 存储引擎
// 26.9 存储引擎
KeyStorageEngine = config.KeyStorageEngine
)
@@ -52,7 +52,7 @@ const (
)
// 敏感键:不允许出现在管理端 config get 下发/前端可见集合中(双模式下一致生效)。
// v3:引擎凭据(webdav_password/s3_secret_access_key/aws_session_token)加入敏感集——
// 26.9:引擎凭据(webdav_password/s3_secret_access_key/aws_session_token)加入敏感集——
// 管理端 get 返回掩码占位,update 时空串/掩码=不修改;公开 config 永不下发。
var SensitiveKeys = []string{
"admin_token", "jwt_secret",
@@ -72,7 +72,7 @@ func IsSensitiveKey(key string) bool {
return false
}
// KVSchema 返回 v2 全量配置键元数据(键名/类型/默认值/边界/说明)。
// KVSchema 返回 26.9 全量配置键元数据(键名/类型/默认值/边界/说明)。
// 默认值必须与 config defaults() 一致(schema 同步测试保证)。
func KVSchema() []config.KVSchemaEntry { return config.KVSchema() }
+1 -1
View File
@@ -9,7 +9,7 @@ import (
// Manager 存储引擎管理器:实现 Storage 全接口并支持运行时热切换。
//
// v3 需求:管理后台可设置存储类型(local|s3|webdav)与各引擎参数,
// 26.9 需求:管理后台可设置存储类型(local|s3|webdav)与各引擎参数,
// 保存后无需重启即生效。设计要点:
// - 读写/保存类操作全部委托到"当前引擎"(原子指针,无锁热路径);
// - Switch 先构建并健康检查新引擎,成功才替换指针,失败保持原引擎;
+1 -1
View File
@@ -87,7 +87,7 @@ func TestSwitchSuccessAndCurrentName(t *testing.T) {
}
}
// TestSwitchFailureKeepsCurrent 健康检查失败时保持原引擎(v3 核心语义)。
// TestSwitchFailureKeepsCurrent 健康检查失败时保持原引擎(26.9 核心语义)。
func TestSwitchFailureKeepsCurrent(t *testing.T) {
var s3Fail atomic.Bool
s3Fail.Store(true) // s3 不健康
-1
View File
@@ -1 +0,0 @@
import{d as p,u as v,i as h,I as g,c as w,G as s,b as e,t as n,f as t,A as d,w as k,F as y,O as r,q as m,B as R,o as V}from"./index-D7AAbqvI.js";import{_ as x}from"./SiteNav.vue_vue_type_script_setup_true_lang-B5OHbKDQ.js";import{u as A}from"./auth-DjiKbqje.js";import"./admin-uFxGdgNa.js";const B={class:"admin-shell"},C={class:"admin-aside"},b={class:"aside-title"},L=["aria-label"],N={class:"admin-main"},q=p({__name:"AdminLayout",setup(S){const{t:a}=v(),u=R(),o=A(),l=h();g(async()=>{o.isAuthed&&!o.checked&&await o.verify()});async function _(){await o.logout(),l.success(a("admin.nav.loggedOut")),u.replace({name:"admin-login"})}return(F,c)=>{const i=r("RouterLink"),f=r("RouterView");return V(),w(y,null,[s(x),e("div",B,[e("aside",C,[e("div",b,n(t(a)("admin.nav.title")),1),e("nav",{class:"aside-menu","aria-label":t(a)("admin.nav.menu")},[s(i,{to:{name:"admin-files"}},{default:d(()=>[m("📁 "+n(t(a)("admin.nav.files")),1)]),_:1}),s(i,{to:{name:"admin-audit"}},{default:d(()=>[m("🛡 "+n(t(a)("admin.nav.audit")),1)]),_:1}),s(i,{to:{name:"admin-settings"}},{default:d(()=>[m("⚙️ "+n(t(a)("admin.nav.settings")),1)]),_:1}),c[0]||(c[0]=e("div",{class:"aside-sep"},null,-1)),e("a",{href:"#",onClick:k(_,["prevent"])},"🚪 "+n(t(a)("admin.nav.logout")),1)],8,L)]),e("div",N,[s(f)])])],64)}}});export{q as default};
-1
View File
@@ -1 +0,0 @@
import{d as p,u as v,i as h,I as g,c as w,G as s,b as e,t as n,f as t,A as d,w as k,F as y,O as r,q as m,B as R,o as V}from"./index-QLbKGtH7.js";import{_ as x}from"./SiteNav.vue_vue_type_script_setup_true_lang-8XmHL8P7.js";import{u as A}from"./auth-Yfi2oa1E.js";import"./admin-BZX1cFNW.js";const B={class:"admin-shell"},C={class:"admin-aside"},b={class:"aside-title"},L=["aria-label"],N={class:"admin-main"},q=p({__name:"AdminLayout",setup(S){const{t:a}=v(),u=R(),o=A(),l=h();g(async()=>{o.isAuthed&&!o.checked&&await o.verify()});async function _(){await o.logout(),l.success(a("admin.nav.loggedOut")),u.replace({name:"admin-login"})}return(F,c)=>{const i=r("RouterLink"),f=r("RouterView");return V(),w(y,null,[s(x),e("div",B,[e("aside",C,[e("div",b,n(t(a)("admin.nav.title")),1),e("nav",{class:"aside-menu","aria-label":t(a)("admin.nav.menu")},[s(i,{to:{name:"admin-files"}},{default:d(()=>[m("📁 "+n(t(a)("admin.nav.files")),1)]),_:1}),s(i,{to:{name:"admin-audit"}},{default:d(()=>[m("🛡 "+n(t(a)("admin.nav.audit")),1)]),_:1}),s(i,{to:{name:"admin-settings"}},{default:d(()=>[m("⚙️ "+n(t(a)("admin.nav.settings")),1)]),_:1}),c[0]||(c[0]=e("div",{class:"aside-sep"},null,-1)),e("a",{href:"#",onClick:k(_,["prevent"])},"🚪 "+n(t(a)("admin.nav.logout")),1)],8,L)]),e("div",N,[s(f)])])],64)}}});export{q as default};
-1
View File
@@ -1 +0,0 @@
import{d as p,u as v,i as h,I as g,c as w,G as s,b as e,t as n,f as t,A as d,w as k,F as y,O as r,q as m,B as R,o as V}from"./index-DSPQyv0Z.js";import{_ as x}from"./SiteNav.vue_vue_type_script_setup_true_lang-DSt3Q6Wq.js";import{u as A}from"./auth-o4aHzrA6.js";import"./admin-IDpKsD_2.js";const B={class:"admin-shell"},C={class:"admin-aside"},b={class:"aside-title"},L=["aria-label"],N={class:"admin-main"},q=p({__name:"AdminLayout",setup(S){const{t:a}=v(),u=R(),o=A(),l=h();g(async()=>{o.isAuthed&&!o.checked&&await o.verify()});async function _(){await o.logout(),l.success(a("admin.nav.loggedOut")),u.replace({name:"admin-login"})}return(F,c)=>{const i=r("RouterLink"),f=r("RouterView");return V(),w(y,null,[s(x),e("div",B,[e("aside",C,[e("div",b,n(t(a)("admin.nav.title")),1),e("nav",{class:"aside-menu","aria-label":t(a)("admin.nav.menu")},[s(i,{to:{name:"admin-files"}},{default:d(()=>[m("📁 "+n(t(a)("admin.nav.files")),1)]),_:1}),s(i,{to:{name:"admin-audit"}},{default:d(()=>[m("🛡 "+n(t(a)("admin.nav.audit")),1)]),_:1}),s(i,{to:{name:"admin-settings"}},{default:d(()=>[m("⚙️ "+n(t(a)("admin.nav.settings")),1)]),_:1}),c[0]||(c[0]=e("div",{class:"aside-sep"},null,-1)),e("a",{href:"#",onClick:k(_,["prevent"])},"🚪 "+n(t(a)("admin.nav.logout")),1)],8,L)]),e("div",N,[s(f)])])],64)}}});export{q as default};
-1
View File
@@ -1 +0,0 @@
import{d as p,u as v,i as h,I as g,c as w,G as s,b as e,t as n,f as t,A as d,w as k,F as y,O as r,q as m,B as R,o as V}from"./index-BKnWAKao.js";import{_ as x}from"./SiteNav.vue_vue_type_script_setup_true_lang-D3yLF4uh.js";import{u as A}from"./auth-Cp2GGuZy.js";import"./admin-KnbIpHLF.js";const B={class:"admin-shell"},C={class:"admin-aside"},b={class:"aside-title"},L=["aria-label"],N={class:"admin-main"},q=p({__name:"AdminLayout",setup(S){const{t:a}=v(),u=R(),o=A(),l=h();g(async()=>{o.isAuthed&&!o.checked&&await o.verify()});async function _(){await o.logout(),l.success(a("admin.nav.loggedOut")),u.replace({name:"admin-login"})}return(F,c)=>{const i=r("RouterLink"),f=r("RouterView");return V(),w(y,null,[s(x),e("div",B,[e("aside",C,[e("div",b,n(t(a)("admin.nav.title")),1),e("nav",{class:"aside-menu","aria-label":t(a)("admin.nav.menu")},[s(i,{to:{name:"admin-files"}},{default:d(()=>[m("📁 "+n(t(a)("admin.nav.files")),1)]),_:1}),s(i,{to:{name:"admin-audit"}},{default:d(()=>[m("🛡 "+n(t(a)("admin.nav.audit")),1)]),_:1}),s(i,{to:{name:"admin-settings"}},{default:d(()=>[m("⚙️ "+n(t(a)("admin.nav.settings")),1)]),_:1}),c[0]||(c[0]=e("div",{class:"aside-sep"},null,-1)),e("a",{href:"#",onClick:k(_,["prevent"])},"🚪 "+n(t(a)("admin.nav.logout")),1)],8,L)]),e("div",N,[s(f)])])],64)}}});export{q as default};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
.field-sub[data-v-d049fcf9]{display:block;font-size:13px;font-weight:600;color:var(--c-text-2);margin-bottom:6px}.result-head[data-v-8bebf3d9]{display:flex;align-items:center;gap:10px;margin-bottom:14px}.result-name[data-v-8bebf3d9]{font-weight:600;word-break:break-all}.link-row[data-v-8bebf3d9]{display:flex;gap:8px}.link-row .input[data-v-8bebf3d9]{flex:1}.result-meta[data-v-8bebf3d9]{display:flex;align-items:center;gap:6px;color:var(--c-text-2);font-size:13px;flex-wrap:wrap}.code-copy[data-v-8bebf3d9]{display:block;width:100%;cursor:pointer;appearance:none;-webkit-appearance:none;transition:filter .15s}.code-copy[data-v-8bebf3d9]:hover{filter:brightness(.96)}.code-copy[data-v-8bebf3d9]:active{filter:brightness(.92)}.dropzone.disabled[data-v-1b57fe5c]{opacity:.55;cursor:not-allowed}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
import{d as w,u as b,a as y,z as x,A as k,b as e,f as t,t as a,w as V,C as B,D as C,c as m,g as _,q as N,j as r,N as S,B as q,o as d,H as L,_ as P}from"./index-DSPQyv0Z.js";import{P as A}from"./PageShell-Bv1Rpp4p.js";import{u as D}from"./auth-o4aHzrA6.js";import"./SiteNav.vue_vue_type_script_setup_true_lang-DSt3Q6Wq.js";import"./admin-IDpKsD_2.js";const E={class:"card",style:{"max-width":"380px",margin:"8vh auto 0"}},I={class:"login-head"},M=["src"],R={class:"card-title"},T={class:"card-sub"},U={class:"field"},j={for:"admin-password"},z=["placeholder"],H={key:0,class:"hint",style:{color:"var(--c-danger)","margin-bottom":"12px"}},F=["disabled"],G={key:0,class:"spin","aria-hidden":"true"},J={class:"hint",style:{"margin-top":"16px"}},K=w({__name:"LoginView",setup(O){const{t:s}=b(),c=S(),g=q(),h=D(),u=y(),i=r(""),l=r(!1),n=r("");async function f(){if(!i.value){n.value=s("admin.login.required");return}l.value=!0,n.value="";try{await h.login(i.value);const o=typeof c.query.redirect=="string"?c.query.redirect:"/admin/files";g.replace(o)}catch(o){n.value=o instanceof L?o.code===401?s("admin.login.wrongPassword"):o.msg:s("admin.login.failed"),i.value=""}finally{l.value=!1}}return(o,p)=>(d(),x(A,null,{default:k(()=>[e("section",E,[e("div",I,[e("img",{src:t(u).displayLogoUrl,alt:"Logo",class:"login-logo"},null,8,M),e("h1",R,a(t(s)("admin.login.title")),1),e("p",T,a(t(s)("admin.login.subtitle",{name:t(u).displayName})),1)]),e("form",{onSubmit:V(f,["prevent"])},[e("div",U,[e("label",j,a(t(s)("admin.login.password")),1),B(e("input",{id:"admin-password","onUpdate:modelValue":p[0]||(p[0]=v=>i.value=v),class:"input",type:"password",placeholder:t(s)("admin.login.passwordPlaceholder"),autocomplete:"current-password",autofocus:""},null,8,z),[[C,i.value]])]),n.value?(d(),m("p",H,a(n.value),1)):_("",!0),e("button",{class:"btn btn-block",type:"submit",disabled:l.value},[l.value?(d(),m("span",G)):_("",!0),N(" "+a(t(s)("admin.login.submit")),1)],8,F)],32),e("p",J,a(t(s)("admin.login.hint")),1)])]),_:1}))}}),$=P(K,[["__scopeId","data-v-26e7f2a4"]]);export{$ as default};
-1
View File
@@ -1 +0,0 @@
import{d as w,u as b,a as y,z as x,A as k,b as e,f as t,t as a,w as V,C as B,D as C,c as m,g as _,q as N,j as r,N as S,B as q,o as d,H as L,_ as P}from"./index-QLbKGtH7.js";import{P as A}from"./PageShell-D1DY7qw8.js";import{u as D}from"./auth-Yfi2oa1E.js";import"./SiteNav.vue_vue_type_script_setup_true_lang-8XmHL8P7.js";import"./admin-BZX1cFNW.js";const E={class:"card",style:{"max-width":"380px",margin:"8vh auto 0"}},I={class:"login-head"},M=["src"],R={class:"card-title"},T={class:"card-sub"},U={class:"field"},j={for:"admin-password"},z=["placeholder"],H={key:0,class:"hint",style:{color:"var(--c-danger)","margin-bottom":"12px"}},F=["disabled"],G={key:0,class:"spin","aria-hidden":"true"},J={class:"hint",style:{"margin-top":"16px"}},K=w({__name:"LoginView",setup(O){const{t:s}=b(),c=S(),g=q(),h=D(),u=y(),i=r(""),l=r(!1),n=r("");async function f(){if(!i.value){n.value=s("admin.login.required");return}l.value=!0,n.value="";try{await h.login(i.value);const o=typeof c.query.redirect=="string"?c.query.redirect:"/admin/files";g.replace(o)}catch(o){n.value=o instanceof L?o.code===401?s("admin.login.wrongPassword"):o.msg:s("admin.login.failed"),i.value=""}finally{l.value=!1}}return(o,p)=>(d(),x(A,null,{default:k(()=>[e("section",E,[e("div",I,[e("img",{src:t(u).displayLogoUrl,alt:"Logo",class:"login-logo"},null,8,M),e("h1",R,a(t(s)("admin.login.title")),1),e("p",T,a(t(s)("admin.login.subtitle",{name:t(u).displayName})),1)]),e("form",{onSubmit:V(f,["prevent"])},[e("div",U,[e("label",j,a(t(s)("admin.login.password")),1),B(e("input",{id:"admin-password","onUpdate:modelValue":p[0]||(p[0]=v=>i.value=v),class:"input",type:"password",placeholder:t(s)("admin.login.passwordPlaceholder"),autocomplete:"current-password",autofocus:""},null,8,z),[[C,i.value]])]),n.value?(d(),m("p",H,a(n.value),1)):_("",!0),e("button",{class:"btn btn-block",type:"submit",disabled:l.value},[l.value?(d(),m("span",G)):_("",!0),N(" "+a(t(s)("admin.login.submit")),1)],8,F)],32),e("p",J,a(t(s)("admin.login.hint")),1)])]),_:1}))}}),$=P(K,[["__scopeId","data-v-26e7f2a4"]]);export{$ as default};
-1
View File
@@ -1 +0,0 @@
import{d as w,u as b,a as y,z as x,A as k,b as e,f as t,t as a,w as V,C as B,D as C,c as m,g as _,q as N,j as r,N as S,B as q,o as d,H as L,_ as P}from"./index-D7AAbqvI.js";import{P as A}from"./PageShell-D3dyUalL.js";import{u as D}from"./auth-DjiKbqje.js";import"./SiteNav.vue_vue_type_script_setup_true_lang-B5OHbKDQ.js";import"./admin-uFxGdgNa.js";const E={class:"card",style:{"max-width":"380px",margin:"8vh auto 0"}},I={class:"login-head"},M=["src"],R={class:"card-title"},T={class:"card-sub"},U={class:"field"},j={for:"admin-password"},z=["placeholder"],H={key:0,class:"hint",style:{color:"var(--c-danger)","margin-bottom":"12px"}},F=["disabled"],G={key:0,class:"spin","aria-hidden":"true"},J={class:"hint",style:{"margin-top":"16px"}},K=w({__name:"LoginView",setup(O){const{t:s}=b(),c=S(),g=q(),h=D(),u=y(),i=r(""),l=r(!1),n=r("");async function f(){if(!i.value){n.value=s("admin.login.required");return}l.value=!0,n.value="";try{await h.login(i.value);const o=typeof c.query.redirect=="string"?c.query.redirect:"/admin/files";g.replace(o)}catch(o){n.value=o instanceof L?o.code===401?s("admin.login.wrongPassword"):o.msg:s("admin.login.failed"),i.value=""}finally{l.value=!1}}return(o,p)=>(d(),x(A,null,{default:k(()=>[e("section",E,[e("div",I,[e("img",{src:t(u).displayLogoUrl,alt:"Logo",class:"login-logo"},null,8,M),e("h1",R,a(t(s)("admin.login.title")),1),e("p",T,a(t(s)("admin.login.subtitle",{name:t(u).displayName})),1)]),e("form",{onSubmit:V(f,["prevent"])},[e("div",U,[e("label",j,a(t(s)("admin.login.password")),1),B(e("input",{id:"admin-password","onUpdate:modelValue":p[0]||(p[0]=v=>i.value=v),class:"input",type:"password",placeholder:t(s)("admin.login.passwordPlaceholder"),autocomplete:"current-password",autofocus:""},null,8,z),[[C,i.value]])]),n.value?(d(),m("p",H,a(n.value),1)):_("",!0),e("button",{class:"btn btn-block",type:"submit",disabled:l.value},[l.value?(d(),m("span",G)):_("",!0),N(" "+a(t(s)("admin.login.submit")),1)],8,F)],32),e("p",J,a(t(s)("admin.login.hint")),1)])]),_:1}))}}),$=P(K,[["__scopeId","data-v-26e7f2a4"]]);export{$ as default};
-1
View File
@@ -1 +0,0 @@
import{d as w,u as b,a as y,z as x,A as k,b as e,f as t,t as a,w as V,C as B,D as C,c as m,g as _,q as N,j as r,N as S,B as q,o as d,H as L,_ as P}from"./index-BKnWAKao.js";import{P as A}from"./PageShell-CBo29Oot.js";import{u as D}from"./auth-Cp2GGuZy.js";import"./SiteNav.vue_vue_type_script_setup_true_lang-D3yLF4uh.js";import"./admin-KnbIpHLF.js";const E={class:"card",style:{"max-width":"380px",margin:"8vh auto 0"}},I={class:"login-head"},M=["src"],R={class:"card-title"},T={class:"card-sub"},U={class:"field"},j={for:"admin-password"},z=["placeholder"],H={key:0,class:"hint",style:{color:"var(--c-danger)","margin-bottom":"12px"}},F=["disabled"],G={key:0,class:"spin","aria-hidden":"true"},J={class:"hint",style:{"margin-top":"16px"}},K=w({__name:"LoginView",setup(O){const{t:s}=b(),c=S(),g=q(),h=D(),u=y(),i=r(""),l=r(!1),n=r("");async function f(){if(!i.value){n.value=s("admin.login.required");return}l.value=!0,n.value="";try{await h.login(i.value);const o=typeof c.query.redirect=="string"?c.query.redirect:"/admin/files";g.replace(o)}catch(o){n.value=o instanceof L?o.code===401?s("admin.login.wrongPassword"):o.msg:s("admin.login.failed"),i.value=""}finally{l.value=!1}}return(o,p)=>(d(),x(A,null,{default:k(()=>[e("section",E,[e("div",I,[e("img",{src:t(u).displayLogoUrl,alt:"Logo",class:"login-logo"},null,8,M),e("h1",R,a(t(s)("admin.login.title")),1),e("p",T,a(t(s)("admin.login.subtitle",{name:t(u).displayName})),1)]),e("form",{onSubmit:V(f,["prevent"])},[e("div",U,[e("label",j,a(t(s)("admin.login.password")),1),B(e("input",{id:"admin-password","onUpdate:modelValue":p[0]||(p[0]=v=>i.value=v),class:"input",type:"password",placeholder:t(s)("admin.login.passwordPlaceholder"),autocomplete:"current-password",autofocus:""},null,8,z),[[C,i.value]])]),n.value?(d(),m("p",H,a(n.value),1)):_("",!0),e("button",{class:"btn btn-block",type:"submit",disabled:l.value},[l.value?(d(),m("span",G)):_("",!0),N(" "+a(t(s)("admin.login.submit")),1)],8,F)],32),e("p",J,a(t(s)("admin.login.hint")),1)])]),_:1}))}}),$=P(K,[["__scopeId","data-v-26e7f2a4"]]);export{$ as default};
-1
View File
@@ -1 +0,0 @@
import{d as c,u as i,z as d,A as a,b as t,t as e,f as s,G as l,q as p,O as u,o as _}from"./index-D7AAbqvI.js";import{P as m}from"./PageShell-D3dyUalL.js";import"./SiteNav.vue_vue_type_script_setup_true_lang-B5OHbKDQ.js";const f={class:"card empty",style:{"max-width":"480px",margin:"10vh auto 0"}},h={style:{"font-weight":"600",color:"var(--c-text)"}},x={class:"hint"},F=c({__name:"NotFoundView",setup(y){const{t:o}=i();return(g,n)=>{const r=u("RouterLink");return _(),d(m,null,{default:a(()=>[t("div",f,[n[0]||(n[0]=t("div",{class:"empty-icon"},"🧭",-1)),t("p",h,e(s(o)("notFound.title")),1),t("p",x,e(s(o)("notFound.desc")),1),l(r,{class:"btn",to:"/",style:{"margin-top":"10px"}},{default:a(()=>[p(e(s(o)("notFound.back")),1)]),_:1})])]),_:1})}}});export{F as default};
-1
View File
@@ -1 +0,0 @@
import{d as c,u as i,z as d,A as a,b as t,t as e,f as s,G as l,q as p,O as u,o as _}from"./index-QLbKGtH7.js";import{P as m}from"./PageShell-D1DY7qw8.js";import"./SiteNav.vue_vue_type_script_setup_true_lang-8XmHL8P7.js";const f={class:"card empty",style:{"max-width":"480px",margin:"10vh auto 0"}},h={style:{"font-weight":"600",color:"var(--c-text)"}},x={class:"hint"},F=c({__name:"NotFoundView",setup(y){const{t:o}=i();return(g,n)=>{const r=u("RouterLink");return _(),d(m,null,{default:a(()=>[t("div",f,[n[0]||(n[0]=t("div",{class:"empty-icon"},"🧭",-1)),t("p",h,e(s(o)("notFound.title")),1),t("p",x,e(s(o)("notFound.desc")),1),l(r,{class:"btn",to:"/",style:{"margin-top":"10px"}},{default:a(()=>[p(e(s(o)("notFound.back")),1)]),_:1})])]),_:1})}}});export{F as default};
-1
View File
@@ -1 +0,0 @@
import{d as c,u as i,z as d,A as a,b as t,t as e,f as s,G as l,q as p,O as u,o as _}from"./index-DSPQyv0Z.js";import{P as m}from"./PageShell-Bv1Rpp4p.js";import"./SiteNav.vue_vue_type_script_setup_true_lang-DSt3Q6Wq.js";const f={class:"card empty",style:{"max-width":"480px",margin:"10vh auto 0"}},h={style:{"font-weight":"600",color:"var(--c-text)"}},x={class:"hint"},F=c({__name:"NotFoundView",setup(y){const{t:o}=i();return(g,n)=>{const r=u("RouterLink");return _(),d(m,null,{default:a(()=>[t("div",f,[n[0]||(n[0]=t("div",{class:"empty-icon"},"🧭",-1)),t("p",h,e(s(o)("notFound.title")),1),t("p",x,e(s(o)("notFound.desc")),1),l(r,{class:"btn",to:"/",style:{"margin-top":"10px"}},{default:a(()=>[p(e(s(o)("notFound.back")),1)]),_:1})])]),_:1})}}});export{F as default};
-1
View File
@@ -1 +0,0 @@
import{d as c,u as i,z as d,A as a,b as t,t as e,f as s,G as l,q as p,O as u,o as _}from"./index-BKnWAKao.js";import{P as m}from"./PageShell-CBo29Oot.js";import"./SiteNav.vue_vue_type_script_setup_true_lang-D3yLF4uh.js";const f={class:"card empty",style:{"max-width":"480px",margin:"10vh auto 0"}},h={style:{"font-weight":"600",color:"var(--c-text)"}},x={class:"hint"},F=c({__name:"NotFoundView",setup(y){const{t:o}=i();return(g,n)=>{const r=u("RouterLink");return _(),d(m,null,{default:a(()=>[t("div",f,[n[0]||(n[0]=t("div",{class:"empty-icon"},"🧭",-1)),t("p",h,e(s(o)("notFound.title")),1),t("p",x,e(s(o)("notFound.desc")),1),l(r,{class:"btn",to:"/",style:{"margin-top":"10px"}},{default:a(()=>[p(e(s(o)("notFound.back")),1)]),_:1})])]),_:1})}}});export{F as default};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
import{d as h,u as g,a as k,c as n,G as i,b as t,Q as y,e as v,f as e,t as a,g as c,q as r,A as d,F as x,h as B,O as C,o as l,_ as b}from"./index-DSPQyv0Z.js";import{_ as w}from"./SiteNav.vue_vue_type_script_setup_true_lang-DSt3Q6Wq.js";const N={class:"site-footer"},S={class:"footer-left"},F={key:0,class:"footer-text"},P={key:1,class:"footer-beian"},T={key:2},V=["aria-label"],$=h({__name:"PageShell",setup(D){const{t:s}=g(),o=k(),m=new Date().getFullYear(),u=B(()=>!!(o.footerText.trim()||o.footerBeian.trim()));return(p,_)=>{const f=C("RouterLink");return l(),n(x,null,[i(w),t("main",{class:v(["page",{"page-wide":p.$route.meta.wide}])},[y(p.$slots,"default",{},void 0,!0)],2),t("footer",N,[t("div",S,[e(o).footerText.trim()?(l(),n("span",F,a(e(o).footerText),1)):c("",!0),e(o).footerBeian.trim()?(l(),n("span",P,a(e(o).footerBeian),1)):c("",!0),u.value?c("",!0):(l(),n("span",T,a(e(s)("footer.copyright",{year:e(m),name:e(o).displayName})),1))]),_[0]||(_[0]=t("span",{class:"footer-powered"},[r(" Powered by "),t("a",{href:"https://skymirror.top",target:"_blank",rel:"noopener noreferrer"},"SKYMirror"),r(" 26.9 ")],-1)),t("nav",{"aria-label":e(s)("footer.linkNav")},[i(f,{to:"/docs"},{default:d(()=>[r(a(e(s)("footer.docs")),1)]),_:1}),i(f,{to:"/openapi"},{default:d(()=>[r(a(e(s)("footer.openapi")),1)]),_:1}),i(f,{to:"/admin/files"},{default:d(()=>[r(a(e(s)("footer.admin")),1)]),_:1})],8,V)])],64)}}}),R=b($,[["__scopeId","data-v-80685d6f"]]);export{R as P};
-1
View File
@@ -1 +0,0 @@
import{d as h,u as g,a as k,c as n,G as i,b as t,Q as y,e as v,f as e,t as a,g as c,q as r,A as d,F as x,h as B,O as C,o as l,_ as b}from"./index-BKnWAKao.js";import{_ as w}from"./SiteNav.vue_vue_type_script_setup_true_lang-D3yLF4uh.js";const N={class:"site-footer"},S={class:"footer-left"},F={key:0,class:"footer-text"},P={key:1,class:"footer-beian"},T={key:2},V=["aria-label"],$=h({__name:"PageShell",setup(D){const{t:s}=g(),o=k(),m=new Date().getFullYear(),u=B(()=>!!(o.footerText.trim()||o.footerBeian.trim()));return(p,_)=>{const f=C("RouterLink");return l(),n(x,null,[i(w),t("main",{class:v(["page",{"page-wide":p.$route.meta.wide}])},[y(p.$slots,"default",{},void 0,!0)],2),t("footer",N,[t("div",S,[e(o).footerText.trim()?(l(),n("span",F,a(e(o).footerText),1)):c("",!0),e(o).footerBeian.trim()?(l(),n("span",P,a(e(o).footerBeian),1)):c("",!0),u.value?c("",!0):(l(),n("span",T,a(e(s)("footer.copyright",{year:e(m),name:e(o).displayName})),1))]),_[0]||(_[0]=t("span",{class:"footer-powered"},[r(" Powered by "),t("a",{href:"https://skymirror.top",target:"_blank",rel:"noopener noreferrer"},"SKYMirror"),r(" 26.9 ")],-1)),t("nav",{"aria-label":e(s)("footer.linkNav")},[i(f,{to:"/docs"},{default:d(()=>[r(a(e(s)("footer.docs")),1)]),_:1}),i(f,{to:"/openapi"},{default:d(()=>[r(a(e(s)("footer.openapi")),1)]),_:1}),i(f,{to:"/admin/files"},{default:d(()=>[r(a(e(s)("footer.admin")),1)]),_:1})],8,V)])],64)}}}),R=b($,[["__scopeId","data-v-80685d6f"]]);export{R as P};
-1
View File
@@ -1 +0,0 @@
import{d as h,u as g,a as k,c as n,G as i,b as t,Q as y,e as v,f as e,t as a,g as c,q as r,A as d,F as x,h as B,O as C,o as l,_ as b}from"./index-QLbKGtH7.js";import{_ as w}from"./SiteNav.vue_vue_type_script_setup_true_lang-8XmHL8P7.js";const N={class:"site-footer"},S={class:"footer-left"},F={key:0,class:"footer-text"},P={key:1,class:"footer-beian"},T={key:2},V=["aria-label"],$=h({__name:"PageShell",setup(D){const{t:s}=g(),o=k(),m=new Date().getFullYear(),u=B(()=>!!(o.footerText.trim()||o.footerBeian.trim()));return(p,_)=>{const f=C("RouterLink");return l(),n(x,null,[i(w),t("main",{class:v(["page",{"page-wide":p.$route.meta.wide}])},[y(p.$slots,"default",{},void 0,!0)],2),t("footer",N,[t("div",S,[e(o).footerText.trim()?(l(),n("span",F,a(e(o).footerText),1)):c("",!0),e(o).footerBeian.trim()?(l(),n("span",P,a(e(o).footerBeian),1)):c("",!0),u.value?c("",!0):(l(),n("span",T,a(e(s)("footer.copyright",{year:e(m),name:e(o).displayName})),1))]),_[0]||(_[0]=t("span",{class:"footer-powered"},[r(" Powered by "),t("a",{href:"https://skymirror.top",target:"_blank",rel:"noopener noreferrer"},"SKYMirror"),r(" 26.9 ")],-1)),t("nav",{"aria-label":e(s)("footer.linkNav")},[i(f,{to:"/docs"},{default:d(()=>[r(a(e(s)("footer.docs")),1)]),_:1}),i(f,{to:"/openapi"},{default:d(()=>[r(a(e(s)("footer.openapi")),1)]),_:1}),i(f,{to:"/admin/files"},{default:d(()=>[r(a(e(s)("footer.admin")),1)]),_:1})],8,V)])],64)}}}),R=b($,[["__scopeId","data-v-80685d6f"]]);export{R as P};
-1
View File
@@ -1 +0,0 @@
import{d as h,u as g,a as k,c as n,G as i,b as t,Q as y,e as v,f as e,t as a,g as c,q as r,A as d,F as x,h as B,O as C,o as l,_ as b}from"./index-D7AAbqvI.js";import{_ as w}from"./SiteNav.vue_vue_type_script_setup_true_lang-B5OHbKDQ.js";const N={class:"site-footer"},S={class:"footer-left"},F={key:0,class:"footer-text"},P={key:1,class:"footer-beian"},T={key:2},V=["aria-label"],$=h({__name:"PageShell",setup(D){const{t:s}=g(),o=k(),m=new Date().getFullYear(),u=B(()=>!!(o.footerText.trim()||o.footerBeian.trim()));return(p,_)=>{const f=C("RouterLink");return l(),n(x,null,[i(w),t("main",{class:v(["page",{"page-wide":p.$route.meta.wide}])},[y(p.$slots,"default",{},void 0,!0)],2),t("footer",N,[t("div",S,[e(o).footerText.trim()?(l(),n("span",F,a(e(o).footerText),1)):c("",!0),e(o).footerBeian.trim()?(l(),n("span",P,a(e(o).footerBeian),1)):c("",!0),u.value?c("",!0):(l(),n("span",T,a(e(s)("footer.copyright",{year:e(m),name:e(o).displayName})),1))]),_[0]||(_[0]=t("span",{class:"footer-powered"},[r(" Powered by "),t("a",{href:"https://skymirror.top",target:"_blank",rel:"noopener noreferrer"},"SKYMirror"),r(" 26.9 ")],-1)),t("nav",{"aria-label":e(s)("footer.linkNav")},[i(f,{to:"/docs"},{default:d(()=>[r(a(e(s)("footer.docs")),1)]),_:1}),i(f,{to:"/openapi"},{default:d(()=>[r(a(e(s)("footer.openapi")),1)]),_:1}),i(f,{to:"/admin/files"},{default:d(()=>[r(a(e(s)("footer.admin")),1)]),_:1})],8,V)])],64)}}}),R=b($,[["__scopeId","data-v-80685d6f"]]);export{R as P};
@@ -1 +0,0 @@
import{d as b,u as p,c as r,b as o,t as i,f as c,h,o as f}from"./index-DSPQyv0Z.js";const v={class:"pager"},x={class:"pager-info"},k=["disabled"],M=["disabled"],y=b({__name:"Pager",props:{page:{},size:{},total:{}},emits:["change"],setup(t,{emit:m}){const a=t,d=m,{t:n}=p(),s=h(()=>Math.max(1,Math.ceil(a.total/a.size)));function g(l){const e=Math.min(Math.max(1,l),s.value);e!==a.page&&d("change",e,a.size)}return(l,e)=>(f(),r("div",v,[o("span",x,i(c(n)("common.pagerInfo",{total:t.total,page:t.page,pages:s.value})),1),o("button",{class:"btn btn-ghost btn-sm",type:"button",disabled:t.page<=1,onClick:e[0]||(e[0]=u=>g(t.page-1))},i(c(n)("common.previousPage")),9,k),o("button",{class:"btn btn-ghost btn-sm",type:"button",disabled:t.page>=s.value,onClick:e[1]||(e[1]=u=>g(t.page+1))},i(c(n)("common.nextPage")),9,M)]))}});export{y as _};
@@ -1 +0,0 @@
import{d as b,u as p,c as r,b as o,t as i,f as c,h,o as f}from"./index-BKnWAKao.js";const v={class:"pager"},x={class:"pager-info"},k=["disabled"],M=["disabled"],y=b({__name:"Pager",props:{page:{},size:{},total:{}},emits:["change"],setup(t,{emit:m}){const a=t,d=m,{t:n}=p(),s=h(()=>Math.max(1,Math.ceil(a.total/a.size)));function g(l){const e=Math.min(Math.max(1,l),s.value);e!==a.page&&d("change",e,a.size)}return(l,e)=>(f(),r("div",v,[o("span",x,i(c(n)("common.pagerInfo",{total:t.total,page:t.page,pages:s.value})),1),o("button",{class:"btn btn-ghost btn-sm",type:"button",disabled:t.page<=1,onClick:e[0]||(e[0]=u=>g(t.page-1))},i(c(n)("common.previousPage")),9,k),o("button",{class:"btn btn-ghost btn-sm",type:"button",disabled:t.page>=s.value,onClick:e[1]||(e[1]=u=>g(t.page+1))},i(c(n)("common.nextPage")),9,M)]))}});export{y as _};
@@ -1 +0,0 @@
import{d as b,u as p,c as r,b as o,t as i,f as c,h,o as f}from"./index-D7AAbqvI.js";const v={class:"pager"},x={class:"pager-info"},k=["disabled"],M=["disabled"],y=b({__name:"Pager",props:{page:{},size:{},total:{}},emits:["change"],setup(t,{emit:m}){const a=t,d=m,{t:n}=p(),s=h(()=>Math.max(1,Math.ceil(a.total/a.size)));function g(l){const e=Math.min(Math.max(1,l),s.value);e!==a.page&&d("change",e,a.size)}return(l,e)=>(f(),r("div",v,[o("span",x,i(c(n)("common.pagerInfo",{total:t.total,page:t.page,pages:s.value})),1),o("button",{class:"btn btn-ghost btn-sm",type:"button",disabled:t.page<=1,onClick:e[0]||(e[0]=u=>g(t.page-1))},i(c(n)("common.previousPage")),9,k),o("button",{class:"btn btn-ghost btn-sm",type:"button",disabled:t.page>=s.value,onClick:e[1]||(e[1]=u=>g(t.page+1))},i(c(n)("common.nextPage")),9,M)]))}});export{y as _};
@@ -1 +0,0 @@
import{d as b,u as p,c as r,b as o,t as i,f as c,h,o as f}from"./index-QLbKGtH7.js";const v={class:"pager"},x={class:"pager-info"},k=["disabled"],M=["disabled"],y=b({__name:"Pager",props:{page:{},size:{},total:{}},emits:["change"],setup(t,{emit:m}){const a=t,d=m,{t:n}=p(),s=h(()=>Math.max(1,Math.ceil(a.total/a.size)));function g(l){const e=Math.min(Math.max(1,l),s.value);e!==a.page&&d("change",e,a.size)}return(l,e)=>(f(),r("div",v,[o("span",x,i(c(n)("common.pagerInfo",{total:t.total,page:t.page,pages:s.value})),1),o("button",{class:"btn btn-ghost btn-sm",type:"button",disabled:t.page<=1,onClick:e[0]||(e[0]=u=>g(t.page-1))},i(c(n)("common.previousPage")),9,k),o("button",{class:"btn btn-ghost btn-sm",type:"button",disabled:t.page>=s.value,onClick:e[1]||(e[1]=u=>g(t.page+1))},i(c(n)("common.nextPage")),9,M)]))}});export{y as _};
-1
View File
@@ -1 +0,0 @@
import{d as N,u as U,i as q,I,J as b,z as L,A as R,H as C,h as T,j as p,b as t,c as u,q as z,t as s,f as n,w as E,C as H,D as $,F as y,s as _,g as w,K as j,L as J,n as K,M as B,k as W,N as G,B as O,o as l}from"./index-DSPQyv0Z.js";import{P as Q}from"./PageShell-Bv1Rpp4p.js";import{g as X,p as Y,b as Z}from"./share-DQTp5ax3.js";import"./SiteNav.vue_vue_type_script_setup_true_lang-DSt3Q6Wq.js";const ee={class:"card",style:{"max-width":"640px",margin:"12px auto 0"}},te={key:0,class:"loading-block"},ae={class:"empty"},oe={style:{"font-weight":"600",color:"var(--c-text)"}},ne={class:"hint"},se=["placeholder"],ie={class:"btn",type:"submit"},le={style:{display:"flex","align-items":"center",gap:"10px","flex-wrap":"wrap","margin-bottom":"4px"}},ue={class:"badge"},re={style:{"font-size":"16px","word-break":"break-all"}},ce={key:0,class:"hint",style:{margin:"0"}},pe={class:"hint",style:{"margin-bottom":"18px"}},de={key:0,class:"loading-block"},me={class:"text-view"},ve={style:{display:"flex",gap:"10px","margin-top":"14px","flex-wrap":"wrap"}},ye={class:"file-summary"},ke={style:{"font-weight":"600","word-break":"break-all"}},fe={class:"hint",style:{margin:"0"}},ge={key:0,style:{margin:"16px 0 6px"}},he={class:"progress"},xe={class:"hint"},_e=["disabled"],Be=N({__name:"PickupView",setup(we){const{t:e}=U(),D=G(),F=O(),k=q(),d=T(()=>String(D.params.code??"").trim().split(/\s+/)[0]),r=p("loading"),m=p(""),o=p(null),v=p(""),f=p(!1),c=p(null);async function g(){if(!d.value){r.value="error",m.value=e("pickup.emptyCode");return}r.value="loading",m.value="",o.value=null,v.value="";try{const a=await X(d.value);if(o.value=a,r.value="ready",a.isText){f.value=!0;try{v.value=await Y(d.value)}finally{f.value=!1}}}catch(a){r.value="error",m.value=a instanceof C?a.code===404?e("pickup.notFound"):a.code===423?e("home.rateLimited"):a.code===428?e("home.notInitialized"):a.msg:e("pickup.failedDefault")}}I(g),b(d,()=>{g()}),b(()=>e("pickup.emptyCode"),()=>{r.value==="error"&&m.value&&g()});async function A(){if(o.value){c.value=0;try{const{blob:a,filename:i}=await Z(d.value,x=>c.value=x);B(a,i||o.value.name||"download"),k.success(e("pickup.downloaded"))}catch(a){k.error(a instanceof C?a.msg:e("pickup.downloadFailed"))}finally{c.value=null}}}async function M(){await W(v.value)?k.success(e("pickup.copied")):k.error(e("common.copyFailed"))}function P(){if(!o.value)return;const a=new Blob([v.value],{type:"text/plain;charset=utf-8"}),i=o.value.name?.includes(".")?o.value.name:`${o.value.name||"text"}.txt`;B(a,i)}const h=p("");function S(){const a=h.value.trim();a&&F.push({name:"pickup",params:{code:a}})}const V=T(()=>{const a=o.value;return a?a.remainingDownloads===null||a.remainingDownloads===void 0||a.remainingDownloads<0?e("pickup.remainingUnlimited"):e("pickup.remainingCount",{n:a.remainingDownloads}):""});return(a,i)=>(l(),L(Q,null,{default:R(()=>[t("section",ee,[r.value==="loading"?(l(),u("div",te,[i[1]||(i[1]=t("span",{class:"spin","aria-hidden":"true"},null,-1)),z(" "+s(n(e)("pickup.querying",{code:d.value})),1)])):r.value==="error"?(l(),u(y,{key:1},[t("div",ae,[i[2]||(i[2]=t("div",{class:"empty-icon"},"📮",-1)),t("p",oe,s(m.value||n(e)("pickup.failed")),1),t("p",ne,s(n(e)("pickup.confirmHint")),1)]),t("form",{class:"quick-pickup",style:{"margin-top":"6px","max-width":"none"},onSubmit:E(S,["prevent"])},[H(t("input",{"onUpdate:modelValue":i[0]||(i[0]=x=>h.value=x),class:"input input-mono",placeholder:n(e)("pickup.retryPlaceholder"),maxlength:"32"},null,8,se),[[$,h.value]]),t("button",ie,s(n(e)("pickup.retryButton")),1)],32)],64)):o.value?(l(),u(y,{key:2},[t("div",le,[t("span",ue,s(o.value.isText?n(e)("common.text"):n(e)("common.file")),1),t("strong",re,s(o.value.name),1),o.value.isText?w("",!0):(l(),u("span",ce,s(n(_)(o.value.size)),1))]),t("p",pe,s(V.value)+" · "+s(n(e)("pickup.expireAt",{time:o.value.expiredAt?n(j)(o.value.expiredAt):n(e)("time.permanent")}))+" · "+s(n(J)(o.value.expiredAt)),1),o.value.isText?(l(),u(y,{key:0},[f.value?(l(),u("div",de,[i[3]||(i[3]=t("span",{class:"spin","aria-hidden":"true"},null,-1)),z(" "+s(n(e)("pickup.loadingText")),1)])):(l(),u(y,{key:1},[t("pre",me,s(v.value),1),t("div",ve,[t("button",{class:"btn",type:"button",onClick:M},s(n(e)("pickup.copyContent")),1),t("button",{class:"btn btn-ghost",type:"button",onClick:P},s(n(e)("pickup.downloadTxt")),1)])],64))],64)):(l(),u(y,{key:1},[t("div",ye,[i[4]||(i[4]=t("span",{style:{"font-size":"30px"},"aria-hidden":"true"},"📄",-1)),t("div",null,[t("div",ke,s(o.value.name),1),t("div",fe,s(n(e)("pickup.sizeUsed",{size:n(_)(o.value.size),n:o.value.usedCount})),1)])]),c.value!==null?(l(),u("div",ge,[t("div",he,[t("i",{style:K({width:`${c.value}%`})},null,4)]),t("p",xe,s(n(e)("pickup.downloading",{percent:c.value})),1)])):w("",!0),t("button",{class:"btn btn-block",type:"button",disabled:c.value!==null,onClick:A}," ⬇ "+s(n(e)("pickup.downloadFile",{size:n(_)(o.value.size)})),9,_e)],64))],64)):w("",!0)])]),_:1}))}});export{Be as default};
-1
View File
@@ -1 +0,0 @@
import{d as N,u as U,i as q,I,J as b,z as L,A as R,H as C,h as T,j as p,b as t,c as u,q as z,t as s,f as n,w as E,C as H,D as $,F as y,s as _,g as w,K as j,L as J,n as K,M as B,k as W,N as G,B as O,o as l}from"./index-BKnWAKao.js";import{P as Q}from"./PageShell-CBo29Oot.js";import{g as X,p as Y,b as Z}from"./share-B-zR67vw.js";import"./SiteNav.vue_vue_type_script_setup_true_lang-D3yLF4uh.js";const ee={class:"card",style:{"max-width":"640px",margin:"12px auto 0"}},te={key:0,class:"loading-block"},ae={class:"empty"},oe={style:{"font-weight":"600",color:"var(--c-text)"}},ne={class:"hint"},se=["placeholder"],ie={class:"btn",type:"submit"},le={style:{display:"flex","align-items":"center",gap:"10px","flex-wrap":"wrap","margin-bottom":"4px"}},ue={class:"badge"},re={style:{"font-size":"16px","word-break":"break-all"}},ce={key:0,class:"hint",style:{margin:"0"}},pe={class:"hint",style:{"margin-bottom":"18px"}},de={key:0,class:"loading-block"},me={class:"text-view"},ve={style:{display:"flex",gap:"10px","margin-top":"14px","flex-wrap":"wrap"}},ye={class:"file-summary"},ke={style:{"font-weight":"600","word-break":"break-all"}},fe={class:"hint",style:{margin:"0"}},ge={key:0,style:{margin:"16px 0 6px"}},he={class:"progress"},xe={class:"hint"},_e=["disabled"],Be=N({__name:"PickupView",setup(we){const{t:e}=U(),D=G(),F=O(),k=q(),d=T(()=>String(D.params.code??"").trim().split(/\s+/)[0]),r=p("loading"),m=p(""),o=p(null),v=p(""),f=p(!1),c=p(null);async function g(){if(!d.value){r.value="error",m.value=e("pickup.emptyCode");return}r.value="loading",m.value="",o.value=null,v.value="";try{const a=await X(d.value);if(o.value=a,r.value="ready",a.isText){f.value=!0;try{v.value=await Y(d.value)}finally{f.value=!1}}}catch(a){r.value="error",m.value=a instanceof C?a.code===404?e("pickup.notFound"):a.code===423?e("home.rateLimited"):a.code===428?e("home.notInitialized"):a.msg:e("pickup.failedDefault")}}I(g),b(d,()=>{g()}),b(()=>e("pickup.emptyCode"),()=>{r.value==="error"&&m.value&&g()});async function A(){if(o.value){c.value=0;try{const{blob:a,filename:i}=await Z(d.value,x=>c.value=x);B(a,i||o.value.name||"download"),k.success(e("pickup.downloaded"))}catch(a){k.error(a instanceof C?a.msg:e("pickup.downloadFailed"))}finally{c.value=null}}}async function M(){await W(v.value)?k.success(e("pickup.copied")):k.error(e("common.copyFailed"))}function P(){if(!o.value)return;const a=new Blob([v.value],{type:"text/plain;charset=utf-8"}),i=o.value.name?.includes(".")?o.value.name:`${o.value.name||"text"}.txt`;B(a,i)}const h=p("");function S(){const a=h.value.trim();a&&F.push({name:"pickup",params:{code:a}})}const V=T(()=>{const a=o.value;return a?a.remainingDownloads===null||a.remainingDownloads===void 0||a.remainingDownloads<0?e("pickup.remainingUnlimited"):e("pickup.remainingCount",{n:a.remainingDownloads}):""});return(a,i)=>(l(),L(Q,null,{default:R(()=>[t("section",ee,[r.value==="loading"?(l(),u("div",te,[i[1]||(i[1]=t("span",{class:"spin","aria-hidden":"true"},null,-1)),z(" "+s(n(e)("pickup.querying",{code:d.value})),1)])):r.value==="error"?(l(),u(y,{key:1},[t("div",ae,[i[2]||(i[2]=t("div",{class:"empty-icon"},"📮",-1)),t("p",oe,s(m.value||n(e)("pickup.failed")),1),t("p",ne,s(n(e)("pickup.confirmHint")),1)]),t("form",{class:"quick-pickup",style:{"margin-top":"6px","max-width":"none"},onSubmit:E(S,["prevent"])},[H(t("input",{"onUpdate:modelValue":i[0]||(i[0]=x=>h.value=x),class:"input input-mono",placeholder:n(e)("pickup.retryPlaceholder"),maxlength:"32"},null,8,se),[[$,h.value]]),t("button",ie,s(n(e)("pickup.retryButton")),1)],32)],64)):o.value?(l(),u(y,{key:2},[t("div",le,[t("span",ue,s(o.value.isText?n(e)("common.text"):n(e)("common.file")),1),t("strong",re,s(o.value.name),1),o.value.isText?w("",!0):(l(),u("span",ce,s(n(_)(o.value.size)),1))]),t("p",pe,s(V.value)+" · "+s(n(e)("pickup.expireAt",{time:o.value.expiredAt?n(j)(o.value.expiredAt):n(e)("time.permanent")}))+" · "+s(n(J)(o.value.expiredAt)),1),o.value.isText?(l(),u(y,{key:0},[f.value?(l(),u("div",de,[i[3]||(i[3]=t("span",{class:"spin","aria-hidden":"true"},null,-1)),z(" "+s(n(e)("pickup.loadingText")),1)])):(l(),u(y,{key:1},[t("pre",me,s(v.value),1),t("div",ve,[t("button",{class:"btn",type:"button",onClick:M},s(n(e)("pickup.copyContent")),1),t("button",{class:"btn btn-ghost",type:"button",onClick:P},s(n(e)("pickup.downloadTxt")),1)])],64))],64)):(l(),u(y,{key:1},[t("div",ye,[i[4]||(i[4]=t("span",{style:{"font-size":"30px"},"aria-hidden":"true"},"📄",-1)),t("div",null,[t("div",ke,s(o.value.name),1),t("div",fe,s(n(e)("pickup.sizeUsed",{size:n(_)(o.value.size),n:o.value.usedCount})),1)])]),c.value!==null?(l(),u("div",ge,[t("div",he,[t("i",{style:K({width:`${c.value}%`})},null,4)]),t("p",xe,s(n(e)("pickup.downloading",{percent:c.value})),1)])):w("",!0),t("button",{class:"btn btn-block",type:"button",disabled:c.value!==null,onClick:A}," ⬇ "+s(n(e)("pickup.downloadFile",{size:n(_)(o.value.size)})),9,_e)],64))],64)):w("",!0)])]),_:1}))}});export{Be as default};
-1
View File
@@ -1 +0,0 @@
import{d as N,u as U,i as q,I,J as b,z as L,A as R,H as C,h as T,j as p,b as t,c as u,q as z,t as s,f as n,w as E,C as H,D as $,F as y,s as _,g as w,K as j,L as J,n as K,M as B,k as W,N as G,B as O,o as l}from"./index-D7AAbqvI.js";import{P as Q}from"./PageShell-D3dyUalL.js";import{g as X,p as Y,b as Z}from"./share-x2wQCCnt.js";import"./SiteNav.vue_vue_type_script_setup_true_lang-B5OHbKDQ.js";const ee={class:"card",style:{"max-width":"640px",margin:"12px auto 0"}},te={key:0,class:"loading-block"},ae={class:"empty"},oe={style:{"font-weight":"600",color:"var(--c-text)"}},ne={class:"hint"},se=["placeholder"],ie={class:"btn",type:"submit"},le={style:{display:"flex","align-items":"center",gap:"10px","flex-wrap":"wrap","margin-bottom":"4px"}},ue={class:"badge"},re={style:{"font-size":"16px","word-break":"break-all"}},ce={key:0,class:"hint",style:{margin:"0"}},pe={class:"hint",style:{"margin-bottom":"18px"}},de={key:0,class:"loading-block"},me={class:"text-view"},ve={style:{display:"flex",gap:"10px","margin-top":"14px","flex-wrap":"wrap"}},ye={class:"file-summary"},ke={style:{"font-weight":"600","word-break":"break-all"}},fe={class:"hint",style:{margin:"0"}},ge={key:0,style:{margin:"16px 0 6px"}},he={class:"progress"},xe={class:"hint"},_e=["disabled"],Be=N({__name:"PickupView",setup(we){const{t:e}=U(),D=G(),F=O(),k=q(),d=T(()=>String(D.params.code??"").trim().split(/\s+/)[0]),r=p("loading"),m=p(""),o=p(null),v=p(""),f=p(!1),c=p(null);async function g(){if(!d.value){r.value="error",m.value=e("pickup.emptyCode");return}r.value="loading",m.value="",o.value=null,v.value="";try{const a=await X(d.value);if(o.value=a,r.value="ready",a.isText){f.value=!0;try{v.value=await Y(d.value)}finally{f.value=!1}}}catch(a){r.value="error",m.value=a instanceof C?a.code===404?e("pickup.notFound"):a.code===423?e("home.rateLimited"):a.code===428?e("home.notInitialized"):a.msg:e("pickup.failedDefault")}}I(g),b(d,()=>{g()}),b(()=>e("pickup.emptyCode"),()=>{r.value==="error"&&m.value&&g()});async function A(){if(o.value){c.value=0;try{const{blob:a,filename:i}=await Z(d.value,x=>c.value=x);B(a,i||o.value.name||"download"),k.success(e("pickup.downloaded"))}catch(a){k.error(a instanceof C?a.msg:e("pickup.downloadFailed"))}finally{c.value=null}}}async function M(){await W(v.value)?k.success(e("pickup.copied")):k.error(e("common.copyFailed"))}function P(){if(!o.value)return;const a=new Blob([v.value],{type:"text/plain;charset=utf-8"}),i=o.value.name?.includes(".")?o.value.name:`${o.value.name||"text"}.txt`;B(a,i)}const h=p("");function S(){const a=h.value.trim();a&&F.push({name:"pickup",params:{code:a}})}const V=T(()=>{const a=o.value;return a?a.remainingDownloads===null||a.remainingDownloads===void 0||a.remainingDownloads<0?e("pickup.remainingUnlimited"):e("pickup.remainingCount",{n:a.remainingDownloads}):""});return(a,i)=>(l(),L(Q,null,{default:R(()=>[t("section",ee,[r.value==="loading"?(l(),u("div",te,[i[1]||(i[1]=t("span",{class:"spin","aria-hidden":"true"},null,-1)),z(" "+s(n(e)("pickup.querying",{code:d.value})),1)])):r.value==="error"?(l(),u(y,{key:1},[t("div",ae,[i[2]||(i[2]=t("div",{class:"empty-icon"},"📮",-1)),t("p",oe,s(m.value||n(e)("pickup.failed")),1),t("p",ne,s(n(e)("pickup.confirmHint")),1)]),t("form",{class:"quick-pickup",style:{"margin-top":"6px","max-width":"none"},onSubmit:E(S,["prevent"])},[H(t("input",{"onUpdate:modelValue":i[0]||(i[0]=x=>h.value=x),class:"input input-mono",placeholder:n(e)("pickup.retryPlaceholder"),maxlength:"32"},null,8,se),[[$,h.value]]),t("button",ie,s(n(e)("pickup.retryButton")),1)],32)],64)):o.value?(l(),u(y,{key:2},[t("div",le,[t("span",ue,s(o.value.isText?n(e)("common.text"):n(e)("common.file")),1),t("strong",re,s(o.value.name),1),o.value.isText?w("",!0):(l(),u("span",ce,s(n(_)(o.value.size)),1))]),t("p",pe,s(V.value)+" · "+s(n(e)("pickup.expireAt",{time:o.value.expiredAt?n(j)(o.value.expiredAt):n(e)("time.permanent")}))+" · "+s(n(J)(o.value.expiredAt)),1),o.value.isText?(l(),u(y,{key:0},[f.value?(l(),u("div",de,[i[3]||(i[3]=t("span",{class:"spin","aria-hidden":"true"},null,-1)),z(" "+s(n(e)("pickup.loadingText")),1)])):(l(),u(y,{key:1},[t("pre",me,s(v.value),1),t("div",ve,[t("button",{class:"btn",type:"button",onClick:M},s(n(e)("pickup.copyContent")),1),t("button",{class:"btn btn-ghost",type:"button",onClick:P},s(n(e)("pickup.downloadTxt")),1)])],64))],64)):(l(),u(y,{key:1},[t("div",ye,[i[4]||(i[4]=t("span",{style:{"font-size":"30px"},"aria-hidden":"true"},"📄",-1)),t("div",null,[t("div",ke,s(o.value.name),1),t("div",fe,s(n(e)("pickup.sizeUsed",{size:n(_)(o.value.size),n:o.value.usedCount})),1)])]),c.value!==null?(l(),u("div",ge,[t("div",he,[t("i",{style:K({width:`${c.value}%`})},null,4)]),t("p",xe,s(n(e)("pickup.downloading",{percent:c.value})),1)])):w("",!0),t("button",{class:"btn btn-block",type:"button",disabled:c.value!==null,onClick:A}," ⬇ "+s(n(e)("pickup.downloadFile",{size:n(_)(o.value.size)})),9,_e)],64))],64)):w("",!0)])]),_:1}))}});export{Be as default};
-1
View File
@@ -1 +0,0 @@
import{d as N,u as U,i as q,I,J as b,z as L,A as R,H as C,h as T,j as p,b as t,c as u,q as z,t as s,f as n,w as E,C as H,D as $,F as y,s as _,g as w,K as j,L as J,n as K,M as B,k as W,N as G,B as O,o as l}from"./index-QLbKGtH7.js";import{P as Q}from"./PageShell-D1DY7qw8.js";import{g as X,p as Y,b as Z}from"./share-Y37-qxxb.js";import"./SiteNav.vue_vue_type_script_setup_true_lang-8XmHL8P7.js";const ee={class:"card",style:{"max-width":"640px",margin:"12px auto 0"}},te={key:0,class:"loading-block"},ae={class:"empty"},oe={style:{"font-weight":"600",color:"var(--c-text)"}},ne={class:"hint"},se=["placeholder"],ie={class:"btn",type:"submit"},le={style:{display:"flex","align-items":"center",gap:"10px","flex-wrap":"wrap","margin-bottom":"4px"}},ue={class:"badge"},re={style:{"font-size":"16px","word-break":"break-all"}},ce={key:0,class:"hint",style:{margin:"0"}},pe={class:"hint",style:{"margin-bottom":"18px"}},de={key:0,class:"loading-block"},me={class:"text-view"},ve={style:{display:"flex",gap:"10px","margin-top":"14px","flex-wrap":"wrap"}},ye={class:"file-summary"},ke={style:{"font-weight":"600","word-break":"break-all"}},fe={class:"hint",style:{margin:"0"}},ge={key:0,style:{margin:"16px 0 6px"}},he={class:"progress"},xe={class:"hint"},_e=["disabled"],Be=N({__name:"PickupView",setup(we){const{t:e}=U(),D=G(),F=O(),k=q(),d=T(()=>String(D.params.code??"").trim().split(/\s+/)[0]),r=p("loading"),m=p(""),o=p(null),v=p(""),f=p(!1),c=p(null);async function g(){if(!d.value){r.value="error",m.value=e("pickup.emptyCode");return}r.value="loading",m.value="",o.value=null,v.value="";try{const a=await X(d.value);if(o.value=a,r.value="ready",a.isText){f.value=!0;try{v.value=await Y(d.value)}finally{f.value=!1}}}catch(a){r.value="error",m.value=a instanceof C?a.code===404?e("pickup.notFound"):a.code===423?e("home.rateLimited"):a.code===428?e("home.notInitialized"):a.msg:e("pickup.failedDefault")}}I(g),b(d,()=>{g()}),b(()=>e("pickup.emptyCode"),()=>{r.value==="error"&&m.value&&g()});async function A(){if(o.value){c.value=0;try{const{blob:a,filename:i}=await Z(d.value,x=>c.value=x);B(a,i||o.value.name||"download"),k.success(e("pickup.downloaded"))}catch(a){k.error(a instanceof C?a.msg:e("pickup.downloadFailed"))}finally{c.value=null}}}async function M(){await W(v.value)?k.success(e("pickup.copied")):k.error(e("common.copyFailed"))}function P(){if(!o.value)return;const a=new Blob([v.value],{type:"text/plain;charset=utf-8"}),i=o.value.name?.includes(".")?o.value.name:`${o.value.name||"text"}.txt`;B(a,i)}const h=p("");function S(){const a=h.value.trim();a&&F.push({name:"pickup",params:{code:a}})}const V=T(()=>{const a=o.value;return a?a.remainingDownloads===null||a.remainingDownloads===void 0||a.remainingDownloads<0?e("pickup.remainingUnlimited"):e("pickup.remainingCount",{n:a.remainingDownloads}):""});return(a,i)=>(l(),L(Q,null,{default:R(()=>[t("section",ee,[r.value==="loading"?(l(),u("div",te,[i[1]||(i[1]=t("span",{class:"spin","aria-hidden":"true"},null,-1)),z(" "+s(n(e)("pickup.querying",{code:d.value})),1)])):r.value==="error"?(l(),u(y,{key:1},[t("div",ae,[i[2]||(i[2]=t("div",{class:"empty-icon"},"📮",-1)),t("p",oe,s(m.value||n(e)("pickup.failed")),1),t("p",ne,s(n(e)("pickup.confirmHint")),1)]),t("form",{class:"quick-pickup",style:{"margin-top":"6px","max-width":"none"},onSubmit:E(S,["prevent"])},[H(t("input",{"onUpdate:modelValue":i[0]||(i[0]=x=>h.value=x),class:"input input-mono",placeholder:n(e)("pickup.retryPlaceholder"),maxlength:"32"},null,8,se),[[$,h.value]]),t("button",ie,s(n(e)("pickup.retryButton")),1)],32)],64)):o.value?(l(),u(y,{key:2},[t("div",le,[t("span",ue,s(o.value.isText?n(e)("common.text"):n(e)("common.file")),1),t("strong",re,s(o.value.name),1),o.value.isText?w("",!0):(l(),u("span",ce,s(n(_)(o.value.size)),1))]),t("p",pe,s(V.value)+" · "+s(n(e)("pickup.expireAt",{time:o.value.expiredAt?n(j)(o.value.expiredAt):n(e)("time.permanent")}))+" · "+s(n(J)(o.value.expiredAt)),1),o.value.isText?(l(),u(y,{key:0},[f.value?(l(),u("div",de,[i[3]||(i[3]=t("span",{class:"spin","aria-hidden":"true"},null,-1)),z(" "+s(n(e)("pickup.loadingText")),1)])):(l(),u(y,{key:1},[t("pre",me,s(v.value),1),t("div",ve,[t("button",{class:"btn",type:"button",onClick:M},s(n(e)("pickup.copyContent")),1),t("button",{class:"btn btn-ghost",type:"button",onClick:P},s(n(e)("pickup.downloadTxt")),1)])],64))],64)):(l(),u(y,{key:1},[t("div",ye,[i[4]||(i[4]=t("span",{style:{"font-size":"30px"},"aria-hidden":"true"},"📄",-1)),t("div",null,[t("div",ke,s(o.value.name),1),t("div",fe,s(n(e)("pickup.sizeUsed",{size:n(_)(o.value.size),n:o.value.usedCount})),1)])]),c.value!==null?(l(),u("div",ge,[t("div",he,[t("i",{style:K({width:`${c.value}%`})},null,4)]),t("p",xe,s(n(e)("pickup.downloading",{percent:c.value})),1)])):w("",!0),t("button",{class:"btn btn-block",type:"button",disabled:c.value!==null,onClick:A}," ⬇ "+s(n(e)("pickup.downloadFile",{size:n(_)(o.value.size)})),9,_e)],64))],64)):w("",!0)])]),_:1}))}});export{Be as default};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
.settings-grid[data-v-11453c11]{display:grid;gap:16px}.save-row[data-v-11453c11]{display:flex;justify-content:flex-end}.notify-switch-row[data-v-11453c11]{display:flex;align-items:center;justify-content:space-between;gap:12px}.notify-switch-row label[data-v-11453c11]{margin-bottom:0}.unit-row[data-v-11453c11]{display:flex;gap:8px;align-items:center}.unit-row .input[data-v-11453c11]{flex:1}.unit-select[data-v-11453c11]{flex:0 0 110px!important}
-1
View File
@@ -1 +0,0 @@
.settings-grid[data-v-03aa64c9]{display:grid;gap:16px}.save-row[data-v-03aa64c9]{display:flex;justify-content:flex-end}.notify-switch-row[data-v-03aa64c9]{display:flex;align-items:center;justify-content:space-between;gap:12px}.notify-switch-row label[data-v-03aa64c9]{margin-bottom:0}.unit-row[data-v-03aa64c9]{display:flex;gap:8px;align-items:center}.unit-row .input[data-v-03aa64c9]{flex:1}.unit-select[data-v-03aa64c9]{flex:0 0 110px!important}
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
import{d as z,u as E,a as x,c as i,G as B,f as t,A as m,b as a,F as _,r as v,az as S,t as r,h as d,aA as p,aB as T,O as A,o,z as M,q as V,e as b,aC as w}from"./index-QLbKGtH7.js";const D={class:"site-nav"},F=["src"],I={class:"brand-name"},O=["aria-label"],R={class:"nav-controls"},U=["aria-label"],q=["aria-checked","title","onClick"],G={class:"nav-control-icon","aria-hidden":"true"},H=["title"],K=z({__name:"SiteNav",setup($){const{t:s}=E(),c=x(),{mode:u,setMode:k}=w(),g=d(()=>[{to:"/",label:s("nav.home"),match:n=>n==="/"}]);function y(n){return n.match(location.pathname)}const C={light:"☀️",dark:"🌙",system:"💻"},f=d(()=>({light:s("theme.light"),dark:s("theme.dark"),system:s("theme.system")})),L=d(()=>p()==="zh-CN"?"中文":"English");function N(){const n=p()==="zh-CN"?"en-US":"zh-CN";T(n)}return(n,l)=>{const h=A("RouterLink");return o(),i("header",D,[B(h,{class:"brand",to:"/",title:t(s)("nav.homeTitle",{name:t(c).displayName})},{default:m(()=>[a("img",{src:t(c).displayLogoUrl,alt:"Logo",onError:l[0]||(l[0]=e=>e.target.style.visibility="hidden")},null,40,F),a("span",I,r(t(c).displayName),1)]),_:1},8,["title"]),a("nav",{class:"nav-links","aria-label":t(s)("nav.mainNav")},[(o(!0),i(_,null,v(g.value,e=>(o(),M(h,{key:e.to,to:e.to,class:b({"router-link-active":y(e)})},{default:m(()=>[V(r(e.label),1)]),_:2},1032,["to","class"]))),128))],8,O),a("div",R,[a("div",{class:"theme-seg",role:"radiogroup","aria-label":t(s)("theme.label")},[(o(!0),i(_,null,v(t(S),e=>(o(),i("button",{key:e,type:"button",role:"radio","aria-checked":t(u)===e,class:b({active:t(u)===e}),title:f.value[e],onClick:j=>t(k)(e)},[a("span",G,r(C[e]),1)],10,q))),128))],8,U),a("button",{class:"nav-control",type:"button",title:t(s)("lang.label"),onClick:N},[l[1]||(l[1]=a("span",{class:"nav-control-icon","aria-hidden":"true"},"🌐",-1)),a("span",null,r(L.value),1)],8,H)])])}}});export{K as _};
@@ -1 +0,0 @@
import{d as z,u as E,a as x,c as i,G as B,f as t,A as m,b as a,F as _,r as v,az as S,t as r,h as d,aA as p,aB as T,O as A,o,z as M,q as V,e as b,aC as w}from"./index-D7AAbqvI.js";const D={class:"site-nav"},F=["src"],I={class:"brand-name"},O=["aria-label"],R={class:"nav-controls"},U=["aria-label"],q=["aria-checked","title","onClick"],G={class:"nav-control-icon","aria-hidden":"true"},H=["title"],K=z({__name:"SiteNav",setup($){const{t:s}=E(),c=x(),{mode:u,setMode:k}=w(),g=d(()=>[{to:"/",label:s("nav.home"),match:n=>n==="/"}]);function y(n){return n.match(location.pathname)}const C={light:"☀️",dark:"🌙",system:"💻"},f=d(()=>({light:s("theme.light"),dark:s("theme.dark"),system:s("theme.system")})),L=d(()=>p()==="zh-CN"?"中文":"English");function N(){const n=p()==="zh-CN"?"en-US":"zh-CN";T(n)}return(n,l)=>{const h=A("RouterLink");return o(),i("header",D,[B(h,{class:"brand",to:"/",title:t(s)("nav.homeTitle",{name:t(c).displayName})},{default:m(()=>[a("img",{src:t(c).displayLogoUrl,alt:"Logo",onError:l[0]||(l[0]=e=>e.target.style.visibility="hidden")},null,40,F),a("span",I,r(t(c).displayName),1)]),_:1},8,["title"]),a("nav",{class:"nav-links","aria-label":t(s)("nav.mainNav")},[(o(!0),i(_,null,v(g.value,e=>(o(),M(h,{key:e.to,to:e.to,class:b({"router-link-active":y(e)})},{default:m(()=>[V(r(e.label),1)]),_:2},1032,["to","class"]))),128))],8,O),a("div",R,[a("div",{class:"theme-seg",role:"radiogroup","aria-label":t(s)("theme.label")},[(o(!0),i(_,null,v(t(S),e=>(o(),i("button",{key:e,type:"button",role:"radio","aria-checked":t(u)===e,class:b({active:t(u)===e}),title:f.value[e],onClick:j=>t(k)(e)},[a("span",G,r(C[e]),1)],10,q))),128))],8,U),a("button",{class:"nav-control",type:"button",title:t(s)("lang.label"),onClick:N},[l[1]||(l[1]=a("span",{class:"nav-control-icon","aria-hidden":"true"},"🌐",-1)),a("span",null,r(L.value),1)],8,H)])])}}});export{K as _};
@@ -1 +0,0 @@
import{d as z,u as E,a as x,c as i,G as B,f as t,A as m,b as a,F as _,r as v,az as S,t as r,h as d,aA as p,aB as T,O as A,o,z as M,q as V,e as b,aC as w}from"./index-BKnWAKao.js";const D={class:"site-nav"},F=["src"],I={class:"brand-name"},O=["aria-label"],R={class:"nav-controls"},U=["aria-label"],q=["aria-checked","title","onClick"],G={class:"nav-control-icon","aria-hidden":"true"},H=["title"],K=z({__name:"SiteNav",setup($){const{t:s}=E(),c=x(),{mode:u,setMode:k}=w(),g=d(()=>[{to:"/",label:s("nav.home"),match:n=>n==="/"}]);function y(n){return n.match(location.pathname)}const C={light:"☀️",dark:"🌙",system:"💻"},f=d(()=>({light:s("theme.light"),dark:s("theme.dark"),system:s("theme.system")})),L=d(()=>p()==="zh-CN"?"中文":"English");function N(){const n=p()==="zh-CN"?"en-US":"zh-CN";T(n)}return(n,l)=>{const h=A("RouterLink");return o(),i("header",D,[B(h,{class:"brand",to:"/",title:t(s)("nav.homeTitle",{name:t(c).displayName})},{default:m(()=>[a("img",{src:t(c).displayLogoUrl,alt:"Logo",onError:l[0]||(l[0]=e=>e.target.style.visibility="hidden")},null,40,F),a("span",I,r(t(c).displayName),1)]),_:1},8,["title"]),a("nav",{class:"nav-links","aria-label":t(s)("nav.mainNav")},[(o(!0),i(_,null,v(g.value,e=>(o(),M(h,{key:e.to,to:e.to,class:b({"router-link-active":y(e)})},{default:m(()=>[V(r(e.label),1)]),_:2},1032,["to","class"]))),128))],8,O),a("div",R,[a("div",{class:"theme-seg",role:"radiogroup","aria-label":t(s)("theme.label")},[(o(!0),i(_,null,v(t(S),e=>(o(),i("button",{key:e,type:"button",role:"radio","aria-checked":t(u)===e,class:b({active:t(u)===e}),title:f.value[e],onClick:j=>t(k)(e)},[a("span",G,r(C[e]),1)],10,q))),128))],8,U),a("button",{class:"nav-control",type:"button",title:t(s)("lang.label"),onClick:N},[l[1]||(l[1]=a("span",{class:"nav-control-icon","aria-hidden":"true"},"🌐",-1)),a("span",null,r(L.value),1)],8,H)])])}}});export{K as _};
@@ -1 +0,0 @@
import{d as z,u as E,a as x,c as i,G as B,f as t,A as m,b as a,F as _,r as v,az as S,t as r,h as d,aA as p,aB as T,O as A,o,z as M,q as V,e as b,aC as w}from"./index-DSPQyv0Z.js";const D={class:"site-nav"},F=["src"],I={class:"brand-name"},O=["aria-label"],R={class:"nav-controls"},U=["aria-label"],q=["aria-checked","title","onClick"],G={class:"nav-control-icon","aria-hidden":"true"},H=["title"],K=z({__name:"SiteNav",setup($){const{t:s}=E(),c=x(),{mode:u,setMode:k}=w(),g=d(()=>[{to:"/",label:s("nav.home"),match:n=>n==="/"}]);function y(n){return n.match(location.pathname)}const C={light:"☀️",dark:"🌙",system:"💻"},f=d(()=>({light:s("theme.light"),dark:s("theme.dark"),system:s("theme.system")})),L=d(()=>p()==="zh-CN"?"中文":"English");function N(){const n=p()==="zh-CN"?"en-US":"zh-CN";T(n)}return(n,l)=>{const h=A("RouterLink");return o(),i("header",D,[B(h,{class:"brand",to:"/",title:t(s)("nav.homeTitle",{name:t(c).displayName})},{default:m(()=>[a("img",{src:t(c).displayLogoUrl,alt:"Logo",onError:l[0]||(l[0]=e=>e.target.style.visibility="hidden")},null,40,F),a("span",I,r(t(c).displayName),1)]),_:1},8,["title"]),a("nav",{class:"nav-links","aria-label":t(s)("nav.mainNav")},[(o(!0),i(_,null,v(g.value,e=>(o(),M(h,{key:e.to,to:e.to,class:b({"router-link-active":y(e)})},{default:m(()=>[V(r(e.label),1)]),_:2},1032,["to","class"]))),128))],8,O),a("div",R,[a("div",{class:"theme-seg",role:"radiogroup","aria-label":t(s)("theme.label")},[(o(!0),i(_,null,v(t(S),e=>(o(),i("button",{key:e,type:"button",role:"radio","aria-checked":t(u)===e,class:b({active:t(u)===e}),title:f.value[e],onClick:j=>t(k)(e)},[a("span",G,r(C[e]),1)],10,q))),128))],8,U),a("button",{class:"nav-control",type:"button",title:t(s)("lang.label"),onClick:N},[l[1]||(l[1]=a("span",{class:"nav-control-icon","aria-hidden":"true"},"🌐",-1)),a("span",null,r(L.value),1)],8,H)])])}}});export{K as _};
-1
View File
@@ -1 +0,0 @@
import{v as n,S as e,x as s}from"./index-QLbKGtH7.js";function u(t){return n(s.adminLogin,{method:"POST",json:{password:t}})}function m(){return n(s.adminVerify)}async function l(){await n(s.adminLogout,{method:"POST"})}async function g(t){const a=await n(s.adminFileList,{query:{page:t.page,size:t.size,keyword:t.keyword||void 0}}),o=e(a,["data","list","items","files"])??[],d=Number(e(a,["total","count"])??o.length);return{page:Number(e(a,["page"])??t.page),size:Number(e(a,["size"])??t.size),total:d,data:o.map(i=>({id:Number(e(i,["id"])??0),code:String(e(i,["code"])??""),name:String(e(i,["name"])??`${e(i,["prefix"])??""}${e(i,["suffix"])??""}`),suffix:String(e(i,["suffix"])??""),size:Number(e(i,["size"])??0),isText:!!(e(i,["isText","is_text"])??!1),expiredAt:e(i,["expiredAt","expired_at","expires_at"])??null,expiredCount:e(i,["expiredCount","expired_count"])??null,usedCount:Number(e(i,["usedCount","used_count"])??0),createdAt:e(i,["createdAt","created_at"])??null,isExpired:!!(e(i,["isExpired","is_expired"])??!1)}))}}async function p(t){await n(s.adminFileDelete,{method:"DELETE",json:{id:t}})}async function f(t){await n(s.adminFileBatchDelete,{method:"POST",json:{ids:t}})}async function y(t){await n(s.adminFileUpdate,{method:"PATCH",json:t})}async function w(){const t=await n(s.adminConfigGet);if(t&&typeof t=="object"&&!Array.isArray(t)){const a=t;return e(a,["config","data","settings"])??a}return{}}async function S(t){await n(s.adminConfigUpdate,{method:"PATCH",json:t})}async function _(t){const a=await n(s.adminStorageSwitch,{method:"POST",json:{engine:t}});return String(a?.engine??t)}async function x(t,a){await n(s.adminPasswordUpdate,{method:"PATCH",json:{old_password:t,new_password:a}})}async function b(t){const a=await n(s.adminAuditList,{query:{page:t.page,size:t.size,action:t.action||void 0,result:t.result||void 0,ip:t.ip||void 0,start_time:t.startTime||void 0,end_time:t.endTime||void 0}}),o=e(a,["data","list","items","logs"])??[],d=Number(e(a,["total","count"])??o.length);return{page:Number(e(a,["page"])??t.page),size:Number(e(a,["size"])??t.size),total:d,data:o.map((i,r)=>({id:Number(e(i,["id"])??r+1),action:String(e(i,["action"])??""),result:String(e(i,["result"])??""),fileCode:String(e(i,["file_code","fileCode","code"])??""),fileName:String(e(i,["file_name","fileName","name"])??""),sizeBytes:e(i,["size_bytes","sizeBytes","size"])??null,transferredBytes:e(i,["transferred_bytes","transferredBytes","bytes"])??null,ip:String(e(i,["ip","client_ip","clientIp"])??""),userAgent:String(e(i,["user_agent","userAgent"])??""),deviceOs:String(e(i,["device_os","deviceOs","os"])??""),deviceBrowser:String(e(i,["device_browser","deviceBrowser","browser"])??""),deviceType:String(e(i,["device_type","deviceType"])??""),actor:String(e(i,["actor"])??""),errorMsg:String(e(i,["error_msg","errorMsg","error"])??""),durationMs:e(i,["duration_ms","durationMs","duration"])??null,createdAt:e(i,["created_at","createdAt","time"])??null}))}}export{g as a,f as b,p as c,y as d,b as e,w as f,_ as g,S as h,x as i,l as j,m as k,u as l};
-1
View File
@@ -1 +0,0 @@
import{v as n,S as e,x as s}from"./index-DSPQyv0Z.js";function u(t){return n(s.adminLogin,{method:"POST",json:{password:t}})}function m(){return n(s.adminVerify)}async function l(){await n(s.adminLogout,{method:"POST"})}async function g(t){const a=await n(s.adminFileList,{query:{page:t.page,size:t.size,keyword:t.keyword||void 0}}),o=e(a,["data","list","items","files"])??[],d=Number(e(a,["total","count"])??o.length);return{page:Number(e(a,["page"])??t.page),size:Number(e(a,["size"])??t.size),total:d,data:o.map(i=>({id:Number(e(i,["id"])??0),code:String(e(i,["code"])??""),name:String(e(i,["name"])??`${e(i,["prefix"])??""}${e(i,["suffix"])??""}`),suffix:String(e(i,["suffix"])??""),size:Number(e(i,["size"])??0),isText:!!(e(i,["isText","is_text"])??!1),expiredAt:e(i,["expiredAt","expired_at","expires_at"])??null,expiredCount:e(i,["expiredCount","expired_count"])??null,usedCount:Number(e(i,["usedCount","used_count"])??0),createdAt:e(i,["createdAt","created_at"])??null,isExpired:!!(e(i,["isExpired","is_expired"])??!1)}))}}async function p(t){await n(s.adminFileDelete,{method:"DELETE",json:{id:t}})}async function f(t){await n(s.adminFileBatchDelete,{method:"POST",json:{ids:t}})}async function y(t){await n(s.adminFileUpdate,{method:"PATCH",json:t})}async function w(){const t=await n(s.adminConfigGet);if(t&&typeof t=="object"&&!Array.isArray(t)){const a=t;return e(a,["config","data","settings"])??a}return{}}async function S(t){await n(s.adminConfigUpdate,{method:"PATCH",json:t})}async function _(t){const a=await n(s.adminStorageSwitch,{method:"POST",json:{engine:t}});return String(a?.engine??t)}async function x(t,a){await n(s.adminPasswordUpdate,{method:"PATCH",json:{old_password:t,new_password:a}})}async function b(t){const a=await n(s.adminAuditList,{query:{page:t.page,size:t.size,action:t.action||void 0,result:t.result||void 0,ip:t.ip||void 0,start_time:t.startTime||void 0,end_time:t.endTime||void 0}}),o=e(a,["data","list","items","logs"])??[],d=Number(e(a,["total","count"])??o.length);return{page:Number(e(a,["page"])??t.page),size:Number(e(a,["size"])??t.size),total:d,data:o.map((i,r)=>({id:Number(e(i,["id"])??r+1),action:String(e(i,["action"])??""),result:String(e(i,["result"])??""),fileCode:String(e(i,["file_code","fileCode","code"])??""),fileName:String(e(i,["file_name","fileName","name"])??""),sizeBytes:e(i,["size_bytes","sizeBytes","size"])??null,transferredBytes:e(i,["transferred_bytes","transferredBytes","bytes"])??null,ip:String(e(i,["ip","client_ip","clientIp"])??""),userAgent:String(e(i,["user_agent","userAgent"])??""),deviceOs:String(e(i,["device_os","deviceOs","os"])??""),deviceBrowser:String(e(i,["device_browser","deviceBrowser","browser"])??""),deviceType:String(e(i,["device_type","deviceType"])??""),actor:String(e(i,["actor"])??""),errorMsg:String(e(i,["error_msg","errorMsg","error"])??""),durationMs:e(i,["duration_ms","durationMs","duration"])??null,createdAt:e(i,["created_at","createdAt","time"])??null}))}}export{g as a,f as b,p as c,y as d,b as e,w as f,_ as g,S as h,x as i,l as j,m as k,u as l};
-1
View File
@@ -1 +0,0 @@
import{v as n,S as e,x as s}from"./index-BKnWAKao.js";function u(t){return n(s.adminLogin,{method:"POST",json:{password:t}})}function m(){return n(s.adminVerify)}async function l(){await n(s.adminLogout,{method:"POST"})}async function g(t){const a=await n(s.adminFileList,{query:{page:t.page,size:t.size,keyword:t.keyword||void 0}}),o=e(a,["data","list","items","files"])??[],d=Number(e(a,["total","count"])??o.length);return{page:Number(e(a,["page"])??t.page),size:Number(e(a,["size"])??t.size),total:d,data:o.map(i=>({id:Number(e(i,["id"])??0),code:String(e(i,["code"])??""),name:String(e(i,["name"])??`${e(i,["prefix"])??""}${e(i,["suffix"])??""}`),suffix:String(e(i,["suffix"])??""),size:Number(e(i,["size"])??0),isText:!!(e(i,["isText","is_text"])??!1),expiredAt:e(i,["expiredAt","expired_at","expires_at"])??null,expiredCount:e(i,["expiredCount","expired_count"])??null,usedCount:Number(e(i,["usedCount","used_count"])??0),createdAt:e(i,["createdAt","created_at"])??null,isExpired:!!(e(i,["isExpired","is_expired"])??!1)}))}}async function p(t){await n(s.adminFileDelete,{method:"DELETE",json:{id:t}})}async function f(t){await n(s.adminFileBatchDelete,{method:"POST",json:{ids:t}})}async function y(t){await n(s.adminFileUpdate,{method:"PATCH",json:t})}async function w(){const t=await n(s.adminConfigGet);if(t&&typeof t=="object"&&!Array.isArray(t)){const a=t;return e(a,["config","data","settings"])??a}return{}}async function S(t){await n(s.adminConfigUpdate,{method:"PATCH",json:t})}async function _(t){const a=await n(s.adminStorageSwitch,{method:"POST",json:{engine:t}});return String(a?.engine??t)}async function x(t,a){await n(s.adminPasswordUpdate,{method:"PATCH",json:{old_password:t,new_password:a}})}async function b(t){const a=await n(s.adminAuditList,{query:{page:t.page,size:t.size,action:t.action||void 0,result:t.result||void 0,ip:t.ip||void 0,start_time:t.startTime||void 0,end_time:t.endTime||void 0}}),o=e(a,["data","list","items","logs"])??[],d=Number(e(a,["total","count"])??o.length);return{page:Number(e(a,["page"])??t.page),size:Number(e(a,["size"])??t.size),total:d,data:o.map((i,r)=>({id:Number(e(i,["id"])??r+1),action:String(e(i,["action"])??""),result:String(e(i,["result"])??""),fileCode:String(e(i,["file_code","fileCode","code"])??""),fileName:String(e(i,["file_name","fileName","name"])??""),sizeBytes:e(i,["size_bytes","sizeBytes","size"])??null,transferredBytes:e(i,["transferred_bytes","transferredBytes","bytes"])??null,ip:String(e(i,["ip","client_ip","clientIp"])??""),userAgent:String(e(i,["user_agent","userAgent"])??""),deviceOs:String(e(i,["device_os","deviceOs","os"])??""),deviceBrowser:String(e(i,["device_browser","deviceBrowser","browser"])??""),deviceType:String(e(i,["device_type","deviceType"])??""),actor:String(e(i,["actor"])??""),errorMsg:String(e(i,["error_msg","errorMsg","error"])??""),durationMs:e(i,["duration_ms","durationMs","duration"])??null,createdAt:e(i,["created_at","createdAt","time"])??null}))}}export{g as a,f as b,p as c,y as d,b as e,w as f,_ as g,S as h,x as i,l as j,m as k,u as l};
-1
View File
@@ -1 +0,0 @@
import{v as n,S as e,x as s}from"./index-D7AAbqvI.js";function u(t){return n(s.adminLogin,{method:"POST",json:{password:t}})}function m(){return n(s.adminVerify)}async function l(){await n(s.adminLogout,{method:"POST"})}async function g(t){const a=await n(s.adminFileList,{query:{page:t.page,size:t.size,keyword:t.keyword||void 0}}),o=e(a,["data","list","items","files"])??[],d=Number(e(a,["total","count"])??o.length);return{page:Number(e(a,["page"])??t.page),size:Number(e(a,["size"])??t.size),total:d,data:o.map(i=>({id:Number(e(i,["id"])??0),code:String(e(i,["code"])??""),name:String(e(i,["name"])??`${e(i,["prefix"])??""}${e(i,["suffix"])??""}`),suffix:String(e(i,["suffix"])??""),size:Number(e(i,["size"])??0),isText:!!(e(i,["isText","is_text"])??!1),expiredAt:e(i,["expiredAt","expired_at","expires_at"])??null,expiredCount:e(i,["expiredCount","expired_count"])??null,usedCount:Number(e(i,["usedCount","used_count"])??0),createdAt:e(i,["createdAt","created_at"])??null,isExpired:!!(e(i,["isExpired","is_expired"])??!1)}))}}async function p(t){await n(s.adminFileDelete,{method:"DELETE",json:{id:t}})}async function f(t){await n(s.adminFileBatchDelete,{method:"POST",json:{ids:t}})}async function y(t){await n(s.adminFileUpdate,{method:"PATCH",json:t})}async function w(){const t=await n(s.adminConfigGet);if(t&&typeof t=="object"&&!Array.isArray(t)){const a=t;return e(a,["config","data","settings"])??a}return{}}async function S(t){await n(s.adminConfigUpdate,{method:"PATCH",json:t})}async function _(t){const a=await n(s.adminStorageSwitch,{method:"POST",json:{engine:t}});return String(a?.engine??t)}async function x(t,a){await n(s.adminPasswordUpdate,{method:"PATCH",json:{old_password:t,new_password:a}})}async function b(t){const a=await n(s.adminAuditList,{query:{page:t.page,size:t.size,action:t.action||void 0,result:t.result||void 0,ip:t.ip||void 0,start_time:t.startTime||void 0,end_time:t.endTime||void 0}}),o=e(a,["data","list","items","logs"])??[],d=Number(e(a,["total","count"])??o.length);return{page:Number(e(a,["page"])??t.page),size:Number(e(a,["size"])??t.size),total:d,data:o.map((i,r)=>({id:Number(e(i,["id"])??r+1),action:String(e(i,["action"])??""),result:String(e(i,["result"])??""),fileCode:String(e(i,["file_code","fileCode","code"])??""),fileName:String(e(i,["file_name","fileName","name"])??""),sizeBytes:e(i,["size_bytes","sizeBytes","size"])??null,transferredBytes:e(i,["transferred_bytes","transferredBytes","bytes"])??null,ip:String(e(i,["ip","client_ip","clientIp"])??""),userAgent:String(e(i,["user_agent","userAgent"])??""),deviceOs:String(e(i,["device_os","deviceOs","os"])??""),deviceBrowser:String(e(i,["device_browser","deviceBrowser","browser"])??""),deviceType:String(e(i,["device_type","deviceType"])??""),actor:String(e(i,["actor"])??""),errorMsg:String(e(i,["error_msg","errorMsg","error"])??""),durationMs:e(i,["duration_ms","durationMs","duration"])??null,createdAt:e(i,["created_at","createdAt","time"])??null}))}}export{g as a,f as b,p as c,y as d,b as e,w as f,_ as g,S as h,x as i,l as j,m as k,u as l};
-1
View File
@@ -1 +0,0 @@
import{av as o,aw as a,W as r}from"./index-BKnWAKao.js";import{j as n,k as i,l as c}from"./admin-KnbIpHLF.js";const s="fcb_admin_flag",l=o("auth",{state:()=>({token:r(),checked:!1}),getters:{isAuthed:t=>!!t.token},actions:{async login(t){const e=(await c(t))?.token;if(!e)throw new Error("登录响应缺少 token");this.token=e,a(e);try{localStorage.setItem(s,"1")}catch{}this.checked=!0},async verify(){if(!this.token)return!1;try{return await i(),this.checked=!0,!0}catch{return this.reset(),!1}},async logout(){try{this.token&&await n()}catch{}this.reset()},reset(){this.token="",this.checked=!1,a("");try{localStorage.removeItem(s)}catch{}}}});export{l as u};
-1
View File
@@ -1 +0,0 @@
import{av as o,aw as a,W as r}from"./index-D7AAbqvI.js";import{j as n,k as i,l as c}from"./admin-uFxGdgNa.js";const s="fcb_admin_flag",l=o("auth",{state:()=>({token:r(),checked:!1}),getters:{isAuthed:t=>!!t.token},actions:{async login(t){const e=(await c(t))?.token;if(!e)throw new Error("登录响应缺少 token");this.token=e,a(e);try{localStorage.setItem(s,"1")}catch{}this.checked=!0},async verify(){if(!this.token)return!1;try{return await i(),this.checked=!0,!0}catch{return this.reset(),!1}},async logout(){try{this.token&&await n()}catch{}this.reset()},reset(){this.token="",this.checked=!1,a("");try{localStorage.removeItem(s)}catch{}}}});export{l as u};
-1
View File
@@ -1 +0,0 @@
import{av as o,aw as a,W as r}from"./index-QLbKGtH7.js";import{j as n,k as i,l as c}from"./admin-BZX1cFNW.js";const s="fcb_admin_flag",l=o("auth",{state:()=>({token:r(),checked:!1}),getters:{isAuthed:t=>!!t.token},actions:{async login(t){const e=(await c(t))?.token;if(!e)throw new Error("登录响应缺少 token");this.token=e,a(e);try{localStorage.setItem(s,"1")}catch{}this.checked=!0},async verify(){if(!this.token)return!1;try{return await i(),this.checked=!0,!0}catch{return this.reset(),!1}},async logout(){try{this.token&&await n()}catch{}this.reset()},reset(){this.token="",this.checked=!1,a("");try{localStorage.removeItem(s)}catch{}}}});export{l as u};
-1
View File
@@ -1 +0,0 @@
import{av as o,aw as a,W as r}from"./index-DSPQyv0Z.js";import{j as n,k as i,l as c}from"./admin-IDpKsD_2.js";const s="fcb_admin_flag",l=o("auth",{state:()=>({token:r(),checked:!1}),getters:{isAuthed:t=>!!t.token},actions:{async login(t){const e=(await c(t))?.token;if(!e)throw new Error("登录响应缺少 token");this.token=e,a(e);try{localStorage.setItem(s,"1")}catch{}this.checked=!0},async verify(){if(!this.token)return!1;try{return await i(),this.checked=!0,!0}catch{return this.reset(),!1}},async logout(){try{this.token&&await n()}catch{}this.reset()},reset(){this.token="",this.checked=!1,a("");try{localStorage.removeItem(s)}catch{}}}});export{l as u};

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