From 236ccf8ed51fed28d7307e4f0d0b0f4358ae3ec7 Mon Sep 17 00:00:00 2001 From: dsvzb <17796147652@163.com> Date: Sun, 4 Oct 2026 23:19:00 +0800 Subject: [PATCH] =?UTF-8?q?fix(webdav/driver):=20href=20=E9=9C=80=E5=B8=A6?= =?UTF-8?q?=E6=8C=82=E8=BD=BD=E5=89=8D=E7=BC=80=EF=BC=9B=E5=85=89=E9=B8=AD?= =?UTF-8?q?=E4=BA=91=E7=9B=98=E4=B8=8A=E4=BC=A0=E5=87=AD=E6=8D=AE=E5=BA=94?= =?UTF-8?q?=E8=AF=BB=20data.creds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PROPFIND 返回的 D:href 由 davPath 拼出,而 davPathOf() 已经把 /dav 挂载 前缀剥掉。RFC 4918 要求 D:href 是完整请求 URI,因此按请求路径解析 href 的客户端 (rclone、Windows 资源管理器、RaiDrive 等)会丢弃全部条目,表现为目录为空。 改为从请求 URL 推导前缀,而不是硬编码 /dav —— index.ts 还会把发往任意路径的 WebDAV 方法交给同一个 router,此时前缀为空。 GuangYaPan 的 get_res_center_token 把 OSS 凭据嵌套在 data.creds 下,而类型定义 与校验都按平铺在 data 上读取,于是 accessKeyID / secretAccessKey 恒为 undefined, 所有上传都抛 "upload token is incomplete"。改为优先取 creds 并保留平铺回退,同时 把 OSS 签名与 x-oss-security-token 头也改用取到的局部变量。 --- src/backend/drivers/guangyapan/driver.ts | 24 ++++++++++++------ src/backend/drivers/guangyapan/types.ts | 19 ++++++++++++--- src/backend/server/webdav.ts | 31 ++++++++++++++++++------ 3 files changed, 56 insertions(+), 18 deletions(-) diff --git a/src/backend/drivers/guangyapan/driver.ts b/src/backend/drivers/guangyapan/driver.ts index a7a72383..39ba77c4 100644 --- a/src/backend/drivers/guangyapan/driver.ts +++ b/src/backend/drivers/guangyapan/driver.ts @@ -313,12 +313,20 @@ export class GuangYaPanDriver implements StorageDriver { return } + // 实时接口把 OSS 临时凭据嵌套在 data.creds 下,而不是平铺在 data 上。 + // 早期实现按平铺读取,导致 accessKeyID/secretAccessKey 恒为 undefined, + // 上传一律抛 "upload token is incomplete"。这里优先取 creds,并保留平铺回退。 + const creds = token?.creds || ({} as any) + const accessKeyID = creds.accessKeyID || token?.accessKeyID || "" + const secretAccessKey = creds.secretAccessKey || token?.secretAccessKey || "" + const sessionToken = creds.sessionToken || token?.sessionToken || "" + if ( !token.objectPath || !token.bucketName || !token.endPoint || - !token.accessKeyID || - !token.secretAccessKey + !accessKeyID || + !secretAccessKey ) { throw new Error("upload token is incomplete") } @@ -329,22 +337,22 @@ export class GuangYaPanDriver implements StorageDriver { const dateStr = new Date().toUTCString() const contentType = "application/octet-stream" - const ossHeaders = token.sessionToken - ? `x-oss-security-token:${token.sessionToken}\n` + const ossHeaders = sessionToken + ? `x-oss-security-token:${sessionToken}\n` : "" const canonicalizedResource = `/${token.bucketName}/${token.objectPath.replace(/^\/+/, "")}` const stringToSign = `PUT\n\n${contentType}\n${dateStr}\n${ossHeaders}${canonicalizedResource}` - const signature = await hmacSha1Base64(stringToSign, token.secretAccessKey) - const authorization = `OSS ${token.accessKeyID}:${signature}` + const signature = await hmacSha1Base64(stringToSign, secretAccessKey) + const authorization = `OSS ${accessKeyID}:${signature}` const headers: Record = { "Content-Type": contentType, Date: dateStr, Authorization: authorization, } - if (token.sessionToken) { - headers["x-oss-security-token"] = token.sessionToken + if (sessionToken) { + headers["x-oss-security-token"] = sessionToken } const res = await fetch(uploadURL, { diff --git a/src/backend/drivers/guangyapan/types.ts b/src/backend/drivers/guangyapan/types.ts index 3760fc09..a6f5f103 100644 --- a/src/backend/drivers/guangyapan/types.ts +++ b/src/backend/drivers/guangyapan/types.ts @@ -66,15 +66,28 @@ export interface GypTokenResp { error_description: string } +/** get_res_center_token 返回的 OSS 临时凭据(嵌套在 data.creds 下) */ +export interface GypUploadCreds { + accessKeyID: string + secretAccessKey: string + sessionToken: string + expiration: string +} + export interface GypUploadTokenData { taskId: string objectPath: string bucketName: string endPoint: string fullEndPoint: string - accessKeyID: string - secretAccessKey: string - sessionToken: string + /** + * 实时接口把 OSS 凭据嵌套在 creds 下,而不是平铺在 data 上。 + * 平铺字段保留为可选,仅用于兼容旧版响应。 + */ + creds?: GypUploadCreds + accessKeyID?: string + secretAccessKey?: string + sessionToken?: string } export interface GypUploadTokenResp { diff --git a/src/backend/server/webdav.ts b/src/backend/server/webdav.ts index eefbcff8..49d420c3 100644 --- a/src/backend/server/webdav.ts +++ b/src/backend/server/webdav.ts @@ -92,6 +92,21 @@ function splitPath(p: string): { dir: string; name: string } { return { dir, name } } +/** + * 推导本次请求实际使用的挂载前缀(正规挂载为 `/dav`)。 + * + * 不能直接硬编码 `/dav`:index.ts 会把**任意路径上**的 WebDAV 方法都交给本 router + * (为兼容把用户填写的地址当 WebDAV 根、对 `/` 发 PROPFIND 的客户端),此时前缀为空。 + * PROPFIND 返回的 href 必须与本请求的 URL 结构一致,客户端才能匹配上。 + */ +function davPrefixOf(c: any, davPath: string): string { + const pathname = new URL(c.req.url).pathname + const stripped = davPath === "/" ? "" : davPath + return pathname.endsWith(stripped) + ? pathname.slice(0, pathname.length - stripped.length) + : "" +} + webdavRouter.all("/*", async (c) => { const user = await webdavAuth(c) if (!user) { @@ -131,13 +146,15 @@ webdavRouter.all("/*", async (c) => { isFolder: !!it.is_dir, modified: it.modified || new Date().toISOString(), })) - const href = - davPath === "/" - ? "/" - : davPath.endsWith("/") - ? davPath - : davPath + "/" - const xml = buildWebDavPropfindResponse(href, items) + // RFC 4918: D:href must be the full request URI, so it has to carry the + // same mount prefix as the request that produced it. davPathOf() strips + // that prefix, so derive it back here — returning a bare "/wewe/..." + // makes clients (rclone, Windows Explorer, RaiDrive) treat every entry + // as an unknown path and show empty directories. + const davPrefix = davPrefixOf(c, davPath) + const hrefPath = + davPath === "/" ? davPrefix + "/" : davPrefix + davPath + "/" + const xml = buildWebDavPropfindResponse(hrefPath, items) return c.body(xml, depth === "0" ? 207 : 207, { "Content-Type": "application/xml; charset=utf-8", })