Skip to content

Commit 05ce96e

Browse files
committed
code update
1 parent 512a62b commit 05ce96e

7 files changed

Lines changed: 316 additions & 16 deletions

File tree

packages/filesystem/googledrive/googledrive.test.ts

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,9 @@ describe("GoogleDriveFileSystem", () => {
137137

138138
it("writer should update expected Google Drive file id with If-Match token", async () => {
139139
const fs = new GoogleDriveFileSystem("/", "token");
140-
const writer = await fs.create("file.txt", { expectedVersion: "file-1:version-7" });
140+
const writer = await fs.create("file.txt", {
141+
expectedVersion: "file-1:version-7",
142+
});
141143
const findSpy = vi.spyOn(fs, "findFileInDirectory");
142144
const requestSpy = vi.spyOn(fs, "request").mockResolvedValue({});
143145

@@ -163,23 +165,49 @@ describe("GoogleDriveFileSystem", () => {
163165
});
164166
});
165167

168+
it("writer should create createOnly files with a generated Google Drive id", async () => {
169+
const fs = new GoogleDriveFileSystem("/", "token");
170+
const writer = await fs.create("file.txt", { createOnly: true });
171+
vi.spyOn(fs, "findFileInDirectory").mockResolvedValue(null);
172+
vi.spyOn(fs, "findFilesInDirectory").mockResolvedValue([{ id: "generated-file" }]);
173+
const requestSpy = vi
174+
.spyOn(fs, "request")
175+
.mockResolvedValueOnce({ ids: ["generated-file"] })
176+
.mockResolvedValueOnce({ id: "generated-file" });
177+
178+
await expect(writer.write("content")).resolves.toBeUndefined();
179+
180+
expect(requestSpy.mock.calls[0][0]).toBe(
181+
"https://www.googleapis.com/drive/v3/files/generateIds?count=1&space=appDataFolder&fields=ids"
182+
);
183+
const createOptions = requestSpy.mock.calls[1][1] as RequestInit;
184+
const headers = createOptions.headers as Headers;
185+
expect(headers.get("If-None-Match")).toBe("*");
186+
const formData = createOptions.body as FormData;
187+
expect(formData.get("metadata")).toBeTruthy();
188+
});
189+
166190
it("writer should rollback and reject createOnly when Google Drive creates a duplicate name", async () => {
167191
const fs = new GoogleDriveFileSystem("/", "token");
168192
const writer = await fs.create("file.txt", { createOnly: true });
169193
vi.spyOn(fs, "findFileInDirectory").mockResolvedValue(null);
170194
vi.spyOn(fs, "findFilesInDirectory").mockResolvedValue([{ id: "other-file" }, { id: "created-file" }]);
171-
const requestSpy = vi.spyOn(fs, "request").mockResolvedValueOnce({ id: "created-file" }).mockResolvedValueOnce({});
195+
const requestSpy = vi
196+
.spyOn(fs, "request")
197+
.mockResolvedValueOnce({ ids: ["generated-file"] })
198+
.mockResolvedValueOnce({ id: "created-file" })
199+
.mockResolvedValueOnce({});
172200

173201
await expect(writer.write("content")).rejects.toMatchObject({
174202
provider: "googledrive",
175203
conflict: true,
176204
});
177205

178-
expect(requestSpy).toHaveBeenCalledTimes(2);
179-
expect(requestSpy.mock.calls[1][0]).toBe(
206+
expect(requestSpy).toHaveBeenCalledTimes(3);
207+
expect(requestSpy.mock.calls[2][0]).toBe(
180208
"https://www.googleapis.com/drive/v3/files/created-file?spaces=appDataFolder"
181209
);
182-
expect((requestSpy.mock.calls[1][1] as RequestInit).method).toBe("DELETE");
210+
expect((requestSpy.mock.calls[2][1] as RequestInit).method).toBe("DELETE");
183211
});
184212

185213
it("list should clear stale path cache and retry once on provider 404", async () => {

packages/filesystem/googledrive/googledrive.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,21 @@ export default class GoogleDriveFileSystem implements FileSystem {
374374
return response.files || [];
375375
}
376376

377+
async generateFileId(): Promise<string> {
378+
const response = await this.request(
379+
"https://www.googleapis.com/drive/v3/files/generateIds?count=1&space=appDataFolder&fields=ids"
380+
);
381+
const id = response.ids?.[0];
382+
if (!id) {
383+
throw new FileSystemError({
384+
provider: "googledrive",
385+
message: "Google Drive did not return a generated file id",
386+
retryable: true,
387+
});
388+
}
389+
return id;
390+
}
391+
377392
clearPathCache(path?: string): void {
378393
if (!path) {
379394
this.pathToIdCache.clear();

packages/filesystem/googledrive/rw.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,10 @@ export class GoogleDriveFileWriter implements FileWriter {
125125
private async createNewFile(fileName: string, parentId: string, content: string | Blob): Promise<void> {
126126
// 不设置Content-Type,让浏览器自动处理multipart/form-data边界
127127

128+
const createOnly = this.opts?.createOnly || this.opts?.overwrite === false;
129+
const generatedId = createOnly ? await this.fs.generateFileId() : undefined;
128130
const metadata = {
131+
...(generatedId ? { id: generatedId } : {}),
129132
name: fileName,
130133
parents: [parentId],
131134
};
@@ -134,8 +137,7 @@ export class GoogleDriveFileWriter implements FileWriter {
134137
formData.append("metadata", new Blob([JSON.stringify(metadata)], { type: "application/json" }));
135138
formData.append("file", content instanceof Blob ? content : new Blob([content]));
136139

137-
const headers =
138-
this.opts?.createOnly || this.opts?.overwrite === false ? new Headers({ "If-None-Match": "*" }) : undefined;
140+
const headers = createOnly ? new Headers({ "If-None-Match": "*" }) : undefined;
139141

140142
const created = await this.fs.request(
141143
`https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&spaces=appDataFolder&fields=id`,
@@ -146,7 +148,7 @@ export class GoogleDriveFileWriter implements FileWriter {
146148
}
147149
);
148150

149-
if (this.opts?.createOnly || this.opts?.overwrite === false) {
151+
if (createOnly) {
150152
await this.rejectDuplicateCreate(fileName, parentId, created?.id);
151153
}
152154

packages/filesystem/s3/s3.test.ts

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,14 @@ function createMockResponse(options: {
2525
statusText?: string;
2626
text?: string;
2727
blob?: Blob;
28+
headers?: Headers;
2829
}): Response {
2930
const { ok = true, status = 200, statusText = "OK", text = "" } = options;
3031
return {
3132
ok,
3233
status,
3334
statusText,
34-
headers: new Headers(),
35+
headers: options.headers || new Headers(),
3536
text: vi.fn().mockResolvedValue(text),
3637
blob: vi.fn().mockResolvedValue(options.blob ?? new Blob([text])),
3738
} as unknown as Response;
@@ -206,7 +207,9 @@ describe("S3FileSystem", () => {
206207
it("S3FileWriter.write 应按 expectedVersion 设置 If-Match", async () => {
207208
(mockClient.request as ReturnType<typeof vi.fn>).mockResolvedValue(createMockResponse({ ok: true }));
208209

209-
const writer = await fs.create("output.txt", { expectedVersion: "etag-1" });
210+
const writer = await fs.create("output.txt", {
211+
expectedVersion: "etag-1",
212+
});
210213
await writer.write("hello world");
211214

212215
expect(mockClient.request).toHaveBeenCalledWith(
@@ -313,6 +316,31 @@ describe("S3FileSystem", () => {
313316
});
314317
});
315318

319+
it("应当从对象 metadata 读取 createtime", async () => {
320+
const xml = `<?xml version="1.0" encoding="UTF-8"?>
321+
<ListBucketResult>
322+
<IsTruncated>false</IsTruncated>
323+
<Contents>
324+
<Key>file1.txt</Key>
325+
<LastModified>2024-01-02T00:00:00.000Z</LastModified>
326+
<ETag>"abc123"</ETag>
327+
<Size>1024</Size>
328+
</Contents>
329+
</ListBucketResult>`;
330+
const headers = new Headers({
331+
"x-amz-meta-createtime": "2024-01-01T00:00:00.000Z",
332+
});
333+
(mockClient.request as ReturnType<typeof vi.fn>)
334+
.mockResolvedValueOnce(createMockResponse({ text: xml }))
335+
.mockResolvedValueOnce(createMockResponse({ headers }));
336+
337+
const files = await fs.list();
338+
339+
expect(files[0].createtime).toBe(new Date("2024-01-01T00:00:00.000Z").getTime());
340+
expect(files[0].updatetime).toBe(new Date("2024-01-02T00:00:00.000Z").getTime());
341+
expect(mockClient.request).toHaveBeenCalledWith("HEAD", "test-bucket", "file1.txt");
342+
});
343+
316344
it("应当正确处理带 basePath 的目录列表", async () => {
317345
const subFs = new S3FileSystem("test-bucket", mockClient, "/docs");
318346

@@ -391,14 +419,16 @@ describe("S3FileSystem", () => {
391419

392420
(mockClient.request as ReturnType<typeof vi.fn>)
393421
.mockResolvedValueOnce(createMockResponse({ text: xmlPage1 }))
394-
.mockResolvedValueOnce(createMockResponse({ text: xmlPage2 }));
422+
.mockResolvedValueOnce(createMockResponse({}))
423+
.mockResolvedValueOnce(createMockResponse({ text: xmlPage2 }))
424+
.mockResolvedValueOnce(createMockResponse({}));
395425

396426
const files = await fs.list();
397427

398428
expect(files).toHaveLength(2);
399429
expect(files[0].name).toBe("file1.txt");
400430
expect(files[1].name).toBe("file2.txt");
401-
expect(mockClient.request).toHaveBeenCalledTimes(2);
431+
expect(mockClient.request).toHaveBeenCalledTimes(4);
402432
});
403433

404434
it("应当返回空数组当目录为空时", async () => {

packages/filesystem/s3/s3.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,14 +231,16 @@ export default class S3FileSystem implements FileSystem {
231231
if (!relativeKey) continue;
232232

233233
const lastModified = new Date(obj.lastModified).getTime() || Date.now();
234+
const metadataCreatetime = await this.getObjectCreatetime(obj.key);
235+
const createtime = metadataCreatetime || lastModified;
234236

235237
files.push({
236238
name: relativeKey,
237239
path: this.basePath,
238240
size: obj.size || 0,
239241
digest: obj.etag?.replace(/"/g, "") || "",
240242
version: obj.etag?.replace(/"/g, "") || "",
241-
createtime: lastModified,
243+
createtime,
242244
updatetime: lastModified,
243245
});
244246
}
@@ -259,6 +261,23 @@ export default class S3FileSystem implements FileSystem {
259261
}
260262
}
261263

264+
private async getObjectCreatetime(key: string): Promise<number | undefined> {
265+
try {
266+
const response = await this.client.request("HEAD", this.bucket, key);
267+
const value = response.headers.get("x-amz-meta-createtime");
268+
if (!value) {
269+
return undefined;
270+
}
271+
const timestamp = new Date(value).getTime();
272+
return Number.isFinite(timestamp) ? timestamp : undefined;
273+
} catch (error) {
274+
if (error instanceof S3Error && error.code === "NoSuchKey") {
275+
return undefined;
276+
}
277+
throw error;
278+
}
279+
}
280+
262281
/**
263282
* 获取当前目录的 URL
264283
* 自定义 endpoint 返回 endpoint + bucket/prefix 路径

0 commit comments

Comments
 (0)