From 6c03013cfef16ed5d82fb920a1ccce5c1786c04c Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 6 May 2026 19:00:46 +0900 Subject: [PATCH] =?UTF-8?q?fix(sync):=20Google=20Drive=20stale=20path=20ca?= =?UTF-8?q?che=20=E5=9C=A8=20404=20=E5=90=8E=E6=B8=85=E7=90=86=E5=B9=B6?= =?UTF-8?q?=E9=87=8D=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../googledrive/googledrive.test.ts | 79 +++++++++++++++++++ .../filesystem/googledrive/googledrive.ts | 31 +++++++- packages/filesystem/googledrive/rw.ts | 13 +++ 3 files changed, 119 insertions(+), 4 deletions(-) diff --git a/packages/filesystem/googledrive/googledrive.test.ts b/packages/filesystem/googledrive/googledrive.test.ts index 2f08656bb..8f0de5859 100644 --- a/packages/filesystem/googledrive/googledrive.test.ts +++ b/packages/filesystem/googledrive/googledrive.test.ts @@ -82,6 +82,85 @@ describe("GoogleDriveFileSystem", () => { expect(requestSpy).toHaveBeenCalledTimes(1); }); + it("writer should clear stale path cache and retry once on provider 404", async () => { + const fs = new GoogleDriveFileSystem("/", "token"); + const notFoundError = new FileSystemError({ + provider: "googledrive", + message: "Parent not found", + status: 404, + notFound: true, + }); + const findFolderSpy = vi + .spyOn(fs, "findFolderByName") + .mockResolvedValueOnce({ id: "stale-base-id", name: "Base" }) + .mockResolvedValueOnce({ id: "fresh-base-id", name: "Base" }); + + await fs.ensureDirExists("/Base"); + + const writer = await fs.create("Base/file.txt"); + const findFileSpy = vi + .spyOn(fs, "findFileInDirectory") + .mockRejectedValueOnce(notFoundError) + .mockResolvedValueOnce(null); + const requestSpy = vi.spyOn(fs, "request").mockResolvedValue({}); + + await expect(writer.write("content")).resolves.toBeUndefined(); + + expect(findFolderSpy.mock.calls).toEqual([ + ["Base", "appDataFolder"], + ["Base", "appDataFolder"], + ]); + expect(findFileSpy.mock.calls).toEqual([ + ["file.txt", "stale-base-id"], + ["file.txt", "fresh-base-id"], + ]); + expect(requestSpy).toHaveBeenCalledTimes(1); + }); + + it("writer should not retry non-404 provider errors", async () => { + const fs = new GoogleDriveFileSystem("/", "token"); + const conflictError = new FileSystemError({ + provider: "googledrive", + message: "Conflict", + status: 409, + conflict: true, + }); + const writer = await fs.create("Base/file.txt"); + const ensureSpy = vi.spyOn(fs, "ensureDirExists").mockResolvedValue("base-id"); + const findFileSpy = vi.spyOn(fs, "findFileInDirectory").mockRejectedValue(conflictError); + + await expect(writer.write("content")).rejects.toBe(conflictError); + + expect(ensureSpy).toHaveBeenCalledTimes(1); + expect(findFileSpy).toHaveBeenCalledTimes(1); + }); + + it("list should clear stale path cache and retry once on provider 404", async () => { + const fs = new GoogleDriveFileSystem("/Base", "token"); + const notFoundError = new FileSystemError({ + provider: "googledrive", + message: "Folder not found", + status: 404, + notFound: true, + }); + const findFolderSpy = vi.spyOn(fs, "findFolderByName").mockResolvedValueOnce({ id: "stale-base-id", name: "Base" }); + + await fs.ensureDirExists("/Base"); + + const requestSpy = vi + .spyOn(fs, "request") + .mockRejectedValueOnce(notFoundError) + .mockResolvedValueOnce({ files: [{ id: "fresh-base-id", name: "Base" }] }) + .mockResolvedValueOnce({ files: [] }); + + await expect(fs.list()).resolves.toEqual([]); + + expect(findFolderSpy).toHaveBeenCalledTimes(1); + expect(String(requestSpy.mock.calls[0][0])).toContain("stale-base-id"); + expect(String(requestSpy.mock.calls[1][0])).toContain("name%3D'Base'"); + expect(String(requestSpy.mock.calls[2][0])).toContain("fresh-base-id"); + }); + it("request should return retry result after token refresh", async () => { await localStorageDAO.saveValue("netdisk:token:googledrive", { accessToken: "expired-token", diff --git a/packages/filesystem/googledrive/googledrive.ts b/packages/filesystem/googledrive/googledrive.ts index 27b4cfb57..b5934f20e 100644 --- a/packages/filesystem/googledrive/googledrive.ts +++ b/packages/filesystem/googledrive/googledrive.ts @@ -1,5 +1,5 @@ import { AuthVerify } from "../auth"; -import { FileSystemError } from "../error"; +import { FileSystemError, isNotFoundError } from "../error"; import type FileSystem from "../filesystem"; import type { FileInfo, FileCreateOptions, FileReader, FileWriter } from "../filesystem"; import { joinPath } from "../utils"; @@ -284,6 +284,18 @@ export default class GoogleDriveFileSystem implements FileSystem { return parentId; } async list(): Promise { + try { + return await this.listWithResolvedFolder(); + } catch (error) { + if (this.path === "/" || !isNotFoundError(error)) { + throw error; + } + this.clearPathCache(); + return this.listWithResolvedFolder(); + } + } + + private async listWithResolvedFolder(): Promise { let folderId = "appDataFolder"; // 获取当前目录的ID @@ -353,11 +365,22 @@ export default class GoogleDriveFileSystem implements FileSystem { return null; } + clearPathCache(path?: string): void { + if (!path) { + this.pathToIdCache.clear(); + return; + } + + const fullPath = joinPath(path); + const pathsToRemove = Array.from(this.pathToIdCache.keys()).filter( + (p) => p === fullPath || p.startsWith(`${fullPath}/`) + ); + pathsToRemove.forEach((p) => this.pathToIdCache.delete(p)); + } + // 清除相关缓存 clearRelatedCache(path: string): void { - // 清除路径缓存 - const pathsToRemove = Array.from(this.pathToIdCache.keys()).filter((p) => p.startsWith(path)); - pathsToRemove.forEach((p) => this.pathToIdCache.delete(p)); + this.clearPathCache(path); } async getDirUrl(): Promise { diff --git a/packages/filesystem/googledrive/rw.ts b/packages/filesystem/googledrive/rw.ts index 022572511..a19a6828d 100644 --- a/packages/filesystem/googledrive/rw.ts +++ b/packages/filesystem/googledrive/rw.ts @@ -1,3 +1,4 @@ +import { isNotFoundError } from "../error"; import type { FileInfo, FileReader, FileWriter } from "../filesystem"; import { joinPath } from "../utils"; import type GoogleDriveFileSystem from "./googledrive"; @@ -51,6 +52,18 @@ export class GoogleDriveFileWriter implements FileWriter { } async write(content: string | Blob): Promise { + try { + return await this.writeWithResolvedParent(content); + } catch (error) { + if (!isNotFoundError(error)) { + throw error; + } + this.fs.clearPathCache(); + return await this.writeWithResolvedParent(content); + } + } + + private async writeWithResolvedParent(content: string | Blob): Promise { // 解析文件路径和文件名 const pathParts = this.path.split("/").filter(Boolean); const fileName = pathParts.pop() || ""; // 获取文件名