Skip to content

Commit 58e0f37

Browse files
committed
Merge branch 'main' into fix/sync/015c
2 parents ce1a8cb + 1c10827 commit 58e0f37

25 files changed

Lines changed: 693 additions & 452 deletions

example/run-at_context-menu.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// ==UserScript==
2+
// @name Context Menu Demo: Search Selection
3+
// @namespace https://bbs.tampermonkey.net.cn/
4+
// @version 0.1.0
5+
// @description Demo for @run-at context-menu
6+
// @author You
7+
// @match *://*/*
8+
// @run-at context-menu
9+
// @grant GM_openInTab
10+
// ==/UserScript==
11+
12+
(function () {
13+
'use strict';
14+
const selectedText = window.getSelection().toString().trim();
15+
if (!selectedText) {
16+
alert("Please select some texts first.");
17+
return;
18+
}
19+
GM_openInTab(`https://scriptcat.org/search?keyword=${encodeURIComponent(selectedText)}`)
20+
})();

packages/filesystem/auth.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,38 @@ describe("AuthVerify", () => {
6868
).resolves.toEqual(["new-access", "new-access", "new-access"]);
6969
expect(fetchMock).toHaveBeenCalledTimes(1);
7070
});
71+
72+
it("concurrent initial token verification should share one auth request and save once", async () => {
73+
vi.useFakeTimers();
74+
const createSpy = vi.spyOn(chrome.tabs, "create").mockImplementation(() => Promise.resolve({ id: 1 }) as any);
75+
const originalGet = (chrome.tabs as any).get;
76+
(chrome.tabs as any).get = vi.fn().mockRejectedValue(new Error("closed"));
77+
const saveSpy = vi.spyOn(LocalStorageDAO.prototype, "saveValue");
78+
const fetchMock = vi.fn().mockResolvedValue({
79+
json: vi.fn().mockResolvedValue({
80+
code: 0,
81+
data: {
82+
token: {
83+
access_token: "initial-access",
84+
refresh_token: "initial-refresh",
85+
},
86+
},
87+
}),
88+
} as unknown as Response);
89+
vi.stubGlobal("fetch", fetchMock);
90+
91+
try {
92+
const auth = Promise.all([AuthVerify("onedrive"), AuthVerify("onedrive"), AuthVerify("onedrive")]);
93+
await vi.advanceTimersByTimeAsync(1000);
94+
95+
await expect(auth).resolves.toEqual(["initial-access", "initial-access", "initial-access"]);
96+
expect(createSpy).toHaveBeenCalledTimes(1);
97+
expect(fetchMock).toHaveBeenCalledTimes(1);
98+
expect(saveSpy).toHaveBeenCalledTimes(1);
99+
expect(saveSpy).toHaveBeenCalledWith(key, expect.objectContaining({ accessToken: "initial-access" }));
100+
} finally {
101+
(chrome.tabs as any).get = originalGet;
102+
vi.useRealTimers();
103+
}
104+
});
71105
});

packages/filesystem/auth.ts

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ export type Token = {
7474
createtime: number;
7575
};
7676
const refreshTokenPromises: Partial<Record<NetDiskType, Promise<string>>> = {};
77+
const authTokenPromises: Partial<Record<NetDiskType, Promise<Token>>> = {};
7778

