Skip to content

Commit 5aea479

Browse files
authored
🐛 为云同步上传补充 modifiedDate (写入 S3 自定义元数据) (#1408)
* fix(sync): cloud sync 写入时传递 modifiedDate,让 S3 metadata 写入路径真正生效 * update
1 parent ea5a1a4 commit 5aea479

4 files changed

Lines changed: 140 additions & 7 deletions

File tree

packages/filesystem/s3/rw.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export class S3FileWriter implements FileWriter {
6565
"content-type": "application/octet-stream",
6666
};
6767
if (this.modifiedDate) {
68-
// 通过自定义元数据保存创建时间(ISO 8601 格式)
68+
// 历史兼容:S3 侧使用 createtime 元数据保存文件时间,实际来源是 FileCreateOptions.modifiedDate。
6969
headers["x-amz-meta-createtime"] = new Date(this.modifiedDate).toISOString();
7070
}
7171

packages/filesystem/s3/s3.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,24 @@ describe("S3FileSystem", () => {
184184
})
185185
);
186186
});
187+
188+
it("S3FileWriter.write 应将 modifiedDate 写入兼容用的 createtime 元数据", async () => {
189+
(mockClient.request as ReturnType<typeof vi.fn>).mockResolvedValue(createMockResponse({ ok: true }));
190+
191+
const writer = await fs.create("output.txt", { modifiedDate: 1234 });
192+
await writer.write("hello world");
193+
194+
expect(mockClient.request).toHaveBeenCalledWith(
195+
"PUT",
196+
"test-bucket",
197+
"output.txt",
198+
expect.objectContaining({
199+
headers: expect.objectContaining({
200+
"x-amz-meta-createtime": new Date(1234).toISOString(),
201+
}),
202+
})
203+
);
204+
});
187205
});
188206

189207
// ---- createDir ----

