Skip to content

Commit 0bbd404

Browse files
authored
🐛 Google Drive stale path cache 在 404 后清理并重试 (#1407)
1 parent f8dcd0f commit 0bbd404

3 files changed

Lines changed: 119 additions & 4 deletions

File tree

packages/filesystem/googledrive/googledrive.test.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,85 @@ describe("GoogleDriveFileSystem", () => {
8282
expect(requestSpy).toHaveBeenCalledTimes(1);
8383
});
8484

85+
it("writer should clear stale path cache and retry once on provider 404", async () => {
86+
const fs = new GoogleDriveFileSystem("/", "token");
87+
const notFoundError = new FileSystemError({
88+
provider: "googledrive",
89+
message: "Parent not found",
90+
status: 404,
91+
notFound: true,
92+
});
93+
const findFolderSpy = vi
94+
.spyOn(fs, "findFolderByName")
95+
.mockResolvedValueOnce({ id: "stale-base-id", name: "Base" })
96+
.mockResolvedValueOnce({ id: "fresh-base-id", name: "Base" });
97+
98+
await fs.ensureDirExists("/Base");
99+
100+
const writer = await fs.create("Base/file.txt");
101+
const findFileSpy = vi
102+
.spyOn(fs, "findFileInDirectory")
103+
.mockRejectedValueOnce(notFoundError)
104+
.mockResolvedValueOnce(null);
105+
const requestSpy = vi.spyOn(fs, "request").mockResolvedValue({});
106+
107+
await expect(writer.write("content")).resolves.toBeUndefined();
108+
109+
expect(findFolderSpy.mock.calls).toEqual([
110+
["Base", "appDataFolder"],
111+
["Base", "appDataFolder"],
112+
]);
113+
expect(findFileSpy.mock.calls).toEqual([
114+
["file.txt", "stale-base-id"],
115+
["file.txt", "fresh-base-id"],
116+
]);
117+
expect(requestSpy).toHaveBeenCalledTimes(1);
118+
});
119+
120+
it("writer should not retry non-404 provider errors", async () => {
121+
const fs = new GoogleDriveFileSystem("/", "token");
122+
const conflictError = new FileSystemError({
123+
provider: "googledrive",
124+
message: "Conflict",
125+
status: 409,
126+
conflict: true,
127+
});
128+
const writer = await fs.create("Base/file.txt");
129+
const ensureSpy = vi.spyOn(fs, "ensureDirExists").mockResolvedValue("base-id");
130+
const findFileSpy = vi.spyOn(fs, "findFileInDirectory").mockRejectedValue(conflictError);
131+
132+
await expect(writer.write("content")).rejects.toBe(conflictError);
133+
134+
expect(ensureSpy).toHaveBeenCalledTimes(1);
135+
expect(findFileSpy).toHaveBeenCalledTimes(1);
136+
});
137+
138+
it("list should clear stale path cache and retry once on provider 404", async () => {
139+
const fs = new GoogleDriveFileSystem("/Base", "token");
140+
const notFoundError = new FileSystemError({
141+
provider: "googledrive",
142+
message: "Folder not found",
143+
status: 404,
144+
notFound: true,
145+
});
146+
const findFolderSpy = vi.spyOn(fs, "findFolderByName").mockResolvedValueOnce({ id: "stale-base-id", name: "Base" });
147+
148+
await fs.ensureDirExists("/Base");
149+
150+
const requestSpy = vi
151+
.spyOn(fs, "request")
152+
.mockRejectedValueOnce(notFoundError)
153+
.mockResolvedValueOnce({ files: [{ id: "fresh-base-id", name: "Base" }] })
154+
.mockResolvedValueOnce({ files: [] });
155+
156+
await expect(fs.list()).resolves.toEqual([]);
157+
158+
expect(findFolderSpy).toHaveBeenCalledTimes(1);
159+
expect(String(requestSpy.mock.calls[0][0])).toContain("stale-base-id");
160+
expect(String(requestSpy.mock.calls[1][0])).toContain("name%3D'Base'");
161+
expect(String(requestSpy.mock.calls[2][0])).toContain("fresh-base-id");
162+
});
163+
85164
it("request should return retry result after token refresh", async () => {
86165
await localStorageDAO.saveValue("netdisk:token:googledrive", {
87166
accessToken: "expired-token",

packages/filesystem/googledrive/googledrive.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { AuthVerify } from "../auth";
2-
import { FileSystemError } from "../error";
2+
import { FileSystemError, isNotFoundError } from "../error";
33
import type FileSystem from "../filesystem";
44
import type { FileInfo, FileCreateOptions, FileReader, FileWriter } from "../filesystem";
55
import { joinPath } from "../utils";
@@ -284,6 +284,18 @@ export default class GoogleDriveFileSystem implements FileSystem {
284284
return parentId;
285285
}
286286
async list(): Promise<FileInfo[]> {
287+
try {
288+
return await this.listWithResolvedFolder();
289+
} catch (error) {
290+
if (this.path === "/" || !isNotFoundError(error)) {
291+
throw error;
292+
}
293+
this.clearPathCache();
294+
return this.listWithResolvedFolder();
295+
}
296+
}
297+
298+
private async listWithResolvedFolder(): Promise<FileInfo[]> {
287299
let folderId = "appDataFolder";
288300

289301
// 获取当前目录的ID
@@ -353,11 +365,22 @@ export default class GoogleDriveFileSystem implements FileSystem {
353365
return null;
354366
}
355367

368+
clearPathCache(path?: string): void {
369+
if (!path) {
370+
this.pathToIdCache.clear();
371+
return;
372+
}
373+
374+
const fullPath = joinPath(path);
375+
const pathsToRemove = Array.from(this.pathToIdCache.keys()).filter(
376+
(p) => p === fullPath || p.startsWith(`${fullPath}/`)
377+
);
378+
pathsToRemove.forEach((p) => this.pathToIdCache.delete(p));
379+
}
380+
356381
// 清除相关缓存
357382
clearRelatedCache(path: string): void {
358-
// 清除路径缓存
359-
const pathsToRemove = Array.from(this.pathToIdCache.keys()).filter((p) => p.startsWith(path));
360-
pathsToRemove.forEach((p) => this.pathToIdCache.delete(p));
383+
this.clearPathCache(path);
361384
}
362385

363386
async getDirUrl(): Promise<string> {

packages/filesystem/googledrive/rw.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { isNotFoundError } from "../error";
12
import type { FileInfo, FileReader, FileWriter } from "../filesystem";
23
import { joinPath } from "../utils";
34
import type GoogleDriveFileSystem from "./googledrive";
@@ -51,6 +52,18 @@ export class GoogleDriveFileWriter implements FileWriter {
5152
}
5253

5354
async write(content: string | Blob): Promise<void> {
55+
try {
56+
return await this.writeWithResolvedParent(content);
57+
} catch (error) {
58+
if (!isNotFoundError(error)) {
59+
throw error;
60+
}
61+
this.fs.clearPathCache();
62+
return await this.writeWithResolvedParent(content);
63+
}
64+
}
65+
66+
private async writeWithResolvedParent(content: string | Blob): Promise<void> {
5467
// 解析文件路径和文件名
5568
const pathParts = this.path.split("/").filter(Boolean);
5669
const fileName = pathParts.pop() || ""; // 获取文件名

0 commit comments

Comments
 (0)