Skip to content

Commit dc38f7f

Browse files
CodFrmCopilot
andauthored
✨ 支持GoogleDrive (#490)
* wip: Google drive * wip: google drive * ✨ 支持GoogleDrive * Update packages/filesystem/googledrive/rw.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * refactor: remove unused methods from GoogleDriveFileWriter - Remove unused size() and md5() methods - Remove unused findFile() private method - Remove unused imports (calculateMd5, MD5) * 修复重复创建目录的问题 --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 37217d4 commit dc38f7f

6 files changed

Lines changed: 414 additions & 4 deletions

File tree

packages/filesystem/auth.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { ExtServer, ExtServerApi } from "@App/app/const";
22
import { WarpTokenError } from "./error";
33
import { LocalStorageDAO } from "@App/app/repo/localStorage";
44

5-
type NetDiskType = "baidu" | "onedrive";
5+
type NetDiskType = "baidu" | "onedrive" | "googledrive";
66

77
export function GetNetDiskToken(netDiskType: NetDiskType): Promise<{
88
code: number;

packages/filesystem/factory.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import i18next from "i18next";
22
import BaiduFileSystem from "./baidu/baidu";
33
import FileSystem from "./filesystem";
4+
import GoogleDriveFileSystem from "./googledrive/googledrive";
45
import OneDriveFileSystem from "./onedrive/onedrive";
56
import WebDAVFileSystem from "./webdav/webdav";
67
import ZipFileSystem from "./zip/zip";
78
import i18n from "@App/locales/locales";
89

9-
export type FileSystemType = "zip" | "webdav" | "baidu-netdsik" | "onedrive";
10+
export type FileSystemType = "zip" | "webdav" | "baidu-netdsik" | "onedrive" | "googledrive";
1011

1112
export type FileSystemParams = {
1213
[key: string]: {
@@ -37,6 +38,9 @@ export default class FileSystemFactory {
3738
case "onedrive":
3839
fs = new OneDriveFileSystem();
3940
break;
41+
case "googledrive":
42+
fs = new GoogleDriveFileSystem();
43+
break;
4044
default:
4145
throw new Error("not found filesystem");
4246
}
@@ -57,6 +61,7 @@ export default class FileSystemFactory {
5761
},
5862
"baidu-netdsik": {},
5963
onedrive: {},
64+
googledrive: {},
6065
};
6166
}
6267

Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
import { AuthVerify } from "../auth";
2+
import FileSystem, { File, FileReader, FileWriter } from "../filesystem";
3+
import { joinPath } from "../utils";
4+
import { GoogleDriveFileReader, GoogleDriveFileWriter } from "./rw";
5+
6+
export default class GoogleDriveFileSystem implements FileSystem {
7+
accessToken?: string;
8+
9+
path: string;
10+
11+
// 缓存路径到文件ID的映射
12+
private pathToIdCache: Map<string, string> = new Map();
13+
14+
constructor(path?: string, accessToken?: string) {
15+
this.path = path || "/";
16+
this.accessToken = accessToken;
17+
}
18+
19+
async verify(): Promise<void> {
20+
const token = await AuthVerify("googledrive");
21+
this.accessToken = token;
22+
return this.list().then();
23+
}
24+
25+
open(file: File): Promise<FileReader> {
26+
return Promise.resolve(new GoogleDriveFileReader(this, file));
27+
}
28+
29+
openDir(path: string): Promise<FileSystem> {
30+
return Promise.resolve(new GoogleDriveFileSystem(joinPath(this.path, path), this.accessToken));
31+
}
32+
33+
create(path: string): Promise<FileWriter> {
34+
return Promise.resolve(new GoogleDriveFileWriter(this, joinPath(this.path, path)));
35+
} async createDir(dir: string): Promise<void> {
36+
if (!dir) {
37+
return Promise.resolve();
38+
}
39+
40+
const fullPath = joinPath(this.path, dir);
41+
const dirs = fullPath.split("/").filter(Boolean);
42+
43+
// 从根目录开始逐级创建目录
44+
let parentId = "root";
45+
let currentPath = "";
46+
47+
// 逐级创建目录,使用缓存减少重复请求
48+
for (const dirName of dirs) {
49+
currentPath = joinPath(currentPath, dirName);
50+
51+
// 先检查缓存
52+
let folderId = this.pathToIdCache.get(currentPath);
53+
54+
if (!folderId) {
55+
// 缓存中没有,查找目录是否已存在
56+
let folder = await this.findFolderByName(dirName, parentId);
57+
if (!folder) {
58+
// 不存在则创建
59+
folder = await this.createFolder(dirName, parentId);
60+
}
61+
folderId = folder.id;
62+
63+
// 更新缓存
64+
this.pathToIdCache.set(currentPath, folderId);
65+
}
66+
67+
parentId = folderId;
68+
}
69+
70+
return Promise.resolve();
71+
} async findFolderByName(name: string, parentId: string): Promise<{ id: string; name: string } | null> {
72+
const query = `name='${name}' and mimeType='application/vnd.google-apps.folder' and '${parentId}' in parents and trashed=false`;
73+
const response = await this.request(
74+
`https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(query)}&fields=files(id,name)`
75+
);
76+
77+
if (response.files && response.files.length > 0) {
78+
return response.files[0];
79+
}
80+
return null;
81+
}
82+
83+
async createFolder(name: string, parentId: string): Promise<{ id: string; name: string }> {
84+
const myHeaders = new Headers();
85+
myHeaders.append("Content-Type", "application/json");
86+
87+
const response = await this.request("https://www.googleapis.com/drive/v3/files", {
88+
method: "POST",
89+
headers: myHeaders,
90+
body: JSON.stringify({
91+
name: name,
92+
mimeType: "application/vnd.google-apps.folder",
93+
parents: [parentId],
94+
}),
95+
});
96+
97+
if (response.error) {
98+
throw new Error(JSON.stringify(response));
99+
}
100+
101+
return {
102+
id: response.id,
103+
name: response.name,
104+
};
105+
}
106+
107+
request(url: string, config?: RequestInit, nothen?: boolean) {
108+
config = config || {};
109+
const headers = <Headers>config.headers || new Headers();
110+
headers.append(`Authorization`, `Bearer ${this.accessToken}`);
111+
config.headers = headers;
112+
const ret = fetch(url, config);
113+
if (nothen) {
114+
return <Promise<Response>>ret;
115+
}
116+
return ret
117+
.then((data) => data.json())
118+
.then(async (data) => {
119+
if (data.error) {
120+
if (data.error.code === 401) {
121+
// Token可能过期,尝试刷新
122+
const token = await AuthVerify("googledrive", true);
123+
this.accessToken = token;
124+
headers.set(`Authorization`, `Bearer ${this.accessToken}`);
125+
return fetch(url, config)
126+
.then((retryData) => retryData.json())
127+
.then((retryData) => {
128+
if (retryData.error) {
129+
throw new Error(JSON.stringify(retryData));
130+
}
131+
return retryData;
132+
});
133+
}
134+
throw new Error(JSON.stringify(data));
135+
}
136+
return data;
137+
});
138+
} async delete(path: string): Promise<void> {
139+
const fullPath = joinPath(this.path, path);
140+
141+
// 首先,找到要删除的文件或文件夹
142+
const fileId = await this.getFileId(fullPath);
143+
if (!fileId) {
144+
throw new Error(`File or directory not found: ${path}`);
145+
}
146+
147+
// 删除文件或文件夹
148+
await this.request(
149+
`https://www.googleapis.com/drive/v3/files/${fileId}`,
150+
{
151+
method: "DELETE",
152+
},
153+
true
154+
).then(async (resp) => {
155+
if (resp.status !== 204 && resp.status !== 200) {
156+
throw new Error(await resp.text());
157+
}
158+
});
159+
160+
// 清除相关缓存
161+
this.clearRelatedCache(fullPath);
162+
}async getFileId(path: string): Promise<string | null> {
163+
if (path === "/" || path === "") {
164+
return "root";
165+
}
166+
167+
// 先检查缓存
168+
const cachedId = this.pathToIdCache.get(path);
169+
if (cachedId) {
170+
return cachedId;
171+
}
172+
173+
// 从根目录开始逐级查找
174+
const pathParts = path.split("/").filter(Boolean);
175+
let parentId = "root";
176+
let currentPath = "";
177+
178+
// 逐级查找路径
179+
for (const part of pathParts) {
180+
currentPath = joinPath(currentPath, part);
181+
182+
// 检查这个路径是否已经缓存
183+
const cachedPartId = this.pathToIdCache.get(currentPath);
184+
if (cachedPartId) {
185+
parentId = cachedPartId;
186+
continue;
187+
}
188+
189+
const query = `name='${part}' and '${parentId}' in parents and trashed=false`;
190+
const response = await this.request(
191+
`https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(query)}&fields=files(id,name)`
192+
);
193+
194+
if (!response.files || response.files.length === 0) {
195+
return null;
196+
}
197+
198+
parentId = response.files[0].id;
199+
200+
// 缓存这个路径的ID
201+
this.pathToIdCache.set(currentPath, parentId);
202+
}
203+
204+
return parentId;
205+
} async list(): Promise<File[]> {
206+
let folderId = "root";
207+
208+
// 获取当前目录的ID
209+
if (this.path !== "/") {
210+
const foundId = await this.getFileId(this.path);
211+
if (!foundId) {
212+
throw new Error(`Directory not found: ${this.path}`);
213+
}
214+
folderId = foundId;
215+
}
216+
217+
// 列出目录内容
218+
const query = `'${folderId}' in parents and trashed=false`;
219+
const response = await this.request(
220+
`https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(query)}&fields=files(id,name,mimeType,size,md5Checksum,createdTime,modifiedTime)`
221+
);
222+
223+
const list: File[] = [];
224+
if (response.files) {
225+
for (const item of response.files) {
226+
list.push({
227+
name: item.name,
228+
path: this.path,
229+
size: item.size ? parseInt(item.size, 10) : 0,
230+
digest: item.md5Checksum || "",
231+
createtime: new Date(item.createdTime).getTime(),
232+
updatetime: new Date(item.modifiedTime).getTime(),
233+
});
234+
}
235+
}
236+
237+
return list;
238+
}
239+
240+
// 辅助方法:在指定目录中查找文件
241+
async findFileInDirectory(fileName: string, parentId: string): Promise<string | null> {
242+
const query = `name='${fileName}' and '${parentId}' in parents and trashed=false and mimeType!='application/vnd.google-apps.folder'`;
243+
const response = await this.request(
244+
`https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(query)}&fields=files(id)`
245+
);
246+
247+
if (response.files && response.files.length > 0) {
248+
return response.files[0].id;
249+
}
250+
return null;
251+
}
252+
253+
// 清除相关缓存
254+
clearRelatedCache(path: string): void {
255+
// 清除路径缓存
256+
const pathsToRemove = Array.from(this.pathToIdCache.keys()).filter(p => p.startsWith(path));
257+
pathsToRemove.forEach(p => this.pathToIdCache.delete(p));
258+
}
259+
260+
async getDirUrl(): Promise<string> {
261+
// Retrieve the folder ID for the current path
262+
const folderId = await this.getFileId(this.path);
263+
if (!folderId) {
264+
throw new Error(`Directory not found: ${this.path}`);
265+
}
266+
267+
// Construct and return the Google Drive folder URL
268+
return `https://drive.google.com/drive/folders/${folderId}`;
269+
}
270+
271+
// 确保目录存在并返回目录ID,优化Writer避免重复获取
272+
async ensureDirExists(dirPath: string): Promise<string> {
273+
if (dirPath === "/" || dirPath === "") {
274+
return "root";
275+
}
276+
277+
// 先检查缓存
278+
const cachedId = this.pathToIdCache.get(dirPath);
279+
if (cachedId) {
280+
return cachedId;
281+
}
282+
283+
// 如果没有缓存,使用getFileId方法
284+
const foundId = await this.getFileId(dirPath);
285+
if (!foundId) {
286+
throw new Error(`Failed to create or find directory: ${dirPath}`);
287+
}
288+
289+
// 缓存结果
290+
this.pathToIdCache.set(dirPath, foundId);
291+
return foundId;
292+
}
293+
}

0 commit comments

Comments
 (0)