No description
  • TypeScript 49.7%
  • Go 47.1%
  • PowerShell 0.9%
  • Shell 0.7%
  • CSS 0.6%
  • Other 0.9%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-10-12 00:48:43 +08:00
cmd/urlexplorer 初始提交:UrlExplorer 网盘 2026-10-10 00:40:27 +08:00
internal 删除复制短链,目录树改为单分支展开,切换目录时清空旧内容 2026-10-12 00:48:43 +08:00
scripts 构建脚本按锁文件判断依赖是否需要重装 2026-10-10 15:01:00 +08:00
web 删除复制短链,目录树改为单分支展开,切换目录时清空旧内容 2026-10-12 00:48:43 +08:00
.dockerignore 初始提交:UrlExplorer 网盘 2026-10-10 00:40:27 +08:00
.gitattributes 添加 Windows 与 Linux 的编译脚本 2026-10-10 12:49:09 +08:00
.gitignore 添加 Windows 与 Linux 的编译脚本 2026-10-10 12:49:09 +08:00
config.example.yaml 初始提交:UrlExplorer 网盘 2026-10-10 00:40:27 +08:00
docker-compose.yml 初始提交:UrlExplorer 网盘 2026-10-10 00:40:27 +08:00
docker-entrypoint.sh 初始提交:UrlExplorer 网盘 2026-10-10 00:40:27 +08:00
Dockerfile 初始提交:UrlExplorer 网盘 2026-10-10 00:40:27 +08:00
go.mod 公告区支持富文本与独立字号,标题栏字号可调,标签页标题跟随站点名 2026-10-10 15:30:35 +08:00
go.sum 公告区支持富文本与独立字号,标题栏字号可调,标签页标题跟随站点名 2026-10-10 15:30:35 +08:00
README.md 访问计数去重,游客按浏览器 visitor id 统计在线 2026-10-10 16:00:47 +08:00

UrlExplorer

Go Report Card License

UrlExplorer 是一个基于 Go (Golang) 开发的高性能、高并发的网盘 / 文件分享站。

首页直接列出目录,左边是留言板,右边是目录里的链接与文件;顶部一条标语栏和统计条, 底部是「在线列表 / 下载记录 / 退出系统」。前后端一起编译进同一个静态二进制,部署只需要一个文件。

🌟 核心特性

  • 多级目录 —— 目录可以套目录(最多八层),带面包屑导航。首页只列顶层,点进去才是内容,层级再深也不会把首页撑爆。
  • 目录里放链接或文件 —— 一个目录同时容纳跳转短链和上传的文件,可置顶、可隐藏、可调顺序。
  • 留言板 —— 姓名 / 公开勾选 / 内容,支持管理员审核、置顶、回复。免责声明是站点设置,用同样的样式渲染但不会混进审核队列。
  • 首页直接投稿 —— 上传文件、建跳转链接、增加目录都在首页左栏,不必先进后台;匿名投稿可以用开关关掉。
  • 管理员后台 —— 目录与条目、留言审核、用户与配额、站点设置、访问统计五块,全部在 /admin。
  • 运行期统计 —— 访问计数、空间占用、在线人数、下载记录,顶部统计条会自动刷新。
  • 单二进制部署 —— 纯 Go 的 SQLite 驱动(无 CGO)+ go:embed 前端产物,CGO_ENABLED=0 交叉编译出静态二进制。

安全上的几个取舍

