Skip to content
Merged
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
2 changes: 1 addition & 1 deletion packages/filesystem/s3/rw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export class S3FileWriter implements FileWriter {
"content-type": "application/octet-stream",
};
if (this.modifiedDate) {
// 通过自定义元数据保存创建时间(ISO 8601 格式)
// 历史兼容:S3 侧使用 createtime 元数据保存文件时间,实际来源是 FileCreateOptions.modifiedDate。
headers["x-amz-meta-createtime"] = new Date(this.modifiedDate).toISOString();
}

Expand Down
18 changes: 18 additions & 0 deletions packages/filesystem/s3/s3.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,24 @@ describe("S3FileSystem", () => {
})
);
});

it("S3FileWriter.write 应将 modifiedDate 写入兼容用的 createtime 元数据", async () => {
(mockClient.request as ReturnType<typeof vi.fn>).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 ----
Expand Down
106 changes: 106 additions & 0 deletions src/app/service/service_worker/synchronize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 覆盖,
Expand Down
21 changes: 15 additions & 6 deletions src/app/service/service_worker/synchronize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,18 @@ type ScriptcatSyncStatus = {
updatetime: number; // 更新时间
};

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

type FileDigestMap = {
[key: string]: string;
};

const SYNC_SERVICE_TASK_KEY = "cloud_sync_queue";

function getScriptModifiedDate(script: PushScriptParam): number {
return script.updatetime || script.createtime || Date.now();
}

export class SynchronizeService {
logger: Logger;

Expand Down Expand Up @@ -423,7 +427,9 @@ export class SynchronizeService {
await this.script.deleteScript(script.uuid, "sync");
InfoNotification(
i18n.t("notification.script_sync_delete"),
i18n.t("notification.script_sync_delete_desc", { scriptName: i18nName(script) })
i18n.t("notification.script_sync_delete_desc", {
scriptName: i18nName(script),
})
);
} else {
// 否则认为是一个无效的.meta文件,进行删除,并进行同步
Expand Down Expand Up @@ -535,7 +541,8 @@ export class SynchronizeService {
}
});
// 保存脚本猫同步状态
const syncFile = await fs.create("scriptcat-sync.json");
const modifiedDate = Date.now();
const syncFile = await fs.create("scriptcat-sync.json", { modifiedDate });
await syncFile.write(JSON.stringify(scriptcatSync, null, 2));
this.logger.info("sync scriptcat-sync.json file success");
}
Expand Down Expand Up @@ -575,7 +582,8 @@ export class SynchronizeService {
await fs.delete(filename);
if (syncDelete) {
// 留下一个.meta.json删除标记
const meta = await fs.create(`${uuid}.meta.json`);
const modifiedDate = Date.now();
const meta = await fs.create(`${uuid}.meta.json`, { modifiedDate });
await meta.write(
JSON.stringify(<SyncMeta>{
uuid: uuid,
Expand Down Expand Up @@ -606,12 +614,13 @@ export class SynchronizeService {
file: filename,
});
try {
const w = await fs.create(filename);
const modifiedDate = getScriptModifiedDate(script);
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(<SyncMeta>{
uuid: script.uuid,
origin: script.origin,
Expand Down
Loading