Skip to content

Commit de64bff

Browse files
committed
fix: webdav profind响应 && XML格式解析bug
1 parent ad9368e commit de64bff

File tree

9 files changed

+78
-68
lines changed

9 files changed

+78
-68
lines changed

backend/src/storage/drivers/s3/operations/S3BackendMultipartOperations.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export class S3BackendMultipartOperations {
6161
const createDirParams = {
6262
Bucket: this.config.bucket_name,
6363
Key: parentPath,
64-
Body: "", // 空内容
64+
Body: Buffer.from("", "utf-8"),
6565
ContentType: "application/x-directory", // 目录内容类型
6666
};
6767

backend/src/storage/drivers/s3/operations/S3BatchOperations.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,7 @@ export class S3BatchOperations {
440440
const createDirParams = {
441441
Bucket: s3Config.bucket_name,
442442
Key: parentPath,
443-
Body: "", // 空内容
443+
Body: Buffer.from("", "utf-8"),
444444
ContentType: "application/x-directory", // 目录内容类型
445445
};
446446

backend/src/storage/drivers/s3/operations/S3DirectoryOperations.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,7 @@ export class S3DirectoryOperations {
355355
const putParams = {
356356
Bucket: this.config.bucket_name,
357357
Key: s3SubPath,
358-
Body: "",
358+
Body: Buffer.from("", "utf-8"),
359359
ContentType: "application/x-directory",
360360
};
361361

backend/src/storage/drivers/s3/operations/S3FileOperations.js

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { handleFsError } from "../../../fs/utils/ErrorHandler.js";
1212
import { updateParentDirectoriesModifiedTime } from "../utils/S3DirectoryUtils.js";
1313
import { CAPABILITIES } from "../../../interfaces/capabilities/index.js";
1414
import { GetFileType, getFileTypeName } from "../../../../utils/fileTypeDetector.js";
15+
import { FILE_TYPES, FILE_TYPE_NAMES } from "../../../../constants/index.js";
1516

1617
export class S3FileOperations {
1718
/**
@@ -140,16 +141,20 @@ export class S3FileOperations {
140141

141142
// 构建文件信息对象
142143
const fileName = path.split("/").filter(Boolean).pop() || "/";
143-
const fileType = await GetFileType(fileName, db);
144-
const fileTypeName = await getFileTypeName(fileName, db);
144+
145+
// 检查是否为目录:基于ContentType判断
146+
const isDirectory = headResponse.ContentType === "application/x-directory";
147+
148+
const fileType = isDirectory ? FILE_TYPES.FOLDER : await GetFileType(fileName, db);
149+
const fileTypeName = isDirectory ? FILE_TYPE_NAMES[FILE_TYPES.FOLDER] : await getFileTypeName(fileName, db);
145150

146151
const result = {
147152
path: path,
148153
name: fileName,
149-
isDirectory: false,
150-
size: headResponse.ContentLength || 0,
154+
isDirectory: isDirectory,
155+
size: isDirectory ? 0 : headResponse.ContentLength || 0, // 目录大小为0
151156
modified: headResponse.LastModified ? headResponse.LastModified.toISOString() : new Date().toISOString(),
152-
mimetype: headResponse.ContentType || "application/octet-stream",
157+
mimetype: headResponse.ContentType || "application/octet-stream",
153158
etag: headResponse.ETag ? headResponse.ETag.replace(/"/g, "") : undefined,
154159
mount_id: mount.id,
155160
storage_type: mount.storage_type,
@@ -214,14 +219,18 @@ export class S3FileOperations {
214219
const getResponse = await this.s3Client.send(getCommand);
215220

216221
const fileName = path.split("/").filter(Boolean).pop() || "/";
217-
const fileType = await GetFileType(fileName, db);
218-
const fileTypeName = await getFileTypeName(fileName, db);
222+
223+
// 检查是否为目录:基于ContentType判断
224+
const isDirectory = getResponse.ContentType === "application/x-directory";
225+
226+
const fileType = isDirectory ? FILE_TYPES.FOLDER : await GetFileType(fileName, db);
227+
const fileTypeName = isDirectory ? FILE_TYPE_NAMES[FILE_TYPES.FOLDER] : await getFileTypeName(fileName, db);
219228

220229
const result = {
221230
path: path,
222231
name: fileName,
223-
isDirectory: false,
224-
size: getResponse.ContentLength || 0,
232+
isDirectory: isDirectory,
233+
size: isDirectory ? 0 : getResponse.ContentLength || 0, // 目录大小为0
225234
modified: getResponse.LastModified ? getResponse.LastModified.toISOString() : new Date().toISOString(),
226235
mimetype: getResponse.ContentType || "application/octet-stream", // 统一使用mimetype字段名
227236
etag: getResponse.ETag ? getResponse.ETag.replace(/"/g, "") : undefined,
@@ -426,7 +435,7 @@ export class S3FileOperations {
426435
success: true,
427436
path: s3SubPath,
428437
etag: result.ETag ? result.ETag.replace(/"/g, "") : undefined,
429-
mimetype: contentType,
438+
mimetype: contentType,
430439
message: "文件更新成功",
431440
isNewFile: !originalMetadata,
432441
};

backend/src/storage/drivers/s3/utils/S3DirectoryUtils.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ export async function createS3DirectoryMarker(s3Client, bucketName, directoryPat
179179
const putParams = {
180180
Bucket: bucketName,
181181
Key: s3Key,
182-
Body: "",
182+
Body: Buffer.from("", "utf-8"),
183183
ContentType: "application/x-directory",
184184
Metadata: {
185185
"last-modified": new Date().toISOString(),

backend/src/utils/database.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -678,6 +678,8 @@ async function migrateFilesTableToMultiStorage(db) {
678678
* @param {D1Database} db - D1数据库实例
679679
*/
680680
async function rebuildFilesTable(db) {
681+
console.log("开始重建files表结构...");
682+
681683
// 创建新表结构
682684
await db
683685
.prepare(

backend/src/webdav/methods/propfind.js

Lines changed: 28 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -368,15 +368,13 @@ function getPropertiesForRequest(item, path, requestInfo) {
368368
* @returns {string} Multi-Status XML
369369
*/
370370
function buildMultiStatusXML(responses) {
371-
let xml = `<?xml version="1.0" encoding="utf-8"?>
372-
<D:multistatus xmlns:D="DAV:">`;
371+
let xml = `<?xml version="1.0" encoding="utf-8"?>\n<D:multistatus xmlns:D="DAV:">`;
373372

374373
for (const response of responses) {
375374
xml += buildResponseXML(response);
376375
}
377376

378-
xml += `
379-
</D:multistatus>`;
377+
xml += `\n</D:multistatus>`;
380378

381379
return xml;
382380
}
@@ -501,6 +499,19 @@ function getStatusText(status) {
501499
* @returns {Response} HTTP响应对象
502500
*/
503501
function createErrorResponse(href, status, message) {
502+
// 404等错误,直接返回对应的状态码
503+
// 客户端才能正确理解资源不存在后继续后续操作(如PUT上传)
504+
if (status === 404) {
505+
return new Response(message, {
506+
status: 404,
507+
headers: {
508+
"Content-Type": "text/plain; charset=utf-8",
509+
DAV: "1, 2",
510+
},
511+
});
512+
}
513+
514+
// 对于其他错误,使用Multi-Status格式
504515
const xml = buildMultiStatusXML([
505516
{
506517
href: href,
@@ -510,9 +521,9 @@ function createErrorResponse(href, status, message) {
510521
]);
511522

512523
return new Response(xml, {
513-
status: 207, // Multi-Status
524+
status: status >= 400 ? status : 207,
514525
headers: {
515-
"Content-Type": "application/xml; charset=utf-8",
526+
"Content-Type": "text/xml; charset=utf-8",
516527
DAV: "1, 2",
517528
},
518529
});
@@ -670,7 +681,7 @@ async function handleVirtualDirectoryPropfind(mounts, path, basicPath, requestIn
670681
return new Response(xml, {
671682
status: 207, // Multi-Status
672683
headers: {
673-
"Content-Type": "application/xml; charset=utf-8",
684+
"Content-Type": "text/xml; charset=utf-8",
674685
DAV: "1, 2",
675686
},
676687
});
@@ -707,53 +718,14 @@ async function handleStoragePropfind(fileSystem, path, requestInfo, userIdOrInfo
707718
items: [], // depth=0时不包含子项
708719
};
709720
} catch (error) {
710-
// nginx风格便利功能:如果资源不存在,尝试自动创建目录
711-
if (error.status === 404) {
712-
console.log(`WebDAV PROPFIND - 资源不存在,尝试自动创建目录: ${path}`);
713-
try {
714-
await fileSystem.createDirectory(path, userIdOrInfo, actualUserType);
715-
console.log(`WebDAV PROPFIND - 自动创建目录成功: ${path}`);
716-
717-
// 重新获取文件信息
718-
const fileInfo = await fileSystem.getFileInfo(path, userIdOrInfo, actualUserType);
719-
result = {
720-
path: path,
721-
isDirectory: fileInfo.isDirectory,
722-
name: fileInfo.name,
723-
size: fileInfo.size,
724-
modified: fileInfo.modified,
725-
created: fileInfo.created,
726-
items: [],
727-
};
728-
} catch (createError) {
729-
console.log(`WebDAV PROPFIND - 自动创建目录失败,返回原始404错误: ${createError.message}`);
730-
throw error; // 返回原始的404错误
731-
}
732-
} else {
733-
throw error;
734-
}
721+
throw error;
735722
}
736723
} else if (requestInfo.depth === "1") {
737724
// 获取当前资源和直接子项
738725
try {
739726
result = await fileSystem.listDirectory(path, userIdOrInfo, actualUserType);
740727
} catch (error) {
741-
// 风格便利功能:如果目录不存在,尝试自动创建
742-
if (error.status === 404) {
743-
console.log(`WebDAV PROPFIND - 目录不存在,尝试自动创建: ${path}`);
744-
try {
745-
await fileSystem.createDirectory(path, userIdOrInfo, actualUserType);
746-
console.log(`WebDAV PROPFIND - 自动创建目录成功: ${path}`);
747-
748-
// 重新列出目录
749-
result = await fileSystem.listDirectory(path, userIdOrInfo, actualUserType);
750-
} catch (createError) {
751-
console.log(`WebDAV PROPFIND - 自动创建目录失败,返回原始404错误: ${createError.message}`);
752-
throw error; // 返回原始的404错误
753-
}
754-
} else {
755-
throw error;
756-
}
728+
throw error;
757729
}
758730
} else {
759731
// infinity深度 - 大多数服务器会拒绝此请求
@@ -797,10 +769,17 @@ async function handleStoragePropfind(fileSystem, path, requestInfo, userIdOrInfo
797769

798770
const xml = buildMultiStatusXML(responses);
799771

772+
// // 调试:输出完整的XML内容(仅在depth=0时,避免过多日志)
773+
// if (requestInfo.depth === "0") {
774+
// console.log(`WebDAV PROPFIND 完整XML响应:\n${xml}`);
775+
// } else {
776+
// console.log(`WebDAV PROPFIND XML响应预览: ${xml.substring(0, 200)}...`);
777+
// }
778+
800779
return new Response(xml, {
801780
status: 207, // Multi-Status
802781
headers: {
803-
"Content-Type": "application/xml; charset=utf-8",
782+
"Content-Type": "text/xml; charset=utf-8",
804783
DAV: "1, 2",
805784
},
806785
});

backend/src/webdav/methods/put.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,25 @@ export async function handlePut(c, path, userId, userType, db) {
6969
const mountManager = new MountManager(db, encryptionSecret);
7070
const fileSystem = new FileSystem(mountManager);
7171

72+
// 在PUT时自动创建父目录
73+
const parentPath = path.substring(0, path.lastIndexOf("/"));
74+
if (parentPath && parentPath !== "/" && parentPath !== "") {
75+
try {
76+
console.log(`WebDAV PUT - 确保父目录存在: ${parentPath}`);
77+
await fileSystem.createDirectory(parentPath, userId, userType);
78+
console.log(`WebDAV PUT - 父目录已确保存在: ${parentPath}`);
79+
} catch (error) {
80+
// 如果目录已存在(409 Conflict),这是正常情况,继续上传
81+
if (error.status === 409 || error.message?.includes("已存在") || error.message?.includes("exists")) {
82+
console.log(`WebDAV PUT - 父目录已存在,继续上传: ${parentPath}`);
83+
} else {
84+
// 其他错误(如权限不足、存储空间不足等)应该阻止上传
85+
console.error(`WebDAV PUT - 创建父目录失败: ${error.message}`);
86+
throw error;
87+
}
88+
}
89+
}
90+
7291
// 获取WebDAV上传模式设置
7392
const uploadMode = await getWebDAVUploadMode(db);
7493
console.log(`WebDAV PUT - 使用配置的上传模式: ${uploadMode}`);

backend/src/webdav/utils/lockUtils.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export function parseLockXML(xmlBody) {
6969
throw new Error("无效的LOCK请求:缺少lockinfo元素");
7070
}
7171

72-
// 解析锁定范围
72+
// 解析锁定范围
7373
let scope = "exclusive"; // 默认为exclusive
7474
const scopeElement = findElementByLocalName(lockinfo, "lockscope");
7575
if (scopeElement) {
@@ -193,19 +193,20 @@ export function buildLockResponseXML(path, lockInfo) {
193193

194194
/**
195195
* 构建lockdiscovery属性XML(用于PROPFIND)
196+
* 注意:只返回内容,不包含外层<D:lockdiscovery>标签,因为会被PROPFIND自动包装
196197
* @param {Object} lockInfo - 锁定信息
197-
* @returns {string} lockdiscovery XML
198+
* @returns {string} lockdiscovery内容XML
198199
*/
199200
export function buildLockDiscoveryXML(lockInfo) {
200201
if (!lockInfo) {
201-
return "<D:lockdiscovery/>";
202+
return ""; // 返回空内容,外层会生成<D:lockdiscovery/>
202203
}
203204

204205
const token = escapeXmlChars(lockInfo.token);
205206
const owner = escapeXmlChars(lockInfo.owner);
206207
const timeout = `Second-${lockInfo.timeoutSeconds}`;
207208

208-
return `<D:lockdiscovery>
209+
return `
209210
<D:activelock>
210211
<D:locktype><D:${lockInfo.type}/></D:locktype>
211212
<D:lockscope><D:${lockInfo.scope}/></D:lockscope>
@@ -216,7 +217,7 @@ export function buildLockDiscoveryXML(lockInfo) {
216217
<D:href>${token}</D:href>
217218
</D:locktoken>
218219
</D:activelock>
219-
</D:lockdiscovery>`;
220+
`;
220221
}
221222

222223
/**

0 commit comments

Comments
 (0)