事项 做法
文件类型 按内容嗅探(http.DetectContentType)判定,不信任文件名与客户端声明;只放行 image/*(不含 SVG)、text/plain、text/markdown、application/json、application/pdf。响应带 X-Content-Type-Options: nosniff,所以即使 .svg 被存下来也是以 text/plain 惰性返回。
跳转目标 只接受 http / https 绝对地址。否则 /s/{code} 会把 javascript: 之类的地址 302 到站点自己的源上执行。
短码 用 crypto/rand 生成。unlisted / 带密码的分享依赖短码本身不可猜。
密码 bcrypt 存储。登录时用户名不存在也会走一次 bcrypt 比较,避免用响应时间探测账号是否存在。
凭据 存库前用 AES-256-GCM 加密,主密钥来自配置文件、不进数据库。
留言 IP 库里存完整地址供管理员查,公开列表一律掩码(1.2.3.x)。
管理员令牌 声称是管理员的 token 每次都会回读数据库校验。否则一个被降权的管理员能凭 7 天有效的 token 继续越权。
配额 上传大小在写盘前和事务内各校验一次;超限返回 507 Insufficient Storage(413 是错的:载荷合法,只是付不起)。

🛠️ 技术栈

后端 Go 1.27 + Gin + GORM + SQLite(libtnb/sqlite,基于 modernc.org/sqlite 的纯 Go 驱动,无 CGO)+ JWT(HS256)

前端 React 18 + TypeScript + Vite、Tailwind CSS v4 + Radix 无头原语(组件手写,非 shadcn CLI 生成)、Zustand、React Router

🚀 快速开始

1. 克隆仓库

git clone https://github.com/xiaoman1221/UrlExplorer.git
cd UrlExplorer

2. 配置文件

cp config.example.yaml config.yaml

配置文件只放基础设施:监听地址、数据库路径、JWT 密钥、凭据加密主密钥、对外地址、上传目录、可信代理。

其余能在页面上改的(站点名、顶部标语与快捷链接、免责声明、注册开关、匿名投稿开关、留言审核开关、上传上限、空间容量、默认配额、在线判定窗口) 都存在数据库里,登录管理员后在 管理网站 → 站点设置 里改。所以改站点名不需要重启,也不需要碰 YAML。

配置文件不存在时不会报错,会用内置默认值启动(监听 0.0.0.0:8080、库在 data/urlexplorer.db) —— 但默认值里带着公开的 jwt.secret 与 secrets.master_key,启动日志会为此大声警告。对外提供服务前务必换掉。

也可以用环境变量覆盖,容器部署时更顺手:

变量 覆盖
URLEXPLORER_CONFIG 配置文件路径
URLEXPLORER_LISTEN server.listen
URLEXPLORER_DB_PATH database.path
URLEXPLORER_JWT_SECRET jwt.secret
URLEXPLORER_MASTER_KEY secrets.master_key
URLEXPLORER_PUBLIC_BASE_URL site.public_base_url

3. 本地运行

go run cmd/urlexplorer/main.go

首次启动会自动建表,并把首位注册的用户设为超级管理员。

4. 前端开发

cd web
npm install
npm run dev        # http://localhost:5173,/api、/s、/f 反向代理到 :8080

5. 生产编译

必须先构建前端 —— 前端产物通过 go:embed 打进二进制,跳过这一步会把上一次的旧产物嵌进去:

cd web && npm install && npm run build && cd ..
CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o urlexplorer cmd/urlexplorer/main.go
./urlexplorer

6. 编译脚本

仓库带了两个脚本,做的是同一件事:按正确的顺序构建(先前端、再 Go),并把 git 版本号写进二进制 —— /api/v1/health 会返回它。

# Linux / macOS / Git Bash
scripts/build.sh                              # 当前平台
scripts/build.sh linux/amd64 linux/arm64      # 交叉编译
scripts/build.sh --skip-web                   # 复用上次的前端产物
scripts/build.sh --clean-web                  # 强制 npm ci 重装依赖
:: Windows,最简单的一种:双击也行
scripts\build.bat
scripts\build.bat linux/amd64 windows/amd64
scripts\build.bat -SkipWeb
# Windows,等价写法(参数与 .bat 相同)
powershell -ExecutionPolicy Bypass -File scripts\build.ps1
powershell -ExecutionPolicy Bypass -File scripts\build.ps1 -Target linux/amd64,windows/amd64
powershell -ExecutionPolicy Bypass -File scripts\build.ps1 -SkipWeb

build.bat 是 build.ps1 的薄包装,本身不实现任何构建逻辑:构建步骤只放一处,否则改一次模块路径或 linker 参数要同步改三个脚本,迟早对不上。它的价值在于 .ps1 在默认执行策略下双击会报错,而它替你传了 -ExecutionPolicy Bypass 并保持窗口不关。

产物落在 dist/urlexplorer-<os>-<arch>[.exe](该目录已在 .gitignore 里)。 全程 CGO_ENABLED=0,所以都是静态二进制,从一个平台交叉编译另一个平台不需要 任何额外工具链。

⚠️ scripts/build.ps1 必须保存为 UTF-8 带 BOM。Windows PowerShell 5.1 对无 BOM 的 .ps1 按系统 ANSI 码页解码,中文 Windows 上就是 GBK,中文注释会被错解成 与真实原因无关的语法错误。别用会去掉 BOM 的编辑器保存它。

🐳 Docker 部署

镜像采用三阶段构建(Node 构建前端 → Go 编译二进制 → Alpine 运行), 运行时以非 root 用户(uid 10001)执行,最终镜像里只有一个静态编译的二进制。

用 compose 启动(推荐)

cp config.example.yaml config.yaml   # 然后务必修改 jwt.secret

mkdir -p data
export URLEXPLORER_MASTER_KEY="$(openssl rand -base64 32)"
docker compose up -d --build

数据落在 ./data,配置文件只读挂载进容器。

用 docker run 启动

docker build -t urlexplorer:latest .

docker run -d \
  --name urlexplorer \
  --restart unless-stopped \
  -p 8080:8080 \
  -v "$PWD/data:/app/data" \
  -v "$PWD/config.yaml:/app/config.yaml:ro" \
  -e TZ=Asia/Shanghai \
  -e URLEXPLORER_MASTER_KEY="$(openssl rand -base64 32)" \
  urlexplorer:latest

几个必须知道的点

事项 说明
数据卷必须持久化 /app/data 里同时躺着 urlexplorer.db(全部数据)和 uploads/(所有文件)。丢了这个卷就是丢了整个站点。
主密钥要和数据一起备份 secrets.master_key 只存在于配置/环境变量里,库里的凭据是用它加密的。主密钥丢了,已保存的凭据就再也解不开。
config.yaml 必须先存在 挂载一个不存在的文件时,Docker 会在宿主机上创建一个同名空目录,应用读不出内容便静默回落到内置默认值 —— 其中包括公开的默认 jwt.secret。
反向代理要配 trusted_proxies 默认谁都不信(否则任何客户端都能伪造自己的地址)。但在代理后面,不配的话访问日志里的地址会全部变成代理的。在线人数不受影响 —— 浏览器自己带 X-Visitor-Id,不依赖地址区分访客(见下面「访客怎么算一个」)。
改端口要同时改三处 容器内监听哪个端口由 config.yaml 的 server.listen 决定,compose 里的 ports 映射和 Dockerfile 的 HEALTHCHECK 都要跟着改。
/app/data 的属主会被自动修正 入口脚本以 root 起步、chown 之后立刻 su-exec 降权到 uid 10001。不想让容器以 root 起步,见 compose 里 user: 那段注释。

镜像自带 ca-certificates 与 tzdata;健康检查走 /api/v1/health, 数据库不可达时该接口返回 503,因此它能真实反映服务可用性。stop_grace_period 设为 20s —— 应用收到 SIGTERM 后最多用 10s 等在途请求跑完,Docker 默认的 10s 正好卡在边界上,下载会被 SIGKILL 截断。

已知限制

  • 在线人数是单进程的:它来自内存里的最近访问记录,不是数据库。服务重启会清零,跑多个副本时每个副本只报自己看到的那部分。这是刻意的取舍 —— 连接池只有一个连接,每人每 30 秒写一条心跳会和上传、跳转统计、登录全部串行排队。
  • 访客怎么算「一个」:登录用户按账号,游客按浏览器自己生成、存在 localStorage 里的随机 id(随请求以 X-Visitor-Id 带上)。所以同一个出口 IP 下的多个人是多个访客,一个人换网络也还是同一个。没带这个头的客户端(curl、爬虫、老书签)回落到按地址算。这个 id 只是计数用的分组依据,不参与任何鉴权,伪造它只影响自己的计数。
  • 访问计数做了去重:同一个访客在 30 分钟内重复打开同一个目录 / 链接 / 文件只记一次,所以它数的是「多少人打开过」而不是「处理了多少请求」。去重表同样在内存里,重启清零。
  • 上传上限的硬上限是 5 MiB:站点设置里可以把上限调低,但不能调高过编译时的常量,因为 MaxMultipartMemory 和嗅探缓冲区都按它分配。要真正调高,得改 service.MaxUploadSize 再重新编译,同时同步 Docker 内存限制和前置换 nginx 的 client_max_body_size。

📂 项目结构

UrlExplorer/
├── cmd/urlexplorer/     # 主入口(装配 + 优雅关闭)
├── scripts/             # 编译脚本(build.sh / build.ps1 / build.bat)
├── internal/
│   ├── config/          # 配置加载(YAML / JSON,含内置默认值)
│   ├── database/        # GORM + SQLite 初始化、pragma、自动迁移
│   ├── model/           # 数据模型(User / Folder / Link / File / Message / Setting / VisitStat)
│   ├── handler/         # HTTP API(站点、目录、留言、投稿、管理后台)
│   ├── middleware/      # Gin 中间件(鉴权、管理员、在线态、CORS、Request ID)
│   ├── service/         # 全部业务逻辑(目录树、配额、在线态、设置缓存…)
│   ├── secret/          # 凭据的 AES-GCM 加解密
│   └── webui/           # go:embed 嵌入前端构建产物(单二进制部署)
├── web/                 # 前端(React 18 + TS + Vite + Tailwind + Zustand)
│   └── src/             # 构建产物输出到 ../internal/webui/dist
├── Dockerfile           # 三阶段构建:前端 → 二进制 → Alpine 运行时
├── docker-compose.yml   # 单机编排(数据卷、健康检查、优雅关闭)
├── docker-entrypoint.sh # 修正数据卷属主并降权执行
├── .dockerignore        # 构建上下文排除清单
└── config.example.yaml  # 配置文件示例

internal/webui/dist/ 里随仓库带了一个占位 index.html。 go:embed 不接受空目录,没有它全新克隆的仓库根本编译不过; 所以全新克隆直接 go build 是能过的,跑起来会显示「前端尚未构建」那一页, 而不是编译报错。npm run build 会把这个目录整个覆盖掉。

构建产物(dist/assets/)在 .gitignore 里,占位 index.html 则在版本控制里。 因此跑完 npm run build 后 dist/index.html 会显示为已修改,这是预期行为: 它此时指向的 assets/ 并没有进仓库。不要单独提交这个改动 —— 要么保持占位文件不动,要么把 dist/assets/ 也从忽略列表里去掉,让两者一起进仓库。

🔌 API 一览

带 🔒 的需要 Authorization: Bearer <token>;标 🛡 的需要管理员。 「可选」表示匿名也能访问,带了 token 则返回自己的数据。

方法 路径 鉴权 说明
GET /api/v1/health – 健康检查,数据库不可达返回 503
GET /api/v1/site – 站点设置 + 统计,一次拿全(标语栏、导航、免责声明、统计条)
GET /api/v1/site/stats · /site/online · /site/downloads – 统计条 / 在线列表 / 下载记录(后者受设置开关约束)
POST /api/v1/auth/register · /auth/login – 注册(可被关闭)/ 登录
GET /api/v1/auth/me 🔒 当前用户
GET /api/v1/folders · /folder-tree 可选 同级目录 / 整棵可见目录树
GET /api/v1/folders/:code · /:code/entries 可选 目录详情(含面包屑)/ 分页条目
POST /api/v1/folders/:code/visit 可选 记一次浏览(独立于 GET,预取不会灌水)
POST / PATCH / DELETE /api/v1/folders · /:code 🔒 新建 / 改名改可见性 / 删除(非空返回 409)
POST /api/v1/folders/:code/move 🔒 换父目录(成环返回 400)
GET / POST /api/v1/links 可选 链接列表 / 新建跳转链接
PATCH / DELETE /api/v1/links/:id 🔒 修改 / 删除(仅属主或管理员)
GET / POST /api/v1/files 可选 文件列表 / 上传(multipart,字段名 file)
DELETE /api/v1/files/:id 🔒 删除(含磁盘文件)
GET / POST /api/v1/messages 可选 留言板 / 发留言
PATCH / DELETE /api/v1/messages/:id 可选 编辑 / 删除(本人、同 IP 的匿名帖,或管理员)
GET /api/v1/stats/overview 🔒 当前账号的汇总
— /api/v1/admin/* 🛡 总览、目录、条目、留言审核、用户与配额、站点设置、访问与下载记录
GET /s/:code – 302 跳转到目标地址并记一次访问
GET /f/:code – 输出原始文件并记一次访问

/s 与 /f 刻意放在 /api/v1 之外 —— 它们是要贴进聊天窗口的地址,越短越好。 /d/:code 是前端路由,由 SPA 回退兜住,服务端不注册它。

🤝 参与贡献

欢迎提交 Issue 和 Pull Request!

  1. Fork 本仓库
  2. 创建您的特性分支 (git checkout -b feature/AmazingFeature)
  3. 提交您的修改 (git commit -m 'Add some AmazingFeature')
  4. 推送到分支 (git push origin feature/AmazingFeature)
  5. 开启一个 Pull Request

📄 开源协议

本项目基于 MIT License 协议开源。