From 01f7828614c2ecafed5d9e4bba9c41c51172c81a Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 6 May 2026 19:10:29 +0900 Subject: [PATCH] =?UTF-8?q?fix(sync):=20cloud=20sync=20=E5=86=99=E5=85=A5?= =?UTF-8?q?=E6=97=B6=E4=BC=A0=E9=80=92=20modifiedDate=EF=BC=8C=E8=AE=A9=20?= =?UTF-8?q?S3=20metadata=20=E5=86=99=E5=85=A5=E8=B7=AF=E5=BE=84=E7=9C=9F?= =?UTF-8?q?=E6=AD=A3=E7=94=9F=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/filesystem/s3/s3.test.ts | 18 +++ .../service_worker/synchronize.test.ts | 106 ++++++++++++++++++ src/app/service/service_worker/synchronize.ts | 15 ++- 3 files changed, 134 insertions(+), 5 deletions(-) diff --git a/packages/filesystem/s3/s3.test.ts b/packages/filesystem/s3/s3.test.ts index c82cc1ebd..8628456ff 100644 --- a/packages/filesystem/s3/s3.test.ts +++ b/packages/filesystem/s3/s3.test.ts @@ -184,6 +184,24 @@ describe("S3FileSystem", () => { }) ); }); + + it("S3FileWriter.write 应在传入 modifiedDate 时写入 createtime 元数据", async () => { + (mockClient.request as ReturnType).mockResolvedValue(createMockResponse({ ok: true })); + + const writer = await fs.create("output.txt", { modifiedDate: 1234 }); + await writer.write("hello world"); + + expect(mockClient.request).toHaveBeenCalledWith( + "PUT", + "test-bucket", + "output.txt", + expect.objectContaining({ + headers: expect.objectContaining({ + "x-amz-meta-createtime": new Date(1234).toISOString(), + }), + }) + ); + }); }); // ---- createDir ---- diff --git a/src/app/service/service_worker/synchronize.test.ts b/src/app/service/service_worker/synchronize.test.ts index 51639239a..4fc2ba742 100644 --- a/src/app/service/service_worker/synchronize.test.ts +++ b/src/app/service/service_worker/synchronize.test.ts @@ -445,6 +445,112 @@ console.log("ok");` }); }); + it("passes script modifiedDate when pushing script and meta files", async () => { + const writeMock = vi.fn().mockResolvedValue(undefined); + const createMock = vi.fn().mockResolvedValue({ write: writeMock }); + const fs = createFs({ + create: createMock, + }); + const service = new SynchronizeService( + {} as any, + {} as any, + {} as any, + {} as any, + {} as any, + {} as any, + {} as any, + { + scriptCodeDAO: { + get: vi.fn().mockResolvedValue({ code: "// code" }), + }, + all: vi.fn().mockResolvedValue([]), + } as any + ); + const script = { + uuid: "push-uuid", + name: "push", + origin: "origin", + downloadUrl: "download-url", + checkUpdateUrl: "check-update-url", + updatetime: 1234, + createtime: 1000, + status: 1, + sort: 0, + metadata: {}, + }; + + await service.pushScript(fs, script as any); + + expect(createMock.mock.calls[0]).toEqual(["push-uuid.user.js", { modifiedDate: 1234 }]); + expect(createMock.mock.calls[1]).toEqual(["push-uuid.meta.json", { modifiedDate: 1234 }]); + }); + + it("uses Date.now as modifiedDate when writing scriptcat-sync.json", async () => { + const nowSpy = vi.spyOn(Date, "now").mockReturnValue(9876); + const createMock = vi.fn().mockResolvedValue({ + write: vi.fn().mockResolvedValue(undefined), + }); + const fs = createFs({ + create: createMock, + }); + const service = new SynchronizeService( + {} as any, + {} as any, + {} as any, + {} as any, + {} as any, + {} as any, + {} as any, + { + scriptCodeDAO: {}, + all: vi.fn().mockResolvedValue([]), + } as any + ); + + try { + await service.syncOnce(syncConfig, fs); + + expect(createMock).toHaveBeenCalledWith("scriptcat-sync.json", { + modifiedDate: 9876, + }); + } finally { + nowSpy.mockRestore(); + } + }); + + it("uses Date.now as modifiedDate when writing delete tombstone meta", async () => { + const nowSpy = vi.spyOn(Date, "now").mockReturnValue(6789); + const createMock = vi.fn().mockResolvedValue({ + write: vi.fn().mockResolvedValue(undefined), + }); + const fs = createFs({ + create: createMock, + }); + const service = new SynchronizeService( + {} as any, + {} as any, + {} as any, + {} as any, + {} as any, + {} as any, + {} as any, + { + scriptCodeDAO: {}, + all: vi.fn().mockResolvedValue([]), + } as any + ); + + try { + await service.deleteCloudScript(fs, "delete-uuid", true); + + expect(createMock).toHaveBeenCalledWith("delete-uuid.meta.json", { + modifiedDate: 6789, + }); + } finally { + nowSpy.mockRestore(); + } + }); + it("preserves cloud-native digest and does not overwrite with pushed md5", async () => { // 各后端 digest 格式不一致(webdav/onedrive 是 etag、dropbox 是 content_hash 等), // 上传后再次 list 已经能拿到原生 digest 时,必须保留它,不能被本地 md5 覆盖, diff --git a/src/app/service/service_worker/synchronize.ts b/src/app/service/service_worker/synchronize.ts index 1c032d3ab..6862c525f 100644 --- a/src/app/service/service_worker/synchronize.ts +++ b/src/app/service/service_worker/synchronize.ts @@ -66,7 +66,7 @@ type ScriptcatSyncStatus = { updatetime: number; // 更新时间 }; -type PushScriptParam = TInstallScriptParams; +type PushScriptParam = TInstallScriptParams & Partial>; type FileDigestMap = { [key: string]: string; @@ -535,7 +535,9 @@ export class SynchronizeService { } }); // 保存脚本猫同步状态 - const syncFile = await fs.create("scriptcat-sync.json"); + const syncFile = await fs.create("scriptcat-sync.json", { + modifiedDate: Date.now(), + }); await syncFile.write(JSON.stringify(scriptcatSync, null, 2)); this.logger.info("sync scriptcat-sync.json file success"); } @@ -575,7 +577,9 @@ export class SynchronizeService { await fs.delete(filename); if (syncDelete) { // 留下一个.meta.json删除标记 - const meta = await fs.create(`${uuid}.meta.json`); + const meta = await fs.create(`${uuid}.meta.json`, { + modifiedDate: Date.now(), + }); await meta.write( JSON.stringify({ uuid: uuid, @@ -606,12 +610,13 @@ export class SynchronizeService { file: filename, }); try { - const w = await fs.create(filename); + const modifiedDate = script.updatetime || script.createtime || Date.now(); + const w = await fs.create(filename, { modifiedDate }); // 获取脚本代码 const code = await this.scriptCodeDAO.get(script.uuid); const scriptCode = code!.code; await w.write(scriptCode); - const meta = await fs.create(metaFilename); + const meta = await fs.create(metaFilename, { modifiedDate }); const metaJson = JSON.stringify({ uuid: script.uuid, origin: script.origin,