7879
function refreshAccessToken(
7980
netDiskType: NetDiskType,
@@ -126,19 +127,30 @@ export async function AuthVerify(netDiskType: NetDiskType, invalid?: boolean) {
126127
}
127128
// token不存在,或者没有accessToken,重新获取
128129
if (!token || !token.accessToken) {
129-
// 强制重新获取token
130-
await NetDisk(netDiskType);
131-
const resp = await GetNetDiskToken(netDiskType);
132-
if (resp.code !== 0) {
133-
throw new WarpTokenError(new Error(resp.msg));
130+
if (!authTokenPromises[netDiskType]) {
131+
const authPromise = (async () => {
132+
// 强制重新获取token
133+
await NetDisk(netDiskType);
134+
const resp = await GetNetDiskToken(netDiskType);
135+
if (resp.code !== 0) {
136+
throw new WarpTokenError(new Error(resp.msg));
137+
}
138+
const newToken = {
139+
accessToken: resp.data.token.access_token,
140+
refreshToken: resp.data.token.refresh_token,
141+
createtime: Date.now(),
142+
};
143+
await localStorageDAO.saveValue(key, newToken);
144+
return newToken;
145+
})().finally(() => {
146+
if (authTokenPromises[netDiskType] === authPromise) {
147+
delete authTokenPromises[netDiskType];
148+
}
149+
});
150+
authTokenPromises[netDiskType] = authPromise;
134151
}
135-
token = {
136-
accessToken: resp.data.token.access_token,
137-
refreshToken: resp.data.token.refresh_token,
138-
createtime: Date.now(),
139-
};
152+
token = await authTokenPromises[netDiskType];
140153
invalid = false;
141-
await localStorageDAO.saveValue(key, token);
142154
}
143155
// token未过期(一小时内)及有效则保留,不用刷新token
144156
const unexpired = Date.now() < token.createtime + 3600000;

packages/filesystem/baidu/baidu.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,26 @@ describe("BaiduFileSystem", () => {
3333
expect(updateDynamicRulesMock).not.toHaveBeenCalled();
3434
});
3535

36+
it("create should normalize double slashes in paths", async () => {
37+
const fs = new BaiduFileSystem("/apps//ScriptCat", "token");
38+
39+
const writer = await fs.create("dir//file.user.js");
40+
41+
expect((writer as any).path).toBe("/apps/ScriptCat/dir/file.user.js");
42+
});
43+
44+
it("delete should normalize double slashes in filelist payload", async () => {
45+
const fs = new BaiduFileSystem("/apps//ScriptCat", "token");
46+
const request = vi.spyOn(fs, "request").mockResolvedValue({ errno: 0 });
47+
48+
await fs.delete("dir//file.user.js");
49+
50+
const [, config] = request.mock.calls[0];
51+
expect((config as RequestInit).body).toBe(
52+
`async=0&filelist=${encodeURIComponent(JSON.stringify(["/apps/ScriptCat/dir/file.user.js"]))}`
53+
);
54+
});
55+
3656
it("create should reject expectedVersion as unsupported", async () => {
3757
const fs = new BaiduFileSystem("/apps", "token");
3858

packages/filesystem/dropbox/dropbox.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,31 @@ describe("DropboxFileSystem", () => {
5252
await expect(fs.exists("/test.txt")).rejects.toThrow("invalid_access_token");
5353
});
5454

55+
it("create should normalize double slashes after the Dropbox app root", async () => {
56+
const fs = new DropboxFileSystem("/ScriptCat//sync", "token");
57+
58+
const writer = await fs.create("dir//file.user.js");
59+
60+
expect((writer as any).path).toBe("/sync/dir/file.user.js");
61+
});
62+
63+
it("delete should normalize double slashes after the Dropbox app root", async () => {
64+
const fs = new DropboxFileSystem("/ScriptCat//sync", "token");
65+
const request = vi.spyOn(fs, "request").mockResolvedValue({});
66+
67+
await fs.delete("dir//file.user.js");
68+
69+
expect(request).toHaveBeenCalledWith(
70+
"https://api.dropboxapi.com/2/files/delete_v2",
71+
expect.objectContaining({
72+
method: "POST",
73+
body: JSON.stringify({
74+
path: "/sync/dir/file.user.js",
75+
}),
76+
})
77+
);
78+
});
79+
5580
it("list should expose Dropbox rev as version", async () => {
5681
const fs = new DropboxFileSystem("/", "token");
5782
vi.spyOn(fs, "request").mockResolvedValue({

packages/filesystem/googledrive/googledrive.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
22
import { LocalStorageDAO } from "@App/app/repo/localStorage";
33
import { FileSystemError, isAuthError, isConflictError, isNotFoundError, isRateLimitError } from "../error";
4+
import { joinPath } from "../utils";
45
import GoogleDriveFileSystem from "./googledrive";
56

67
function createMockResponse(options: { ok?: boolean; status?: number; text?: string; json?: any }): Response {
@@ -121,6 +122,21 @@ describe("GoogleDriveFileSystem", () => {
121122
expect(requestSpy).toHaveBeenCalledTimes(1);
122123
});
123124

125+
it("create should normalize double slashes in paths", async () => {
126+
const fs = new GoogleDriveFileSystem("/ScriptCat//sync", "token");
127+
128+
const writer = await fs.create("dir//file.user.js");
129+
130+
expect((writer as any).path).toBe("/ScriptCat/sync/dir/file.user.js");
131+
});
132+
133+
it("clearPathCache should accept normalized paths derived from duplicate slashes", () => {
134+
const fs = new GoogleDriveFileSystem("/ScriptCat//sync", "token");
135+
136+
expect(joinPath("/ScriptCat//sync", "dir//file.user.js")).toBe("/ScriptCat/sync/dir/file.user.js");
137+
expect(() => fs.clearPathCache("/ScriptCat//sync/dir")).not.toThrow();
138+
});
139+
124140
it("writer should clear stale path cache and retry once on provider 404", async () => {
125141
const fs = new GoogleDriveFileSystem("/", "token");
126142
const notFoundError = new FileSystemError({

packages/filesystem/onedrive/onedrive.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,27 @@ describe("OneDriveFileSystem", () => {
9595
await expect(fs.delete("missing.txt")).resolves.toBeUndefined();
9696
});
9797

98+
it("create should normalize double slashes in paths", async () => {
99+
const fs = new OneDriveFileSystem("/ScriptCat//sync", "token");
100+
101+
const writer = await fs.create("dir//file.user.js");
102+
103+
expect((writer as any).path).toBe("/ScriptCat/sync/dir/file.user.js");
104+
});
105+
106+
it("delete should normalize double slashes in URL paths", async () => {
107+
const fs = new OneDriveFileSystem("/ScriptCat//sync", "token");
108+
const request = vi.spyOn(fs, "request").mockResolvedValue({ status: 204 });
109+
110+
await fs.delete("dir//file.user.js");
111+
112+
expect(request).toHaveBeenCalledWith(
113+
"https://graph.microsoft.com/v1.0/me/drive/special/approot:/ScriptCat/sync/dir/file.user.js",
114+
{ method: "DELETE" },
115+
true
116+
);
117+
});
118+
98119
it("delete should send If-Match when expectedVersion is provided", async () => {
99120
const fs = new OneDriveFileSystem("/", "token");
100121
const requestSpy = vi.spyOn(fs, "request").mockResolvedValue({} as unknown as Response);

0 commit comments

Comments
 (0)