Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 16 additions & 8 deletions src/backend/drivers/guangyapan/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand All @@ -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<string, string> = {
"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, {
Expand Down
19 changes: 16 additions & 3 deletions src/backend/drivers/guangyapan/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
31 changes: 24 additions & 7 deletions src/backend/server/webdav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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",
})
Expand Down