Skip to content

Commit 6575bc1

Browse files
authored
Merge branch 'ling-drag0n:main' into main
2 parents 9736bd1 + 46491ff commit 6575bc1

File tree

143 files changed

+17501
-2099
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

143 files changed

+17501
-2099
lines changed

Api-doc.md

Lines changed: 63 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -619,22 +619,22 @@ X-Custom-Auth-Key: <api_key>
619619

620620
### 公共文件查询
621621

622-
- `GET /api/public/files/:slug`
623-
- 描述:获取文件公开信息
622+
- `GET /api/share/get/:slug`
623+
- 描述:获取分享文件公开信息(控制面 JSON)
624624
- 参数:slug - 文件短链接
625625
- 授权:无需授权
626-
- 响应:包含文件基本信息(不含下载链接)
626+
- 响应:包含文件基本信息 + 语义明确的 `previewUrl`/`downloadUrl`/`linkType` 等字段;若文件需要密码且未验证,则 `previewUrl`/`downloadUrl` 为 null。
627627

628-
- `POST /api/public/files/:slug/verify`
629-
- 描述:验证文件访问密码
628+
- `POST /api/share/verify/:slug`
629+
- 描述:验证文件访问密码(成功后返回同 GET 的公开视图)
630630
- 参数:slug - 文件短链接
631631
- 请求体:
632632
```json
633633
{
634634
"password": "文件密码"
635635
}
636636
```
637-
- 响应:验证成功后返回带下载链接的文件信息
637+
- 响应:验证成功后返回包含 `previewUrl`/`downloadUrl` 的文件信息
638638

639639
### 文件管理
640640

@@ -655,7 +655,12 @@ X-Custom-Auth-Key: <api_key>
655655
- 描述:获取单个文件详情
656656
- 授权:需要管理员令牌或有文件权限的 API 密钥
657657
- 参数:id - 文件 ID
658-
- 响应:文件详细信息和下载链接
658+
- 查询参数(可选):
659+
- `include=links` - 返回链接信息(`previewUrl`/`downloadUrl`/`linkType`/`documentPreview`)
660+
- `links=true` - 兼容写法,等价 `include=links`
661+
- 响应:
662+
- 默认:仅返回文件元信息(不包含任何链接字段)
663+
- `include=links` 时:在元信息基础上额外返回 `previewUrl`(预览入口)、`downloadUrl`(下载入口)、`linkType`(direct/url_proxy/proxy)以及 `documentPreview`
659664

660665
#### 更新文件信息
661666

@@ -742,19 +747,23 @@ X-Custom-Auth-Key: <api_key>
742747
### 文件分享内容访问
743748