src/app/service/service_worker/synchronize.test.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,112 @@ console.log("ok");`
445445
});
446446
});
447447

448+
it("passes script modifiedDate when pushing script and meta files", async () => {
449+
const writeMock = vi.fn().mockResolvedValue(undefined);
450+
const createMock = vi.fn().mockResolvedValue({ write: writeMock });
451+
const fs = createFs({
452+
create: createMock,
453+
});
454+
const service = new SynchronizeService(
455+
{} as any,
456+
{} as any,
457+
{} as any,
458+
{} as any,
459+
{} as any,
460+
{} as any,
461+
{} as any,
462+
{
463+
scriptCodeDAO: {
464+
get: vi.fn().mockResolvedValue({ code: "// code" }),
465+
},
466+
all: vi.fn().mockResolvedValue([]),
467+
} as any
468+
);
469+
const script = {
470+
uuid: "push-uuid",
471+
name: "push",
472+
origin: "origin",
473+
downloadUrl: "download-url",
474+
checkUpdateUrl: "check-update-url",
475+
updatetime: 1234,
476+
createtime: 1000,
477+
status: 1,
478+
sort: 0,
479+
metadata: {},
480+
};
481+
482+
await service.pushScript(fs, script as any);
483+
484+
expect(createMock.mock.calls[0]).toEqual(["push-uuid.user.js", { modifiedDate: 1234 }]);
485+
expect(createMock.mock.calls[1]).toEqual(["push-uuid.meta.json", { modifiedDate: 1234 }]);
486+
});
487+
488+
it("uses Date.now as modifiedDate when writing scriptcat-sync.json", async () => {
489+
const nowSpy = vi.spyOn(Date, "now").mockReturnValue(9876);
490+
const createMock = vi.fn().mockResolvedValue({
491+
write: vi.fn().mockResolvedValue(undefined),
492+
});
493+
const fs = createFs({
494+
create: createMock,
495+
});
496+
const service = new SynchronizeService(
497+
{} as any,
498+
{} as any,
499+
{} as any,
500+
{} as any,
501+
{} as any,
502+
{} as any,
503+
{} as any,
504+
{
505+
scriptCodeDAO: {},
506+
all: vi.fn().mockResolvedValue([]),
507+
} as any
508+
);
509+
510+
try {
511+
await service.syncOnce(syncConfig, fs);
512+
513+
expect(createMock).toHaveBeenCalledWith("scriptcat-sync.json", {
514+
modifiedDate: 9876,
515+
});
516+
} finally {
517+
nowSpy.mockRestore();
518+
}
519+
});
520+
521+
it("uses Date.now as modifiedDate when writing delete tombstone meta", async () => {
522+
const nowSpy = vi.spyOn(Date, "now").mockReturnValue(6789);
523+
const createMock = vi.fn().mockResolvedValue({
524+
write: vi.fn().mockResolvedValue(undefined),
525+
});
526+
const fs = createFs({
527+
create: createMock,
528+
});
529+
const service = new SynchronizeService(
530+
{} as any,
531+
{} as any,
532+
{} as any,
533+
{} as any,
534+
{} as any,
535+
{} as any,
536+
{} as any,
537+
{
538+
scriptCodeDAO: {},
539+
all: vi.fn().mockResolvedValue([]),
540+
} as any
541+
);
542+
543+
try {
544+
await service.deleteCloudScript(fs, "delete-uuid", true);
545+
546+
expect(createMock).toHaveBeenCalledWith("delete-uuid.meta.json", {
547+
modifiedDate: 6789,
548+
});
549+
} finally {
550+
nowSpy.mockRestore();
551+
}
552+
});
553+
448554
it("preserves cloud-native digest and does not overwrite with pushed md5", async () => {
449555
// 各后端 digest 格式不一致(webdav/onedrive 是 etag、dropbox 是 content_hash 等),
450556
// 上传后再次 list 已经能拿到原生 digest 时,必须保留它,不能被本地 md5 覆盖,

src/app/service/service_worker/synchronize.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,18 @@ type ScriptcatSyncStatus = {
6666
updatetime: number; // 更新时间
6767
};
6868

69-
type PushScriptParam = TInstallScriptParams;
69+
type PushScriptParam = TInstallScriptParams & Partial<Pick<Script, "createtime" | "updatetime">>;
7070

7171
type FileDigestMap = {
7272
[key: string]: string;
7373
};
7474

7575
const SYNC_SERVICE_TASK_KEY = "cloud_sync_queue";
7676

77+
function getScriptModifiedDate(script: PushScriptParam): number {
78+
return script.updatetime || script.createtime || Date.now();
79+
}
80+
7781
export class SynchronizeService {
7882
logger: Logger;
7983

@@ -423,7 +427,9 @@ export class SynchronizeService {
423427
await this.script.deleteScript(script.uuid, "sync");
424428
InfoNotification(
425429
i18n.t("notification.script_sync_delete"),
426-
i18n.t("notification.script_sync_delete_desc", { scriptName: i18nName(script) })
430+
i18n.t("notification.script_sync_delete_desc", {
431+
scriptName: i18nName(script),
432+
})
427433
);
428434
} else {
429435
// 否则认为是一个无效的.meta文件,进行删除,并进行同步
@@ -535,7 +541,8 @@ export class SynchronizeService {
535541
}
536542
});
537543
// 保存脚本猫同步状态
538-
const syncFile = await fs.create("scriptcat-sync.json");
544+
const modifiedDate = Date.now();
545+
const syncFile = await fs.create("scriptcat-sync.json", { modifiedDate });
539546
await syncFile.write(JSON.stringify(scriptcatSync, null, 2));
540547
this.logger.info("sync scriptcat-sync.json file success");
541548
}
@@ -575,7 +582,8 @@ export class SynchronizeService {
575582
await fs.delete(filename);
576583
if (syncDelete) {
577584
// 留下一个.meta.json删除标记
578-
const meta = await fs.create(`${uuid}.meta.json`);
585+
const modifiedDate = Date.now();
586+
const meta = await fs.create(`${uuid}.meta.json`, { modifiedDate });
579587
await meta.write(
580588
JSON.stringify(<SyncMeta>{
581589
uuid: uuid,
@@ -606,12 +614,13 @@ export class SynchronizeService {
606614
file: filename,
607615
});
608616
try {
609-
const w = await fs.create(filename);
617+
const modifiedDate = getScriptModifiedDate(script);
618+
const w = await fs.create(filename, { modifiedDate });
610619
// 获取脚本代码
611620
const code = await this.scriptCodeDAO.get(script.uuid);
612621
const scriptCode = code!.code;
613622
await w.write(scriptCode);
614-
const meta = await fs.create(metaFilename);
623+
const meta = await fs.create(metaFilename, { modifiedDate });
615624
const metaJson = JSON.stringify(<SyncMeta>{
616625
uuid: script.uuid,
617626
origin: script.origin,

0 commit comments

Comments
 (0)