Skip to content
Open
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
20 changes: 20 additions & 0 deletions packages/filesystem/baidu/baidu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,24 @@ describe("BaiduFileSystem", () => {
);
expect(updateDynamicRulesMock).not.toHaveBeenCalled();
});

it("create should normalize double slashes in paths", async () => {
const fs = new BaiduFileSystem("/apps//ScriptCat", "token");

const writer = await fs.create("dir//file.user.js");

expect((writer as any).path).toBe("/apps/ScriptCat/dir/file.user.js");
});

it("delete should normalize double slashes in filelist payload", async () => {
const fs = new BaiduFileSystem("/apps//ScriptCat", "token");
const request = vi.spyOn(fs, "request").mockResolvedValue({ errno: 0 });

await fs.delete("dir//file.user.js");

const [, config] = request.mock.calls[0];
expect((config as RequestInit).body).toBe(
`async=0&filelist=${encodeURIComponent(JSON.stringify(["/apps/ScriptCat/dir/file.user.js"]))}`
);
});
});
25 changes: 25 additions & 0 deletions packages/filesystem/dropbox/dropbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,29 @@ describe("DropboxFileSystem", () => {

await expect(fs.exists("/test.txt")).rejects.toThrow("invalid_access_token");
});

it("create should normalize double slashes after the Dropbox app root", async () => {
const fs = new DropboxFileSystem("/ScriptCat//sync", "token");

const writer = await fs.create("dir//file.user.js");

expect((writer as any).path).toBe("/sync/dir/file.user.js");
});

it("delete should normalize double slashes after the Dropbox app root", async () => {
const fs = new DropboxFileSystem("/ScriptCat//sync", "token");
const request = vi.spyOn(fs, "request").mockResolvedValue({});

await fs.delete("dir//file.user.js");

expect(request).toHaveBeenCalledWith(
"https://api.dropboxapi.com/2/files/delete_v2",
expect.objectContaining({
method: "POST",
body: JSON.stringify({
path: "/sync/dir/file.user.js",
}),
})
);
});
});
16 changes: 16 additions & 0 deletions packages/filesystem/googledrive/googledrive.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { LocalStorageDAO } from "@App/app/repo/localStorage";
import { FileSystemError, isAuthError, isConflictError, isNotFoundError, isRateLimitError } from "../error";
import { joinPath } from "../utils";
import GoogleDriveFileSystem from "./googledrive";

function createMockResponse(options: { ok?: boolean; status?: number; text?: string; json?: any }): Response {
Expand Down Expand Up @@ -82,6 +83,21 @@ describe("GoogleDriveFileSystem", () => {
expect(requestSpy).toHaveBeenCalledTimes(1);
});

it("create should normalize double slashes in paths", async () => {
const fs = new GoogleDriveFileSystem("/ScriptCat//sync", "token");

const writer = await fs.create("dir//file.user.js");

expect((writer as any).path).toBe("/ScriptCat/sync/dir/file.user.js");
});

it("clearPathCache should accept normalized paths derived from duplicate slashes", () => {
const fs = new GoogleDriveFileSystem("/ScriptCat//sync", "token");

expect(joinPath("/ScriptCat//sync", "dir//file.user.js")).toBe("/ScriptCat/sync/dir/file.user.js");
expect(() => fs.clearPathCache("/ScriptCat//sync/dir")).not.toThrow();
});

it("writer should clear stale path cache and retry once on provider 404", async () => {
const fs = new GoogleDriveFileSystem("/", "token");
const notFoundError = new FileSystemError({
Expand Down
21 changes: 21 additions & 0 deletions packages/filesystem/onedrive/onedrive.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,27 @@ describe("OneDriveFileSystem", () => {
await expect(fs.delete("missing.txt")).resolves.toBeUndefined();
});

it("create should normalize double slashes in paths", async () => {
const fs = new OneDriveFileSystem("/ScriptCat//sync", "token");

const writer = await fs.create("dir//file.user.js");

expect((writer as any).path).toBe("/ScriptCat/sync/dir/file.user.js");
});

it("delete should normalize double slashes in URL paths", async () => {
const fs = new OneDriveFileSystem("/ScriptCat//sync", "token");
const request = vi.spyOn(fs, "request").mockResolvedValue({ status: 204 });

await fs.delete("dir//file.user.js");

expect(request).toHaveBeenCalledWith(
"https://graph.microsoft.com/v1.0/me/drive/special/approot:/ScriptCat/sync/dir/file.user.js",
{ method: "DELETE" },
true
);
});

it("createDir should create nested directories from root", async () => {
const fs = new OneDriveFileSystem("/", "token");
const requestSpy = vi.spyOn(fs, "request").mockResolvedValue({});
Expand Down
17 changes: 17 additions & 0 deletions packages/filesystem/s3/s3.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,14 @@ describe("S3FileSystem", () => {
})
);
});

it("normalizes double slashes in object keys", async () => {
const subFs = new S3FileSystem("test-bucket", mockClient, "/ScriptCat//sync");

const writer = await subFs.create("dir//file.user.js");

expect((writer as any).key).toBe("ScriptCat/sync/dir/file.user.js");
});
});

// ---- createDir ----
Expand Down Expand Up @@ -235,6 +243,15 @@ describe("S3FileSystem", () => {

await expect(fs.delete("test.txt")).rejects.toThrow();
});

it("normalizes double slashes in object keys", async () => {
const subFs = new S3FileSystem("test-bucket", mockClient, "/ScriptCat//sync");
(mockClient.request as ReturnType<typeof vi.fn>).mockResolvedValue(createMockResponse({ ok: true, status: 204 }));

await subFs.delete("dir//file.user.js");

expect(mockClient.request).toHaveBeenCalledWith("DELETE", "test-bucket", "ScriptCat/sync/dir/file.user.js");
});
});