744749
- `GET /api/s/:slug`
745-
- 描述:访问文件分享内容(预览/下载
750+
- 描述:Share 本地代理内容入口(等价 FS 的本地代理语义),用于同源预览/下载
746751
- 参数:slug - 文件短链接
747-
- 授权:无需授权(受密码保护和过期时间限制
752+
- 授权:无需授权(受密码保护、过期时间、浏览次数限制
748753
- 查询参数:
749-
- `password` - 文件密码(如果文件受密码保护)
750-
- `mode` - 访问模式(可选):
751-
- `inline` - 预览模式(默认)
752-
- `attachment` - 下载模式
753-
- 响应:文件内容流
754-
- 说明:
755-
- 当 `use_proxy = 1` 时,通过服务器代理流式返回文件内容
756-
- 当 `use_proxy = 0` 时,使用存储直链(S3 预签名 URL 等)
757-
- 支持 Range 请求,可实现断点续传和视频流播放
754+
- `password` - 文件密码(文件受密码保护时必填)
755+
- `down` - 下载语义开关(`true`/`1` 表示下载;不传或 `false` 表示预览)
756+
- `mode` - 旧版兼容参数(`attachment`/`download` 等价 `down=true`,`inline` 等价预览;新代码不推荐使用)
757+
- 响应:同源 200 文件内容流(始终后端代理,不做 302 外跳),支持 Range 断点续传/媒体流播放
758+
- 说明:直链 / url_proxy / Worker 等决策只体现在 `/api/share/get/:slug` 返回的 `previewUrl`/`downloadUrl`/`linkType` 上;`/api/s/:slug` 永远走本地代理
759+
760+
- `GET /api/share/content/:slug`
761+
- 描述:分享文件同源内容口(文本预览/编码检测专用)
762+
- 参数:slug - 文件短链接
763+
- 授权:无需授权(受密码保护、过期时间、浏览次数限制)
764+
- 查询参数:
765+
- `password` - 文件密码(文件受密码保护时必填)
766+
- 响应:同源 200 文件内容流(预览语义),支持 Range;不做 302 外跳
758767

759768
---
760769

@@ -931,8 +940,9 @@ X-Custom-Auth-Key: <api_key>
931940
- 授权:需要管理员令牌或有文件权限的 API 密钥
932941
- 查询参数:
933942
- `path` - 要列出内容的目录路径,默认为根目录("/")
943+
- `path_token` - 路径密码验证令牌(可选;等价请求头 `X-FS-Path-Token`)
934944
- 请求头(可选):
935-
- `X-FS-Path-Token` - 路径密码验证令牌(访问受密码保护的目录时需要)
945+
- `X-FS-Path-Token` - 路径密码验证令牌(访问受密码保护的目录时需要;也可用 `path_token` 传递
936946
- 响应:目录内容列表,包含文件和子目录信息
937947

938948
### 文件操作
@@ -944,27 +954,56 @@ X-Custom-Auth-Key: <api_key>
944954
- 授权:需要管理员令牌或有文件权限的 API 密钥
945955
- 查询参数:
946956
- `path` - 文件路径
947-
- 响应:文件详细信息
957+
- `path_token` - 路径密码验证令牌(可选;等价请求头 `X-FS-Path-Token`)
958+
- 请求头(可选):
959+
- `X-FS-Path-Token` - 路径密码验证令牌(访问受密码保护的目录时需要;也可用 `path_token` 传递)
960+
- 响应:文件详细信息 + 语义明确的 `previewUrl`/`downloadUrl`/`linkType`/`documentPreview` 字段
948961

949962
#### 下载文件
950963

951964
- `GET /api/fs/download`
952-
- 描述:下载文件(强制下载
965+
- 描述:下载文件(attachment 语义,优先外部入口
953966
- 授权:需要管理员令牌或有文件权限的 API 密钥
954967
- 查询参数:
955968
- `path` - 文件路径
956-
- 响应:文件内容(下载),包含 Content-Disposition: attachment 头
969+
- `path_token` - 路径密码验证令牌(可选;等价请求头 `X-FS-Path-Token`)
970+
- 请求头(可选):
971+
- `X-FS-Path-Token` - 路径密码验证令牌(访问受密码保护的目录时需要;也可用 `path_token` 传递)
972+
- 响应:
973+
- 若可生成外部下载入口(直链 / url_proxy / 本地代理),返回 `302` 重定向到该 URL
974+
- 否则回退为同源 `200` 流式下载(支持 Range)
975+
976+
#### 获取文件内容(同源代理)
977+
978+
- `GET /api/fs/content`
979+
- 描述:同源返回 FS 文件原始内容(预览/文本编码检测专用,后端代理)
980+
- 授权:需要管理员令牌或有文件权限的 API 密钥
981+
- 查询参数:
982+
- `path` - 文件路径(必填)
983+
- `path_token` - 路径密码验证令牌(可选;等价请求头 `X-FS-Path-Token`)
984+
- 请求头(可选):
985+
- `X-FS-Path-Token` - 路径密码验证令牌(访问受密码保护的目录时需要;也可用 `path_token` 传递)
986+
- 响应:同源 `200` 文件内容流(预览语义),支持 Range;不做 `302` 外跳
957987

958988
#### 获取文件直链
959989

960990
- `GET /api/fs/file-link`
961-
- 描述:获取文件直链(预签名 URL)
991+
- 描述:获取文件外部访问链接(直链 / url_proxy / 代理入口)
962992
- 授权:需要管理员令牌或有文件权限的 API 密钥
963993
- 查询参数:
964994
- `path` - 文件路径(必填)
965995
- `expires_in` - 链接有效期(秒),默认为 604800(7 天)
966996
- `force_download` - 是否强制下载,true 或 false(默认 false)
967-
- 响应:包含预签名 URL 的对象
997+
- `path_token` - 路径密码验证令牌(可选;等价请求头 `X-FS-Path-Token`)
998+
- 请求头(可选):
999+
- `X-FS-Path-Token` - 路径密码验证令牌(访问受密码保护的目录时需要;也可用 `path_token` 传递)
1000+
- 响应:
1001+
```json
1002+
{
1003+
"url": "最终可访问 URL",
1004+
"linkType": "direct|url_proxy|proxy"
1005+
}
1006+
```
9681007

9691008
#### 创建目录
9701009

@@ -1545,5 +1584,3 @@ X-Custom-Auth-Key: <api_key>
15451584
- 搜索结果会被缓存 5 分钟
15461585

15471586
---
1548-
1549-

Api-s3_direct.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ https://{域名}/api/upload-direct/{filename}
3030
| expires_in | number || 0 | 文件过期时间(小时)。0 表示永不过期。 |
3131
| max_views | number || 0 | 文件最大查看次数。0 表示无限制。 |
3232
| remark | string ||| 文件备注信息。 |
33-
| password | string ||| 访问密码。设置后文件在公共接口中需要密码才能访问;upload-direct 响应中始终返回 rawUrl,便于受信任端使用。 |
33+
| password | string ||| 访问密码。设置后文件在公共接口中需要密码才能访问;upload-direct 响应中始终返回 previewUrl/downloadUrl,便于受信任端使用。 |
3434
| use_proxy | string || 系统设置 | 是否使用代理访问。未提供时按系统设置 `default_use_proxy` 决定(默认直链),传 "1" 强制代理,传 "0" 强制直链。 |
3535
| override | string || "false" | 是否覆盖同名 slug 的已存在文件。"true"表示覆盖,"false"表示不覆盖。只能覆盖自己创建的文件。 |
3636
| original_filename | string || "false" | 是否标记使用原始文件名。直传场景下具体命名仍由系统策略决定,此参数主要作为元数据标记。 |
@@ -71,8 +71,9 @@ https://{域名}/api/upload-direct/{filename}
7171
"max_views": null, // 最大查看次数限制
7272
"expires_at": null, // 过期时间
7373

74-
"rawUrl": "https://cdn.example.com/file/abcd12", // 最终可对外访问的 URL(直链或基于 `/api/s/:slug` 的代理 URL)
75-
"linkType": "direct", // "direct" | "proxy"
74+
"previewUrl": "https://cdn.example.com/file/abcd12", // 最终可对外预览入口(inline 语义;直链或基于 `/api/s/:slug` 的代理 URL)
75+
"downloadUrl": "https://cdn.example.com/file/abcd12", // 最终可对外下载入口(attachment 语义;可能与 previewUrl 相同)
76+
"linkType": "direct", // "direct" | "proxy" | "url_proxy"
7677
"use_proxy": 1, // 是否使用代理 (1=代理, 0=直接)
7778
"created_by": "admin:1" // 创建者信息 (admin:ID 或 apikey:ID)
7879
},

Cloudpaste-Proxy.js

Lines changed: 60 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ function isSelfRedirect(location, currentHost) {
116116
return true;
117117
}
118118
} catch {
119+
// location 不是完整 URL 时,视为相对路径,由平台按照当前 Host 解析,这种情况也认为是“自身”
119120
return true;
120121
}
121122
return false;
@@ -139,9 +140,7 @@ export async function handleProxyRequest(request, env) {
139140

140141
// 统一处理 CORS 预检请求,避免将 OPTIONS 透传到上游
141142
if (request.method === "OPTIONS" && path.startsWith("/proxy/")) {
142-
const allowHeaders =
143-
request.headers.get("Access-Control-Request-Headers") ||
144-
"Range, Content-Type";
143+
const allowHeaders = request.headers.get("Access-Control-Request-Headers") || "Range, Content-Type";
145144

146145
return new Response(null, {
147146
status: 204,
@@ -186,10 +185,34 @@ export async function handleProxyRequest(request, env) {
186185
body: JSON.stringify({ type: "fs", path: fsPath }),
187186
});
188187

189-
const linkJson = await linkResp.json().catch(() => null);
188+
// 日志
189+
let rawBody = null;
190+
let linkJson = null;
191+
try {
192+
rawBody = await linkResp.text();
193+
linkJson = rawBody ? JSON.parse(rawBody) : null;
194+
} catch {
195+
linkJson = null;
196+
}
197+
190198
const code = linkJson?.code ?? linkResp.status;
191199
const data = linkJson?.data ?? null;
192200
if (code !== 200 || !data?.url) {
201+
console.error(
202+
JSON.stringify({
203+
type: "proxy.debug",
204+
scope: "fs",
205+
reason: "link-resolve-failed",
206+
requestPath: fsPath,
207+
origin: ORIGIN,
208+
controlUrl: controlUrl.toString(),
209+
httpStatus: linkResp.status,
210+
code,
211+
// 为防止日志过大,截断响应体
212+
rawBody: rawBody && rawBody.length > 2048 ? rawBody.slice(0, 2048) + "...truncated" : rawBody,
213+
})
214+
);
215+
193216
return new Response(JSON.stringify({ code, message: "proxy link resolve failed" }), {
194217
status: 502,
195218
headers: { "content-type": "application/json;charset=UTF-8" },
@@ -221,10 +244,7 @@ export async function handleProxyRequest(request, env) {
221244
const resp = new Response(upstreamRes.body, upstreamRes);
222245
resp.headers.delete("set-cookie");
223246
resp.headers.set("Access-Control-Allow-Origin", requestOrigin);
224-
resp.headers.set(
225-
"Access-Control-Expose-Headers",
226-
"Content-Length, Content-Range, Accept-Ranges, ETag, Last-Modified, Content-Type",
227-
);
247+
resp.headers.set("Access-Control-Expose-Headers", "Content-Length, Content-Range, Accept-Ranges, ETag, Last-Modified, Content-Type");
228248
resp.headers.append("Vary", "Origin");
229249
return resp;
230250
}
@@ -251,10 +271,33 @@ export async function handleProxyRequest(request, env) {
251271
body: JSON.stringify({ type: "share", slug }),
252272
});
253273

254-
const linkJson = await linkResp.json().catch(() => null);
274+
// 分享视图同样增加详细调试日志,方便排查 502
275+
let rawBody = null;
276+
let linkJson = null;
277+
try {
278+
rawBody = await linkResp.text();
279+
linkJson = rawBody ? JSON.parse(rawBody) : null;
280+
} catch {
281+
linkJson = null;
282+
}
283+
255284
const code = linkJson?.code ?? linkResp.status;
256285
const data = linkJson?.data ?? null;
257286
if (code !== 200 || !data?.url) {
287+
console.error(
288+
JSON.stringify({
289+
type: "proxy.debug",
290+
scope: "share",
291+
reason: "link-resolve-failed",
292+
slug,
293+
origin: ORIGIN,
294+
controlUrl: controlUrl.toString(),
295+
httpStatus: linkResp.status,
296+
code,
297+
rawBody: rawBody && rawBody.length > 2048 ? rawBody.slice(0, 2048) + "...truncated" : rawBody,
298+
})
299+
);
300+
258301
return new Response(JSON.stringify({ code, message: "proxy link resolve failed" }), {
259302
status: 502,
260303
headers: { "content-type": "application/json;charset=UTF-8" },
@@ -283,10 +326,7 @@ export async function handleProxyRequest(request, env) {
283326
const resp = new Response(upstreamRes.body, upstreamRes);
284327
resp.headers.delete("set-cookie");
285328
resp.headers.set("Access-Control-Allow-Origin", requestOrigin);
286-
resp.headers.set(
287-
"Access-Control-Expose-Headers",
288-
"Content-Length, Content-Range, Accept-Ranges, ETag, Last-Modified, Content-Type",
289-
);
329+
resp.headers.set("Access-Control-Expose-Headers", "Content-Length, Content-Range, Accept-Ranges, ETag, Last-Modified, Content-Type");
290330
resp.headers.append("Vary", "Origin");
291331
return resp;
292332
}
@@ -300,7 +340,7 @@ export async function handleProxyRequest(request, env) {
300340
// - Deno Deploy :如果检测到 Deno.serve,则自动注册 HTTP 服务
301341
// - 其他 Fetch 兼容运行时:导入 handleProxyRequest 自行调用
302342

303-
// Cloudflare Workers
343+
// Cloudflare Workers / workerd 风格:默认导出 fetch 处理器
304344
export default {
305345
async fetch(request, env, ctx) {
306346
return handleProxyRequest(request, env);
@@ -310,12 +350,12 @@ export default {
310350
// Deno Deploy:若存在 Deno.serve,则自动启动服务
311351
if (typeof globalThis.Deno !== "undefined" && typeof globalThis.Deno.serve === "function") {
312352
globalThis.Deno.serve((req) =>
313-
handleProxyRequest(req, {
314-
ORIGIN: globalThis.Deno.env?.get?.("ORIGIN") ?? ORIGIN,
315-
TOKEN: globalThis.Deno.env?.get?.("TOKEN") ?? TOKEN,
316-
SIGN_SECRET: globalThis.Deno.env?.get?.("SIGN_SECRET") ?? SIGN_SECRET,
317-
WORKER_BASE: globalThis.Deno.env?.get?.("WORKER_BASE") ?? WORKER_BASE,
318-
})
353+
handleProxyRequest(req, {
354+
ORIGIN: globalThis.Deno.env?.get?.("ORIGIN") ?? ORIGIN,
355+
TOKEN: globalThis.Deno.env?.get?.("TOKEN") ?? TOKEN,
356+
SIGN_SECRET: globalThis.Deno.env?.get?.("SIGN_SECRET") ?? SIGN_SECRET,
357+
WORKER_BASE: globalThis.Deno.env?.get?.("WORKER_BASE") ?? WORKER_BASE,
358+
})
319359
);
320360
}
321361

0 commit comments

Comments
 (0)