Skip to content
Merged
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
38 changes: 9 additions & 29 deletions packages/filesystem/webdav/webdav.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,24 +70,19 @@ describe("WebDAVFileSystem", () => {
});

describe("verify", () => {
it("应当通过列目录、写入探针文件和清理探针完成验证", async () => {
it("应当通过 getQuota 与列目录完成只读校验", async () => {
const fs = createTestFS(mockClient);

await expect(fs.verify()).resolves.toBeUndefined();
expect(mockClient.getQuota).toHaveBeenCalled();
expect(mockClient.getDirectoryContents).toHaveBeenCalledWith("/");
expect(mockClient.createDirectory).toHaveBeenCalledWith(expect.stringMatching(/^\/\.scriptcat-verify-/));
expect(mockClient.putFileContents).toHaveBeenCalledWith(
expect.stringMatching(/^\/\.scriptcat-verify-.+\/probe\.txt$/),
""
);
expect(mockClient.deleteFile).toHaveBeenCalledWith(
expect.stringMatching(/^\/\.scriptcat-verify-.+\/probe\.txt$/)
);
expect(mockClient.deleteFile).toHaveBeenCalledWith(expect.stringMatching(/^\/\.scriptcat-verify-/));
// 不应在 verify 阶段尝试写探针(坚果云等根目录不可写的服务会被误杀)
expect(mockClient.createDirectory).not.toHaveBeenCalled();
expect(mockClient.putFileContents).not.toHaveBeenCalled();
expect(mockClient.deleteFile).not.toHaveBeenCalled();
});

it("应当在 401 时抛出 WarpTokenError 1", async () => {
it("应当在 getQuota 401 时抛出 WarpTokenError", async () => {
(mockClient.getQuota as ReturnType<typeof vi.fn>).mockRejectedValue({
response: { status: 401 },
message: "Unauthorized",
Expand All @@ -97,7 +92,7 @@ describe("WebDAVFileSystem", () => {
await expect(fs.verify()).rejects.toBeInstanceOf(WarpTokenError);
});

it("应当在 401 时抛出 WarpTokenError 2", async () => {
it("应当在 getDirectoryContents 401 时抛出 WarpTokenError", async () => {
(mockClient.getDirectoryContents as ReturnType<typeof vi.fn>).mockRejectedValue({
response: { status: 401 },
message: "Unauthorized",
Expand All @@ -107,7 +102,7 @@ describe("WebDAVFileSystem", () => {
await expect(fs.verify()).rejects.toBeInstanceOf(WarpTokenError);
});

it("应当在其他错误时抛出包含原始信息的 Error 1", async () => {
it("应当在 getQuota 其他错误时抛出包含原始信息的 Error", async () => {
(mockClient.getQuota as ReturnType<typeof vi.fn>).mockRejectedValue({
message: "Network error",
});
Expand All @@ -116,29 +111,14 @@ describe("WebDAVFileSystem", () => {
await expect(fs.verify()).rejects.toThrow("WebDAV verify failed: Network error");
});

it("应当在其他错误时抛出包含原始信息的 Error 2", async () => {
it("应当在 getDirectoryContents 其他错误时抛出包含原始信息的 Error", async () => {
(mockClient.getDirectoryContents as ReturnType<typeof vi.fn>).mockRejectedValue({
message: "Network error",
});
const fs = createTestFS(mockClient);

await expect(fs.verify()).rejects.toThrow("WebDAV verify failed: Network error");
});

it("应当在无法写入探针文件时验证失败并清理探针目录", async () => {
(mockClient.putFileContents as ReturnType<typeof vi.fn>).mockResolvedValue(false);
const fs = createTestFS(mockClient);

await expect(fs.verify()).rejects.toThrow("WebDAV verify failed: probe file write returned false");
expect(mockClient.deleteFile).toHaveBeenCalledWith(expect.stringMatching(/^\/\.scriptcat-verify-/));
});

it("应当在删除探针文件失败时验证失败", async () => {
(mockClient.deleteFile as ReturnType<typeof vi.fn>).mockRejectedValueOnce(new Error("Delete denied"));
const fs = createTestFS(mockClient);

await expect(fs.verify()).rejects.toThrow("WebDAV verify failed: Delete denied");
});
});

describe("openDir", () => {
Expand Down
29 changes: 3 additions & 26 deletions packages/filesystem/webdav/webdav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,43 +53,20 @@ export default class WebDAVFileSystem implements FileSystem {
}

async verify(): Promise<void> {
const verifyDir = joinPath(this.basePath, `.scriptcat-verify-${Date.now()}-${Math.random().toString(36).slice(2)}`);
const verifyFile = joinPath(verifyDir, "probe.txt");
let dirCreated = false;
let fileCreated = false;

// 只做只读校验:凭据 + URL 可达性。
// 写权限不在此处探测——不同 basePath 写策略不同(坚果云等根目录不可写的服务会被误杀,见 #1444),
// 真正的写操作会在 backupToCloud / buildFileSystem 中由 createDir 立即触发并报错。
try {
await this.client.getQuota();
await this.client.getDirectoryContents(this.basePath);
await this.client.createDirectory(verifyDir);
dirCreated = true;
const written = await this.client.putFileContents(verifyFile, "");
if (!written) {
throw new Error("probe file write returned false");
}
fileCreated = true;
await this.client.deleteFile(verifyFile);
fileCreated = false;
await this.client.deleteFile(verifyDir);
dirCreated = false;
} catch (e: any) {
await this.cleanupVerifyProbe(verifyFile, verifyDir, fileCreated, dirCreated);
if (e.response?.status === 401) {
throw new WarpTokenError(e);
}
throw new Error(`WebDAV verify failed: ${e.message}`); // 保留原始信息
}
}

private async cleanupVerifyProbe(verifyFile: string, verifyDir: string, fileCreated: boolean, dirCreated: boolean) {
if (fileCreated) {
await this.client.deleteFile(verifyFile).catch(() => undefined);
}
if (dirCreated) {
await this.client.deleteFile(verifyDir).catch(() => undefined);
}
}

async open(file: FileInfo): Promise<FileReader> {
return new WebDAVFileReader(this.client, joinPath(file.path, file.name));
}
Expand Down
Loading