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", })