一个自用的阿里云 OSS 文件管理网站,后端 Go,前端 Vue 3 + Vite。
- 固定管理用户登录
- 上传文件到 OSS,可指定对象 Key 或目录前缀
- 按前缀列举、搜索、刷新对象
- 在线查阅图片、PDF、文本等浏览器可预览文件
- 下载文件
- 删除对象
- 对外提供鉴权后的上传 API
复制 .env.example 为 .env,或直接设置同名环境变量。
Copy-Item .env.example .env必填项:
ADMIN_USERNAME/ADMIN_PASSWORDAPP_SESSION_SECRETUPLOAD_API_TOKENOSS_REGIONOSS_BUCKETOSS_ACCESS_KEY_IDOSS_ACCESS_KEY_SECRET
可选项:
OSS_ENDPOINT:OSS endpoint,例如https://oss-cn-hangzhou.aliyuncs.com;OSS 设为仅内网访问时可填写服务器可达的内网 endpointAPP_PUBLIC_BASE_URL:可选,服务器对外访问基地址,例如https://files.example.com。只有配置后才会生成绝对服务器下载链接;未配置时统一返回/api/download?...同源相对路径,外部上传调用方如需绝对链接必须配置此项DOWNLOAD_LINK_SECRET:可选,服务器下载链接 HMAC 签名密钥,至少 32 字节;不配置时安全复用已校验的APP_SESSION_SECRETOSS_PUBLIC_BASE_URL:旧版兼容配置,当前不参与服务器下载链接生成,可留空OSS_PREFIX:限制管理范围,例如private/UPLOAD_MAX_BYTES:全局上传大小上限,默认100MB,支持KB/MB/GB后缀DEDUPE_MYSQL_DSN:素材查重使用的 MySQL DSN,例如user:pass@tcp(host:3306)/db?parseTime=true&charset=utf8mb4DOWNLOAD_QUOTA_MYSQL_DSN:下载额度持久化使用的 MySQL DSN;留空时复用DEDUPE_MYSQL_DSN。额度数据会跨重启、跨多实例共享DOWNLOAD_DAILY_LIMIT_BYTES:每个文件每日成功传输的全局字节上限,0表示默认不限。设置非零全局限额时,必须确保DOWNLOAD_QUOTA_MYSQL_DSN(或其回退的DEDUPE_MYSQL_DSN)已配置且可连接DOWNLOAD_QUOTA_TIMEZONE:额度日期所用时区,默认Asia/Shanghai
后端:
$env:GOPROXY='https://goproxy.cn,direct'
go mod tidy
go run ./cmd/server前端:
cd web
npm install --registry=https://registry.npmmirror.com
npm run dev前端开发服务器默认代理 /api 到 http://localhost:8080。
cd web
npm install --registry=https://registry.npmmirror.com
npm run build
cd ..
go build -o bin/oss-files.exe ./cmd/server构建后运行 bin/oss-files.exe,访问 http://localhost:8080。
本地构建镜像:
docker build -t oss-files:local .运行:
docker run -d --name oss-files --restart unless-stopped \
--env-file .env \
-p 8080:8080 \
oss-files:local也可以使用 docker-compose.yml,先把镜像名改成你的 Docker Hub 镜像地址:
image: your-dockerhub-username/oss-files:latest然后运行:
docker compose up -d仓库已包含 .github/workflows/docker-image.yml。推送到 main / master 或推送 v* tag 时,会自动构建镜像。
默认会推送到 GitHub Container Registry:
ghcr.io/<owner>/<repo>:latest
ghcr.io/<owner>/<repo>:main
ghcr.io/<owner>/<repo>:sha-xxxxxxx
ghcr.io/<owner>/<repo>:v1.0.0
如果阿里云服务器访问不了 GitHub/GHCR,可以在 GitHub 仓库 Settings -> Secrets and variables -> Actions 里配置 Docker Hub:
DOCKERHUB_USERNAME=你的 Docker Hub 用户名
DOCKERHUB_TOKEN=你的 Docker Hub Access Token
配置后 workflow 会额外推送到 Docker Hub:
docker.io/<dockerhub-username>/<repo>:latest
docker.io/<dockerhub-username>/<repo>:main
docker.io/<dockerhub-username>/<repo>:sha-xxxxxxx
docker.io/<dockerhub-username>/<repo>:v1.0.0
阿里云服务器上使用 Docker Hub 镜像:
docker compose pull
docker compose up -d镜像不包含 .env,部署时需要通过环境变量或 --env-file .env 注入配置。
最简单的上传接口不需要先登录,请配置 .env 里的 UPLOAD_API_TOKEN,然后直接发一个 HTTP 请求:
curl -X PUT "http://localhost:8080/api/upload/docs/demo.txt?token=your-upload-token" \
--data-binary @demo.txt请求体就是文件内容,URL 里的 docs/demo.txt 就是 OSS 对象 Key。
在上传请求中追加 permanent=1,可让返回的 url 使用永久服务器签名下载链接:
curl -X PUT "http://localhost:8080/api/upload/docs/demo.txt?token=your-upload-token&permanent=1" \
--data-binary @demo.txt如果希望“同样内容的素材不重复上传”,需要配置 DEDUPE_MYSQL_DSN,并在上传时显式传 dedupe=1。未传 dedupe=1 时仍按普通上传流程执行:
curl -X PUT "http://localhost:8080/api/upload/docs/demo.txt?token=your-upload-token&dedupe=1" \
--data-binary @demo.txt命中查重时不会再次调用 OSS 上传,直接返回第一次保存的素材地址,并带上 deduplicated: true。
也可以不把 token 放 URL,改用请求头:
curl -X PUT "http://localhost:8080/api/upload/docs/demo.txt" \
-H "X-Upload-Token: your-upload-token" \
--data-binary @demo.txt或者:
curl -X PUT "http://localhost:8080/api/upload/docs/demo.txt" \
-H "Authorization: Bearer your-upload-token" \
--data-binary @demo.txt返回:
{
"key": "docs/demo.txt",
"fullKey": "base/docs/demo.txt",
"name": "demo.txt",
"size": 12345,
"etag": "...",
"requestId": "...",
"url": "https://files.example.com/api/download?key=docs%2Fdemo.txt&expires=...&sig=...",
"expiration": "2026-06-26T07:00:00Z",
"expiresIn": 3600,
"permanent": false
}未传 permanent=1 时,url 是默认有效期 3600 秒的临时服务器签名链接;expires 只接受 60 到 604800 秒。客户端访问该链接时由服务器使用 OSS 凭据读取对象并流式转发,签名链接不需要管理台 Cookie。
上传时传入 permanent=1 的返回示例:
{
"key": "docs/demo.txt",
"fullKey": "base/docs/demo.txt",
"name": "demo.txt",
"size": 12345,
"etag": "...",
"requestId": "...",
"url": "https://files.example.com/api/download?key=docs%2Fdemo.txt&permanent=1&sig=...",
"expiration": null,
"expiresIn": null,
"permanent": true
}永久只表示不按时间过期,服务器仍会校验 HMAC;更换 DOWNLOAD_LINK_SECRET 或删除对象即可使链接失效。若同一个 Key 被替换,旧永久链接会访问该 Key 的新内容;需要不可变分享语义时请使用不可复用的 Key。
管理台内部仍保留 multipart 上传接口,登录后使用 Cookie 调用:
curl -b cookie.txt -F "file=@demo.txt" -F "key=docs/demo.txt" http://localhost:8080/api/objects管理台上传也可以用表单字段启用查重:
curl -b cookie.txt -F "file=@demo.txt" -F "key=docs/demo.txt" -F "dedupe=1" http://localhost:8080/api/objects如果不传 key,后端会使用上传文件名;如果配置了 OSS_PREFIX,对象会自动限定在该前缀下。
所有接口都在 /api 下,除登录和带签名的 /api/download 外都需要 oss_files_session Cookie。
例外:PUT /api/upload/... 是简单上传接口,只需要 UPLOAD_API_TOKEN。
POST /api/login
请求:
{
"username": "admin",
"password": "your-password"
}返回:
{
"username": "admin"
}GET /api/objects?prefix=docs/&limit=100&token=...
返回中的 isPrefix: true 表示 OSS 前缀目录项,可以继续用它的 key 作为 prefix 查询下一级。
{
"objects": [
{
"key": "docs/images/",
"fullKey": "base/docs/images/",
"name": "images/",
"isPrefix": true,
"size": 0
},
{
"key": "docs/demo.txt",
"fullKey": "base/docs/demo.txt",
"name": "demo.txt",
"isPrefix": false,
"size": 12345,
"etag": "...",
"lastModified": "2026-06-26T06:00:00Z",
"storageClass": "Standard",
"contentType": "text/plain"
}
],
"nextToken": "",
"truncated": false,
"prefix": "docs/"
}POST /api/objects
格式:multipart/form-data
file:必填,文件字段key:可选,OSS 对象 Key,例如docs/demo.txtdedupe:可选,传1时启用 MySQL 素材查重;不传时普通上传permanent:可选,传1时返回永久服务器签名下载链接;不传时返回默认 3600 秒的临时链接
临时链接返回:
{
"key": "docs/demo.txt",
"fullKey": "base/docs/demo.txt",
"etag": "...",
"requestId": "...",
"url": "https://files.example.com/api/download?key=docs%2Fdemo.txt&expires=...&sig=...",
"permanent": false,
"expiration": "2026-06-26T07:00:00Z",
"expiresIn": 3600
}传入 permanent=1 时,返回永久链接及对应字段:
{
"key": "docs/demo.txt",
"fullKey": "base/docs/demo.txt",
"etag": "...",
"requestId": "...",
"url": "https://files.example.com/api/download?key=docs%2Fdemo.txt&permanent=1&sig=...",
"permanent": true,
"expiration": null,
"expiresIn": null
}GET /api/objects/raw?key=docs/demo.txt:以内联方式由服务器流式返回 OSS 文件内容,需要登录 CookieGET /api/objects/download?key=docs/demo.txt:以附件方式由服务器流式返回 OSS 文件内容,需要登录 CookieGET /api/download?key=docs/demo.txt&expires=...&sig=...:临时服务器签名下载,校验签名后由服务器流式返回附件,不需要登录 Cookie;expires限制为 60 到 604800 秒,默认 3600 秒GET /api/download?key=docs/demo.txt&permanent=1&sig=...:永久服务器签名下载,不按时间过期但每次仍需有效 HMAC;更换DOWNLOAD_LINK_SECRET或删除对象可使链接失效GET /api/objects/meta?key=docs/demo.txt:返回文件元数据 JSONGET /api/objects/link?key=docs/demo.txt&expires=3600:生成临时服务器签名下载链接GET /api/objects/link?key=docs/demo.txt&permanent=1:生成永久服务器签名下载链接。临时响应的expiration为过期时间、expiresIn为秒数;永久响应中两者均为null,且permanent为truepermanent=1与expires同时传入时返回400,永久链接不带expiresDELETE /api/objects?key=docs/demo.txt:删除文件,成功返回{"ok": true}DELETE /api/objects?key=docs/:递归删除目录下的所有文件和子目录,成功返回{"ok": true, "deleted": 12}
每个文件按配置的日期时区(DOWNLOAD_QUOTA_TIMEZONE,默认 Asia/Shanghai)分别统计每日额度。额度数据存储在 MySQL 中,因此可跨进程重启、跨多实例共享;DOWNLOAD_QUOTA_MYSQL_DSN 为空时回退到 DEDUPE_MYSQL_DSN。如果设置了非零的 DOWNLOAD_DAILY_LIMIT_BYTES,必须提供可用的额度 MySQL DSN。
额度按服务器实际成功写给客户端的字节数计入。开始完整文件下载前,服务端会原子预留本次所需额度;额度不足时直接返回 429,不会下载到一半才失败。客户端中断连接时,尚未传出的字节会退回额度。/api/objects/raw、登录后的 /api/objects/download 和带签名的 /api/download 均计入额度。
管理员可按文件查看或覆盖每日限额:
GET /api/objects/download-limit?key=docs/demo.txt:查看该文件当前限额、来源和用量PUT /api/objects/download-limit?key=docs/demo.txt:请求体为{"dailyLimitBytes": N};N > 0设置该文件覆盖值,N = 0恢复全局默认限额DELETE /api/objects/download-limit?key=docs/demo.txt:删除该文件覆盖值,恢复全局默认限额
响应包含 source、usageDate、usedBytes、remainingBytes 等字段;source 为 file(单文件覆盖)、default(全局默认)或 unlimited(不限),例如:
{
"key": "docs/demo.txt",
"dailyLimitBytes": 104857600,
"source": "file",
"usageDate": "2026-06-26",
"usedBytes": 12345,
"remainingBytes": 104845255
}