云同步功能
This commit is contained in:
parent
185ba6e5cc
commit
1de1ba6373
@ -1,14 +1,21 @@
|
||||
import LoggerCore from "@App/app/logger/core";
|
||||
import { MessageConnect, MessageSend } from "./server";
|
||||
import Logger from "@App/app/logger/logger";
|
||||
|
||||
export async function sendMessage(msg: MessageSend, action: string, data?: any): Promise<any> {
|
||||
const res = await msg.sendMessage({ action, data });
|
||||
LoggerCore.getInstance().logger().trace("sendMessage", { action, data, response: res });
|
||||
const logger = LoggerCore.getInstance().logger().with({ action, data, response: res });
|
||||
logger.trace("sendMessage");
|
||||
if (res && res.code) {
|
||||
console.error(res);
|
||||
throw res.message;
|
||||
} else {
|
||||
return res.data;
|
||||
try {
|
||||
return res.data;
|
||||
} catch (e) {
|
||||
logger.trace("Invalid response data", Logger.E(e));
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -123,9 +123,16 @@ export class ExtensionContentMessageSend extends ExtensionMessageSend {
|
||||
|
||||
sendMessage(data: any): Promise<any> {
|
||||
return new Promise((resolve) => {
|
||||
chrome.tabs.sendMessage(this.tabId, data, this.options || {}, (resp) => {
|
||||
resolve(resp);
|
||||
});
|
||||
if (!this.options?.documentId || this.options?.frameId) {
|
||||
// 发送给指定的tab
|
||||
chrome.tabs.sendMessage(this.tabId, data, (resp) => {
|
||||
resolve(resp);
|
||||
});
|
||||
} else {
|
||||
chrome.tabs.sendMessage(this.tabId, data, this.options, (resp) => {
|
||||
resolve(resp);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { MessageQueue } from "@Packages/message/message_queue";
|
||||
import { Script, SCRIPT_RUN_STATUS } from "../repo/scripts";
|
||||
import { InstallSource } from "./service_worker";
|
||||
|
||||
export function subscribeScriptInstall(
|
||||
messageQueue: MessageQueue,
|
||||
callback: (message: { script: Script; update: boolean }) => void
|
||||
callback: (message: { script: Script; update: boolean; upsertBy: InstallSource }) => void
|
||||
) {
|
||||
return messageQueue.subscribe("installScript", callback);
|
||||
}
|
||||
|
@ -347,7 +347,7 @@ export default class GMApi {
|
||||
const url = URL.createObjectURL(blob);
|
||||
setTimeout(() => {
|
||||
URL.revokeObjectURL(url);
|
||||
}, 30*1000);
|
||||
}, 30 * 1000);
|
||||
return { action: "onload", data: url };
|
||||
} catch (e: any) {
|
||||
return { action: "error", data: { code: 5, error: e.message } };
|
||||
|
@ -39,7 +39,15 @@ export default class ServiceWorkerManager {
|
||||
const popup = new PopupService(this.api.group("popup"), this.mq, runtime);
|
||||
popup.init();
|
||||
value.init(runtime, popup);
|
||||
const synchronize = new SynchronizeService(this.sender, this.api.group("synchronize"), value, resource);
|
||||
const synchronize = new SynchronizeService(
|
||||
this.sender,
|
||||
this.api.group("synchronize"),
|
||||
script,
|
||||
value,
|
||||
resource,
|
||||
this.mq,
|
||||
systemConfig
|
||||
);
|
||||
synchronize.init();
|
||||
|
||||
// 定时器处理
|
||||
@ -48,6 +56,13 @@ export default class ServiceWorkerManager {
|
||||
case "checkScriptUpdate":
|
||||
script.checkScriptUpdate();
|
||||
break;
|
||||
case "cloudSync":
|
||||
// 进行一次云同步
|
||||
systemConfig.getCloudSync().then((config) => {
|
||||
synchronize.buildFileSystem(config).then((fs) => {
|
||||
synchronize.syncOnce(fs);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -56,10 +71,14 @@ export default class ServiceWorkerManager {
|
||||
console.log("systemConfigChange", msg);
|
||||
switch (msg.key) {
|
||||
case "cloud_sync": {
|
||||
synchronize.startCloudSync(msg.value);
|
||||
synchronize.cloudSyncConfigChange(msg.value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
// 启动一次云同步
|
||||
systemConfig.getCloudSync().then((config) => {
|
||||
synchronize.cloudSyncConfigChange(config);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -179,7 +179,7 @@ export class ScriptService {
|
||||
});
|
||||
logger.info("install success");
|
||||
// 广播一下
|
||||
this.mq.publish("installScript", { script, update });
|
||||
this.mq.publish("installScript", { script, update, upsertBy });
|
||||
return Promise.resolve({ update });
|
||||
})
|
||||
.catch((e: any) => {
|
||||
|
@ -4,7 +4,7 @@ import { Resource } from "@App/app/repo/resource";
|
||||
import { Script, SCRIPT_STATUS_ENABLE, ScriptCodeDAO, ScriptDAO } from "@App/app/repo/scripts";
|
||||
import BackupExport from "@App/pkg/backup/export";
|
||||
import { BackupData, ResourceBackup, ScriptBackupData, ScriptOptions, ValueStorage } from "@App/pkg/backup/struct";
|
||||
import FileSystem from "@Packages/filesystem/filesystem";
|
||||
import FileSystem, { File } from "@Packages/filesystem/filesystem";
|
||||
import ZipFileSystem from "@Packages/filesystem/zip/zip";
|
||||
import { Group, MessageSend } from "@Packages/message/server";
|
||||
import JSZip from "jszip";
|
||||
@ -13,8 +13,31 @@ import { ResourceService } from "./resource";
|
||||
import dayjs from "dayjs";
|
||||
import { createObjectURL } from "../offscreen/client";
|
||||
import FileSystemFactory, { FileSystemType } from "@Packages/filesystem/factory";
|
||||
import { systemConfig } from "@App/pages/store/global";
|
||||
import { CloudSyncConfig } from "@App/pkg/config/config";
|
||||
import { CloudSyncConfig, SystemConfig } from "@App/pkg/config/config";
|
||||
import { MessageQueue } from "@Packages/message/message_queue";
|
||||
import { subscribeScriptDelete, subscribeScriptInstall } from "../queue";
|
||||
import { isWarpTokenError } from "@Packages/filesystem/error";
|
||||
import { errorMsg, InfoNotification } from "@App/pkg/utils/utils";
|
||||
import { t } from "i18next";
|
||||
import ChromeStorage from "@App/pkg/config/chrome_storage";
|
||||
import { ScriptService } from "./script";
|
||||
import { prepareScriptByCode } from "@App/pkg/utils/script";
|
||||
import { InstallSource } from ".";
|
||||
|
||||
export type SynchronizeTarget = "local";
|
||||
|
||||
type SyncFiles = {
|
||||
script: File;
|
||||
meta: File;
|
||||
};
|
||||
|
||||
export type SyncMeta = {
|
||||
uuid: string;
|
||||
origin?: string; // 脚本来源
|
||||
downloadUrl?: string;
|
||||
checkUpdateUrl?: string;
|
||||
isDeleted?: boolean;
|
||||
};
|
||||
|
||||
export class SynchronizeService {
|
||||
logger: Logger;
|
||||
@ -22,11 +45,16 @@ export class SynchronizeService {
|
||||
scriptDAO = new ScriptDAO();
|
||||
scriptCodeDAO = new ScriptCodeDAO();
|
||||
|
||||
storage: ChromeStorage = new ChromeStorage("sync", true);
|
||||
|
||||
constructor(
|
||||
private send: MessageSend,
|
||||
private group: Group,
|
||||
private script: ScriptService,
|
||||
private value: ValueService,
|
||||
private resource: ResourceService
|
||||
private resource: ResourceService,
|
||||
private mq: MessageQueue,
|
||||
private systemConfig: SystemConfig
|
||||
) {
|
||||
this.logger = LoggerCore.logger().with({ service: "synchronize" });
|
||||
}
|
||||
@ -226,12 +254,289 @@ export class SynchronizeService {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
cloudSync() {}
|
||||
// 开始一次云同步
|
||||
async buildFileSystem(config: CloudSyncConfig) {
|
||||
let fs: FileSystem;
|
||||
try {
|
||||
fs = await FileSystemFactory.create(config.filesystem, config.params[config.filesystem]);
|
||||
// 创建base目录
|
||||
await FileSystemFactory.mkdirAll(fs, "ScriptCat/sync");
|
||||
fs = await fs.openDir("ScriptCat/sync");
|
||||
} catch (e: any) {
|
||||
this.logger.error("create filesystem error", Logger.E(e), {
|
||||
type: config.filesystem,
|
||||
});
|
||||
// 判断错误是不是网络类型的错误, 网络类型的错误不做任何处理
|
||||
// 如果是token失效之类的错误,通知用户并关闭云同步
|
||||
if (isWarpTokenError(e)) {
|
||||
InfoNotification(
|
||||
`${t("sync_system_connect_failed")}, ${t("sync_system_closed")}`,
|
||||
`${t("sync_system_closed_description")}\n${errorMsg(e)}`
|
||||
);
|
||||
this.systemConfig.setCloudSync({
|
||||
...config,
|
||||
enable: false,
|
||||
});
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
return fs;
|
||||
}
|
||||
|
||||
startCloudSync(value: CloudSyncConfig) {
|
||||
// 同步一次
|
||||
async syncOnce(fs: FileSystem) {
|
||||
this.logger.info("start sync once");
|
||||
// 获取文件列表
|
||||
const list = await fs.list();
|
||||
// 根据文件名生成一个map
|
||||
const uuidMap = new Map<
|
||||
string,
|
||||
{
|
||||
script?: File;
|
||||
meta?: File;
|
||||
}
|
||||
>();
|
||||
// 储存文件摘要,用于检测文件是否有变化
|
||||
const fileDigestMap =
|
||||
((await this.storage.get("file_digest")) as {
|
||||
[key: string]: string;
|
||||
}) || {};
|
||||
|
||||
list.forEach((file) => {
|
||||
if (file.name.endsWith(".user.js")) {
|
||||
const uuid = file.name.substring(0, file.name.length - 8);
|
||||
let files = uuidMap.get(uuid);
|
||||
if (!files) {
|
||||
files = {};
|
||||
uuidMap.set(uuid, files);
|
||||
}
|
||||
files.script = file;
|
||||
} else if (file.name.endsWith(".meta.json")) {
|
||||
const uuid = file.name.substring(0, file.name.length - 10);
|
||||
let files = uuidMap.get(uuid);
|
||||
if (!files) {
|
||||
files = {};
|
||||
uuidMap.set(uuid, files);
|
||||
}
|
||||
files.meta = file;
|
||||
}
|
||||
});
|
||||
|
||||
// 获取脚本列表
|
||||
const scriptList = await this.scriptDAO.all();
|
||||
// 遍历脚本列表生成一个map
|
||||
const scriptMap = new Map<string, Script>();
|
||||
scriptList.forEach((script) => {
|
||||
scriptMap.set(script.uuid, script);
|
||||
});
|
||||
// 对比脚本列表和文件列表,进行同步
|
||||
const result: Promise<void>[] = [];
|
||||
uuidMap.forEach((file, uuid) => {
|
||||
const script = scriptMap.get(uuid);
|
||||
if (script) {
|
||||
// 脚本存在但是文件不存在,则读取.meta.json内容判断是否需要删除脚本
|
||||
if (!file.script) {
|
||||
result.push(
|
||||
new Promise((resolve) => {
|
||||
const handler = async () => {
|
||||
// 读取meta文件
|
||||
const meta = await fs.open(file.meta!);
|
||||
const metaJson = (await meta.read("string")) as string;
|
||||
const metaObj = JSON.parse(metaJson) as SyncMeta;
|
||||
if (metaObj.isDeleted) {
|
||||
if (script) {
|
||||
this.script.deleteScript(script.uuid);
|
||||
InfoNotification("脚本删除同步", `脚本${script.name}已被删除`);
|
||||
}
|
||||
scriptMap.delete(uuid);
|
||||
} else {
|
||||
// 否则认为是一个无效的.meta文件,进行删除
|
||||
await fs.delete(file.meta!.path);
|
||||
}
|
||||
resolve();
|
||||
};
|
||||
handler();
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
// 过滤掉无变动的文件
|
||||
if (fileDigestMap[file.script!.name] === file.script!.digest) {
|
||||
// 删除了之后,剩下的就是需要上传的脚本了
|
||||
scriptMap.delete(uuid);
|
||||
return;
|
||||
}
|
||||
const updatetime = script.updatetime || script.createtime;
|
||||
// 对比脚本更新时间和文件更新时间
|
||||
if (updatetime > file.script!.updatetime) {
|
||||
// 如果脚本更新时间大于文件更新时间,则上传文件
|
||||
result.push(this.pushScript(fs, script));
|
||||
} else {
|
||||
// 如果脚本更新时间小于文件更新时间,则更新脚本
|
||||
result.push(this.pullScript(fs, file as SyncFiles, script));
|
||||
}
|
||||
scriptMap.delete(uuid);
|
||||
return;
|
||||
}
|
||||
// 如果脚本不存在,且文件存在,则安装脚本
|
||||
if (file.script) {
|
||||
result.push(this.pullScript(fs, file as SyncFiles));
|
||||
}
|
||||
});
|
||||
// 上传剩下的脚本
|
||||
scriptMap.forEach((script) => {
|
||||
result.push(this.pushScript(fs, script));
|
||||
});
|
||||
// 忽略错误
|
||||
await Promise.allSettled(result);
|
||||
// 重新获取文件列表,保存文件摘要
|
||||
await this.updateFileDigest(fs);
|
||||
this.logger.info("sync complete");
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async updateFileDigest(fs: FileSystem) {
|
||||
const newList = await fs.list();
|
||||
const newFileDigestMap: { [key: string]: string } = {};
|
||||
newList.forEach((file) => {
|
||||
newFileDigestMap[file.name] = file.digest;
|
||||
});
|
||||
await this.storage.set("file_digest", newFileDigestMap);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// 删除云端脚本数据
|
||||
async deleteCloudScript(fs: FileSystem, uuid: string, syncDelete: boolean) {
|
||||
const filename = `${uuid}.user.js`;
|
||||
const logger = this.logger.with({
|
||||
uuid: uuid,
|
||||
file: filename,
|
||||
});
|
||||
try {
|
||||
await fs.delete(filename);
|
||||
if (syncDelete) {
|
||||
// 留下一个.meta.json删除标记
|
||||
const meta = await fs.create(`${uuid}.meta.json`);
|
||||
await meta.write(
|
||||
JSON.stringify(<SyncMeta>{
|
||||
uuid: uuid,
|
||||
// origin: script.origin,
|
||||
// downloadUrl: script.downloadUrl,
|
||||
// checkUpdateUrl: script.checkUpdateUrl,
|
||||
isDeleted: true,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
// 直接删除所有相关文件
|
||||
await fs.delete(filename);
|
||||
await fs.delete(`${uuid}.meta.json`);
|
||||
}
|
||||
logger.info("delete success");
|
||||
} catch (e) {
|
||||
logger.error("delete file error", Logger.E(e));
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// 上传脚本
|
||||
async pushScript(fs: FileSystem, script: Script) {
|
||||
const filename = `${script.uuid}.user.js`;
|
||||
const logger = this.logger.with({
|
||||
uuid: script.uuid,
|
||||
name: script.name,
|
||||
file: filename,
|
||||
});
|
||||
try {
|
||||
const w = await fs.create(filename);
|
||||
// 获取脚本代码
|
||||
const code = await this.scriptCodeDAO.get(script.uuid);
|
||||
await w.write(code!.code);
|
||||
const meta = await fs.create(`${script.uuid}.meta.json`);
|
||||
await meta.write(
|
||||
JSON.stringify(<SyncMeta>{
|
||||
uuid: script.uuid,
|
||||
origin: script.origin,
|
||||
downloadUrl: script.downloadUrl,
|
||||
checkUpdateUrl: script.checkUpdateUrl,
|
||||
})
|
||||
);
|
||||
logger.info("push script success");
|
||||
} catch (e) {
|
||||
logger.error("push script error", Logger.E(e));
|
||||
throw e;
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async pullScript(fs: FileSystem, file: SyncFiles, script?: Script) {
|
||||
const logger = this.logger.with({
|
||||
uuid: script?.uuid || "",
|
||||
name: script?.name || "",
|
||||
file: file.script.name,
|
||||
});
|
||||
try {
|
||||
// 读取代码文件
|
||||
const r = await fs.open(file.script);
|
||||
const code = (await r.read("string")) as string;
|
||||
// 读取meta文件
|
||||
const meta = await fs.open(file.meta);
|
||||
const metaJson = (await meta.read("string")) as string;
|
||||
const metaObj = JSON.parse(metaJson) as SyncMeta;
|
||||
const prepareScript = await prepareScriptByCode(
|
||||
code,
|
||||
script?.downloadUrl || metaObj.downloadUrl || "",
|
||||
script?.uuid || metaObj.uuid
|
||||
);
|
||||
prepareScript.script.origin = prepareScript.script.origin || metaObj.origin;
|
||||
this.script.installScript({
|
||||
script: prepareScript.script,
|
||||
code: code,
|
||||
upsertBy: "sync",
|
||||
});
|
||||
logger.info("pull script success");
|
||||
} catch (e) {
|
||||
logger.error("pull script error", Logger.E(e));
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
cloudSyncConfigChange(value: CloudSyncConfig) {
|
||||
if (value.enable) {
|
||||
|
||||
this.cloudSync();
|
||||
// 开启云同步同步
|
||||
this.buildFileSystem(value).then(async (fs) => {
|
||||
await this.syncOnce(fs);
|
||||
// 开启定时器, 一小时一次
|
||||
chrome.alarms.create("cloudSync", {
|
||||
periodInMinutes: 60,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// 停止计时器
|
||||
chrome.alarms.clear("cloudSync");
|
||||
}
|
||||
}
|
||||
|
||||
async scriptInstall(params: { script: Script; update: boolean; upsertBy: InstallSource }) {
|
||||
if (params.upsertBy === "sync") {
|
||||
return;
|
||||
}
|
||||
// 判断是否开启了同步
|
||||
const config = await this.systemConfig.getCloudSync();
|
||||
if (config.enable) {
|
||||
this.buildFileSystem(config).then(async (fs) => {
|
||||
await this.pushScript(fs, params.script);
|
||||
this.updateFileDigest(fs);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async scriptDelete(script: { uuid: string }) {
|
||||
// 判断是否开启了同步
|
||||
const config = await this.systemConfig.getCloudSync();
|
||||
if (config.enable) {
|
||||
this.buildFileSystem(config).then(async (fs) => {
|
||||
await this.deleteCloudScript(fs, script.uuid, config.syncDelete);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -239,5 +544,8 @@ export class SynchronizeService {
|
||||
this.group.on("export", this.requestExport.bind(this));
|
||||
this.group.on("backupToCloud", this.backupToCloud.bind(this));
|
||||
// this.group.on("import", this.openImportWindow.bind(this));
|
||||
// 监听脚本变化, 进行同步
|
||||
subscribeScriptInstall(this.mq, this.scriptInstall.bind(this));
|
||||
subscribeScriptDelete(this.mq, this.scriptDelete.bind(this));
|
||||
}
|
||||
}
|
||||
|
@ -138,7 +138,9 @@ function Tools() {
|
||||
<Button
|
||||
key="list"
|
||||
type="primary"
|
||||
loading={loading.cloud}
|
||||
onClick={async () => {
|
||||
setLoading((prev) => ({ ...prev, cloud: true }));
|
||||
let fs = await FileSystemFactory.create(fileSystemType, fileSystemParams);
|
||||
try {
|
||||
fs = await fs.openDir("ScriptCat");
|
||||
@ -154,6 +156,7 @@ function Tools() {
|
||||
} catch (e) {
|
||||
Message.error(`${t("get_backup_files_failed")}: ${e}`);
|
||||
}
|
||||
setLoading((prev) => ({ ...prev, cloud: false }));
|
||||
}}
|
||||
>
|
||||
{t("backup_list")}
|
||||
|
@ -251,3 +251,16 @@ export function calculateMd5(blob: Blob) {
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function errorMsg(e: any): string {
|
||||
if (typeof e === "string") {
|
||||
return e;
|
||||
}
|
||||
if (e instanceof Error) {
|
||||
return e.message;
|
||||
}
|
||||
if (typeof e === "object") {
|
||||
return JSON.stringify(e);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user