// ---- list ----
Expand Down
43 changes: 43 additions & 0 deletions packages/filesystem/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { describe, expect, it } from "vitest";
import { joinPath } from "./utils";

describe("joinPath", () => {
it("joins relative path segments as an absolute normalized path", () => {
expect(joinPath("path1", "path2")).toBe("/path1/path2");
});

it("does not create duplicate slashes when segments already contain slashes", () => {
expect(joinPath("/path1", "/path2")).toBe("/path1/path2");
expect(joinPath("/path1/", "/path2/")).toBe("/path1/path2");
expect(joinPath("path1/", "path2/")).toBe("/path1/path2");
expect(joinPath("/path1/", "path2")).toBe("/path1/path2");
});

it("keeps root-relative behavior when the first segment is empty", () => {
expect(joinPath("", "file.txt")).toBe("/file.txt");
expect(joinPath("", "dir", "file.txt")).toBe("/dir/file.txt");
});

it("handles root path segments", () => {
expect(joinPath("/", "file.txt")).toBe("/file.txt");
expect(joinPath("/", "dir", "file.txt")).toBe("/dir/file.txt");
});

it("skips empty path segments", () => {
expect(joinPath("dir", "", "file.txt")).toBe("/dir/file.txt");
expect(joinPath("", "dir", "", "file.txt", "")).toBe("/dir/file.txt");
});

it("returns empty string when no meaningful path is provided", () => {
expect(joinPath()).toBe("");
expect(joinPath("")).toBe("");
expect(joinPath("", "")).toBe("");
expect(joinPath("/")).toBe("");
expect(joinPath("/", "")).toBe("");
});

it("normalizes multiple adjacent slashes inside segments", () => {
expect(joinPath("//path1//", "//path2//")).toBe("/path1/path2");
expect(joinPath("path1//nested", "path2")).toBe("/path1/nested/path2");
});
});
29 changes: 19 additions & 10 deletions packages/filesystem/utils.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
export function joinPath(...paths: string[]): string {
let path = "";
for (let value of paths) {
if (!value) {
let result = "";

for (const path of paths) {
if (!path) {
continue;
}
if (!value.startsWith("/")) {
value = `/${value}`;

let start = 0;

for (let i = 0; i <= path.length; i++) {
if (i !== path.length && path[i] !== "/") {
continue;
}

if (i > start) {
result += `/${path.slice(start, i)}`;
}

start = i + 1;
}
if (value.endsWith("/")) {
value = value.substring(0, value.length - 1);
}
path += value;
}
return path;

return result;
}
24 changes: 24 additions & 0 deletions packages/filesystem/webdav/webdav.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,17 @@ describe("WebDAVFileSystem", () => {
expect(mockClient.deleteFile).toHaveBeenCalledWith("/test.txt");
});

it("normalizes double slashes in paths", async () => {
const fs = WebDAVFileSystem.fromSameClient(
{ client: mockClient, url: "https://dav.example.com", basePath: "/ScriptCat//sync" } as any,
"/ScriptCat//sync"
);

await fs.delete("dir//file.user.js");

expect(mockClient.deleteFile).toHaveBeenCalledWith("/ScriptCat/sync/dir/file.user.js");
});

it("应当在 404 时静默成功(幂等删除)", async () => {
(mockClient.deleteFile as ReturnType<typeof vi.fn>).mockRejectedValue({
response: { status: 404 },
Expand All @@ -217,6 +228,19 @@ describe("WebDAVFileSystem", () => {
});
});

describe("create", () => {
it("normalizes double slashes in paths", async () => {
const fs = WebDAVFileSystem.fromSameClient(
{ client: mockClient, url: "https://dav.example.com", basePath: "/ScriptCat//sync" } as any,
"/ScriptCat//sync"
);

const writer = await fs.create("dir//file.user.js");

expect((writer as any).path).toBe("/ScriptCat/sync/dir/file.user.js");
});
});

describe("list", () => {
it("应当列出文件并过滤目录", async () => {
(mockClient.getDirectoryContents as ReturnType<typeof vi.fn>).mockResolvedValue([
Expand Down
Loading