Compare commits
9 Commits
44e1449e03
...
d9fdded7fb
Author | SHA1 | Date | |
---|---|---|---|
d9fdded7fb | |||
498d36567b | |||
d7adffcd9f | |||
44066d9543 | |||
9a53c4e4e9 | |||
1de1ba6373 | |||
185ba6e5cc | |||
07c4518cba | |||
e2832093f0 |
7
.github/workflows/test.yaml
vendored
7
.github/workflows/test.yaml
vendored
@ -3,11 +3,8 @@ name: test
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- disable # 暂时禁用
|
||||||
- release/*
|
# pull_request:
|
||||||
- dev
|
|
||||||
- develop/*
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
tests:
|
tests:
|
||||||
|
31
.gitignore
vendored
31
.gitignore
vendored
@ -1,16 +1,31 @@
|
|||||||
# Local
|
# Logs
|
||||||
.DS_Store
|
logs
|
||||||
*.local
|
*.log
|
||||||
*.log*
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
# Dist
|
|
||||||
node_modules
|
node_modules
|
||||||
dist/
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
# IDE
|
# Editor directories and files
|
||||||
.vscode/*
|
.vscode
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
.idea
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
coverage
|
coverage
|
||||||
|
CHANGELOG.md
|
||||||
|
|
||||||
tailwind.config.js
|
tailwind.config.js
|
||||||
|
|
||||||
|
.env
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "scriptcat",
|
"name": "scriptcat",
|
||||||
"version": "0.17.0-alpha.1",
|
"version": "0.17.0-alpha.2",
|
||||||
"description": "脚本猫,一个可以执行用户脚本的浏览器扩展,万物皆可脚本化,让你的浏览器可以做更多的事情!",
|
"description": "脚本猫,一个可以执行用户脚本的浏览器扩展,万物皆可脚本化,让你的浏览器可以做更多的事情!",
|
||||||
"author": "CodFrm",
|
"author": "CodFrm",
|
||||||
"license": "GPLv3",
|
"license": "GPLv3",
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
/* eslint-disable max-classes-per-file */
|
|
||||||
/* eslint-disable import/prefer-default-export */
|
|
||||||
import { calculateMd5 } from "@App/pkg/utils/utils";
|
import { calculateMd5 } from "@App/pkg/utils/utils";
|
||||||
import { MD5 } from "crypto-js";
|
import { MD5 } from "crypto-js";
|
||||||
import { File, FileReader, FileWriter } from "../filesystem";
|
import { File, FileReader, FileWriter } from "../filesystem";
|
||||||
|
@ -1,9 +1,5 @@
|
|||||||
import JSZip from "jszip";
|
import JSZip from "jszip";
|
||||||
import FileSystem, {
|
import FileSystem, { File, FileReader, FileWriter } from "@Packages/filesystem/filesystem";
|
||||||
File,
|
|
||||||
FileReader,
|
|
||||||
FileWriter,
|
|
||||||
} from "@Pkg/filesystem/filesystem";
|
|
||||||
import { ZipFileReader, ZipFileWriter } from "./rw";
|
import { ZipFileReader, ZipFileWriter } from "./rw";
|
||||||
|
|
||||||
export default class ZipFileSystem implements FileSystem {
|
export default class ZipFileSystem implements FileSystem {
|
||||||
|
@ -1,14 +1,21 @@
|
|||||||
import LoggerCore from "@App/app/logger/core";
|
import LoggerCore from "@App/app/logger/core";
|
||||||
import { MessageConnect, MessageSend } from "./server";
|
import { MessageConnect, MessageSend } from "./server";
|
||||||
|
import Logger from "@App/app/logger/logger";
|
||||||
|
|
||||||
export async function sendMessage(msg: MessageSend, action: string, data?: any): Promise<any> {
|
export async function sendMessage(msg: MessageSend, action: string, data?: any): Promise<any> {
|
||||||
const res = await msg.sendMessage({ action, data });
|
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) {
|
if (res && res.code) {
|
||||||
console.error(res);
|
console.error(res);
|
||||||
throw res.message;
|
throw res.message;
|
||||||
} else {
|
} 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> {
|
sendMessage(data: any): Promise<any> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
chrome.tabs.sendMessage(this.tabId, data, this.options || {}, (resp) => {
|
if (!this.options?.documentId || this.options?.frameId) {
|
||||||
resolve(resp);
|
// 发送给指定的tab
|
||||||
});
|
chrome.tabs.sendMessage(this.tabId, data, (resp) => {
|
||||||
|
resolve(resp);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
chrome.tabs.sendMessage(this.tabId, data, this.options, (resp) => {
|
||||||
|
resolve(resp);
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,6 +36,7 @@ export default defineConfig({
|
|||||||
popup: `${src}/pages/popup/main.tsx`,
|
popup: `${src}/pages/popup/main.tsx`,
|
||||||
install: `${src}/pages/install/main.tsx`,
|
install: `${src}/pages/install/main.tsx`,
|
||||||
confirm: `${src}/pages/confirm/main.tsx`,
|
confirm: `${src}/pages/confirm/main.tsx`,
|
||||||
|
import: `${src}/pages/import/main.tsx`,
|
||||||
options: `${src}/pages/options/main.tsx`,
|
options: `${src}/pages/options/main.tsx`,
|
||||||
"editor.worker": "monaco-editor/esm/vs/editor/editor.worker.js",
|
"editor.worker": "monaco-editor/esm/vs/editor/editor.worker.js",
|
||||||
"ts.worker": "monaco-editor/esm/vs/language/typescript/ts.worker.js",
|
"ts.worker": "monaco-editor/esm/vs/language/typescript/ts.worker.js",
|
||||||
@ -152,7 +153,6 @@ export default defineConfig({
|
|||||||
minify: true,
|
minify: true,
|
||||||
chunks: ["install"],
|
chunks: ["install"],
|
||||||
}),
|
}),
|
||||||
,
|
|
||||||
new rspack.HtmlRspackPlugin({
|
new rspack.HtmlRspackPlugin({
|
||||||
filename: `${dist}/ext/src/confirm.html`,
|
filename: `${dist}/ext/src/confirm.html`,
|
||||||
template: `${src}/pages/template.html`,
|
template: `${src}/pages/template.html`,
|
||||||
@ -161,6 +161,14 @@ export default defineConfig({
|
|||||||
minify: true,
|
minify: true,
|
||||||
chunks: ["confirm"],
|
chunks: ["confirm"],
|
||||||
}),
|
}),
|
||||||
|
new rspack.HtmlRspackPlugin({
|
||||||
|
filename: `${dist}/ext/src/import.html`,
|
||||||
|
template: `${src}/pages/template.html`,
|
||||||
|
inject: "head",
|
||||||
|
title: "Import - ScriptCat",
|
||||||
|
minify: true,
|
||||||
|
chunks: ["import"],
|
||||||
|
}),
|
||||||
new rspack.HtmlRspackPlugin({
|
new rspack.HtmlRspackPlugin({
|
||||||
filename: `${dist}/ext/src/options.html`,
|
filename: `${dist}/ext/src/options.html`,
|
||||||
template: `${src}/pages/options.html`,
|
template: `${src}/pages/options.html`,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ConfirmParam } from "@App/runtime/service_worker/permission_verify";
|
import { ConfirmParam } from "./service/service_worker/permission_verify";
|
||||||
|
|
||||||
export default class CacheKey {
|
export default class CacheKey {
|
||||||
// 加载脚本信息时的缓存
|
// 加载脚本信息时的缓存
|
||||||
@ -9,4 +9,9 @@ export default class CacheKey {
|
|||||||
static permissionConfirm(scriptUuid: string, confirm: ConfirmParam): string {
|
static permissionConfirm(scriptUuid: string, confirm: ConfirmParam): string {
|
||||||
return `permission:${scriptUuid}:${confirm.permissionValue || ""}:${confirm.permission || ""}`;
|
return `permission:${scriptUuid}:${confirm.permissionValue || ""}:${confirm.permission || ""}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// importFile 导入文件
|
||||||
|
static importFile(uuid: string): string {
|
||||||
|
return `importFile:${uuid}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { db } from "./repo/dao";
|
import { db } from "./repo/dao";
|
||||||
import { Script } from "./repo/scripts";
|
import { Script, ScriptAndCode, ScriptCodeDAO, ScriptDAO } from "./repo/scripts";
|
||||||
|
import { Subscribe, SubscribeDAO } from "./repo/subscribe";
|
||||||
|
|
||||||
// 0.10.0重构,重命名字段,统一使用小峰驼
|
// 0.10.0重构,重命名字段,统一使用小峰驼
|
||||||
function renameField(): void {
|
function renameField() {
|
||||||
db.version(16)
|
db.version(16)
|
||||||
.stores({
|
.stores({
|
||||||
scripts:
|
scripts:
|
||||||
@ -33,9 +34,100 @@ function renameField(): void {
|
|||||||
export: "++id,&scriptId",
|
export: "++id,&scriptId",
|
||||||
});
|
});
|
||||||
// 将脚本数据迁移到chrome.storage
|
// 将脚本数据迁移到chrome.storage
|
||||||
// db.version(18)
|
db.version(18).upgrade(async (tx) => {
|
||||||
// .stores({})
|
// 迁移脚本
|
||||||
// .upgrade((tx) => {});
|
const scripts = await tx.table("scripts").toArray();
|
||||||
|
const scriptDAO = new ScriptDAO();
|
||||||
|
const scriptCodeDAO = new ScriptCodeDAO();
|
||||||
|
await Promise.all(
|
||||||
|
scripts.map((script: ScriptAndCode) => {
|
||||||
|
const {
|
||||||
|
uuid,
|
||||||
|
name,
|
||||||
|
namespace,
|
||||||
|
author,
|
||||||
|
originDomain,
|
||||||
|
subscribeUrl,
|
||||||
|
type,
|
||||||
|
sort,
|
||||||
|
status,
|
||||||
|
runStatus,
|
||||||
|
metadata,
|
||||||
|
createtime,
|
||||||
|
checktime,
|
||||||
|
code,
|
||||||
|
} = script;
|
||||||
|
return scriptDAO
|
||||||
|
.save({
|
||||||
|
uuid,
|
||||||
|
name,
|
||||||
|
namespace,
|
||||||
|
author,
|
||||||
|
originDomain,
|
||||||
|
subscribeUrl,
|
||||||
|
type,
|
||||||
|
sort,
|
||||||
|
status,
|
||||||
|
runStatus,
|
||||||
|
metadata,
|
||||||
|
createtime,
|
||||||
|
checktime,
|
||||||
|
})
|
||||||
|
.then((s) =>
|
||||||
|
scriptCodeDAO.save({
|
||||||
|
uuid: s.uuid,
|
||||||
|
code,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
// 迁移订阅
|
||||||
|
const subscribe = await tx.table("subscribe").toArray();
|
||||||
|
const subscribeDAO = new SubscribeDAO();
|
||||||
|
await Promise.all(
|
||||||
|
subscribe.map((s: Subscribe) => {
|
||||||
|
const { url, name, code, author, scripts, metadata, status, createtime, updatetime, checktime } = s;
|
||||||
|
return subscribeDAO.save({
|
||||||
|
url,
|
||||||
|
name,
|
||||||
|
code,
|
||||||
|
author,
|
||||||
|
scripts,
|
||||||
|
metadata,
|
||||||
|
status,
|
||||||
|
createtime,
|
||||||
|
updatetime,
|
||||||
|
checktime,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
// 迁移value
|
||||||
|
interface MV2Value {
|
||||||
|
id: number;
|
||||||
|
scriptId: number;
|
||||||
|
storageName?: string;
|
||||||
|
key: string;
|
||||||
|
value: any;
|
||||||
|
createtime: number;
|
||||||
|
updatetime: number;
|
||||||
|
}
|
||||||
|
const values = await tx.table("value").toArray();
|
||||||
|
const valueDAO = new ScriptCodeDAO();
|
||||||
|
await Promise.all(
|
||||||
|
values.map((v) => {
|
||||||
|
const { scriptId, storageName, key, value, createtime } = v;
|
||||||
|
return valueDAO.save({
|
||||||
|
scriptId,
|
||||||
|
storageName,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
createtime,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
// 迁移permission
|
||||||
|
});
|
||||||
|
return db.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function migrate() {
|
export default function migrate() {
|
||||||
@ -90,7 +182,8 @@ export default function migrate() {
|
|||||||
value: "++id,scriptId,storageName,key,createtime",
|
value: "++id,scriptId,storageName,key,createtime",
|
||||||
})
|
})
|
||||||
.upgrade((tx) => {
|
.upgrade((tx) => {
|
||||||
tx.table("value")
|
return tx
|
||||||
|
.table("value")
|
||||||
.toCollection()
|
.toCollection()
|
||||||
.modify((value) => {
|
.modify((value) => {
|
||||||
if (value.namespace) {
|
if (value.namespace) {
|
||||||
@ -112,5 +205,5 @@ export default function migrate() {
|
|||||||
permission: "++id,scriptId,[scriptId+permission+permissionValue],createtime,updatetime",
|
permission: "++id,scriptId,[scriptId+permission+permissionValue],createtime,updatetime",
|
||||||
});
|
});
|
||||||
// 使用小峰驼统一命名规范
|
// 使用小峰驼统一命名规范
|
||||||
renameField();
|
return renameField();
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ export abstract class Repo<T> {
|
|||||||
return this.prefix + key;
|
return this.prefix + key;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async _save(key: string, val: T):Promise<T> {
|
protected async _save(key: string, val: T): Promise<T> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const data = {
|
const data = {
|
||||||
[this.joinKey(key)]: val,
|
[this.joinKey(key)]: val,
|
||||||
|
@ -12,7 +12,6 @@ export interface SubscribeScript {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Subscribe {
|
export interface Subscribe {
|
||||||
id: number;
|
|
||||||
url: string;
|
url: string;
|
||||||
name: string;
|
name: string;
|
||||||
code: string;
|
code: string;
|
||||||
@ -31,6 +30,10 @@ export class SubscribeDAO extends Repo<Subscribe> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public findByUrl(url: string) {
|
public findByUrl(url: string) {
|
||||||
return this.findOne((key, value) => value.url === url);
|
return this.get(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
public save(val: Subscribe) {
|
||||||
|
return super._save(val.url, val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { WindowMessage } from "@Packages/message/window_message";
|
import { WindowMessage } from "@Packages/message/window_message";
|
||||||
import { SCRIPT_RUN_STATUS, ScriptRunResouce } from "@App/app/repo/scripts";
|
import { SCRIPT_RUN_STATUS, ScriptRunResouce } from "@App/app/repo/scripts";
|
||||||
import { sendMessage } from "@Packages/message/client";
|
import { sendMessage } from "@Packages/message/client";
|
||||||
import { MessageSend } from "@Packages/message/server";
|
import { MessageSend } from "@Packages/message/server";
|
||||||
|
|
||||||
export function preparationSandbox(msg: WindowMessage) {
|
export function preparationSandbox(msg: WindowMessage) {
|
||||||
return sendMessage(msg, "offscreen/preparationSandbox");
|
return sendMessage(msg, "offscreen/preparationSandbox");
|
||||||
@ -31,3 +31,7 @@ export function runScript(msg: MessageSend, data: ScriptRunResouce) {
|
|||||||
export function stopScript(msg: MessageSend, uuid: string) {
|
export function stopScript(msg: MessageSend, uuid: string) {
|
||||||
return sendMessage(msg, "offscreen/script/stopScript", uuid);
|
return sendMessage(msg, "offscreen/script/stopScript", uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createObjectURL(msg: MessageSend, data: Blob) {
|
||||||
|
return sendMessage(msg, "offscreen/createObjectURL", data);
|
||||||
|
}
|
||||||
|
@ -54,5 +54,14 @@ export class OffscreenManager {
|
|||||||
|
|
||||||
const gmApi = new GMApi(this.windowServer.group("gmApi"));
|
const gmApi = new GMApi(this.windowServer.group("gmApi"));
|
||||||
gmApi.init();
|
gmApi.init();
|
||||||
|
|
||||||
|
this.windowServer.on("createObjectURL", (data: Blob) => {
|
||||||
|
console.log("createObjectURL", data);
|
||||||
|
const url = URL.createObjectURL(data);
|
||||||
|
setTimeout(() => {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}, 1000 * 60);
|
||||||
|
return Promise.resolve(url);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import { MessageQueue } from "@Packages/message/message_queue";
|
import { MessageQueue } from "@Packages/message/message_queue";
|
||||||
import { Script, SCRIPT_RUN_STATUS } from "../repo/scripts";
|
import { Script, SCRIPT_RUN_STATUS } from "../repo/scripts";
|
||||||
|
import { InstallSource } from "./service_worker";
|
||||||
|
import { Subscribe } from "../repo/subscribe";
|
||||||
|
|
||||||
export function subscribeScriptInstall(
|
export function subscribeScriptInstall(
|
||||||
messageQueue: MessageQueue,
|
messageQueue: MessageQueue,
|
||||||
callback: (message: { script: Script; update: boolean }) => void
|
callback: (message: { script: Script; update: boolean; upsertBy: InstallSource }) => void
|
||||||
) {
|
) {
|
||||||
return messageQueue.subscribe("installScript", callback);
|
return messageQueue.subscribe("installScript", callback);
|
||||||
}
|
}
|
||||||
@ -12,6 +14,17 @@ export function subscribeScriptDelete(messageQueue: MessageQueue, callback: (mes
|
|||||||
return messageQueue.subscribe("deleteScript", callback);
|
return messageQueue.subscribe("deleteScript", callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function subscribeSubscribeInstall(
|
||||||
|
messageQueue: MessageQueue,
|
||||||
|
callback: (message: { subscribe: Subscribe; update: boolean }) => void
|
||||||
|
) {
|
||||||
|
return messageQueue.subscribe("installSubscribe", callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function publishSubscribeInstall(messageQueue: MessageQueue, message: { subscribe: Subscribe }) {
|
||||||
|
return messageQueue.publish("installSubscribe", message);
|
||||||
|
}
|
||||||
|
|
||||||
export type ScriptEnableCallbackValue = { uuid: string; enable: boolean };
|
export type ScriptEnableCallbackValue = { uuid: string; enable: boolean };
|
||||||
|
|
||||||
export function subscribeScriptEnable(
|
export function subscribeScriptEnable(
|
||||||
|
@ -5,6 +5,11 @@ import { Resource } from "@App/app/repo/resource";
|
|||||||
import { MessageSend } from "@Packages/message/server";
|
import { MessageSend } from "@Packages/message/server";
|
||||||
import { ScriptMenu, ScriptMenuItem } from "./popup";
|
import { ScriptMenu, ScriptMenuItem } from "./popup";
|
||||||
import PermissionVerify, { ConfirmParam, UserConfirm } from "./permission_verify";
|
import PermissionVerify, { ConfirmParam, UserConfirm } from "./permission_verify";
|
||||||
|
import { FileSystemType } from "@Packages/filesystem/factory";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import Cache from "@App/app/cache";
|
||||||
|
import CacheKey from "@App/app/cache_key";
|
||||||
|
import { Subscribe } from "@App/app/repo/subscribe";
|
||||||
|
|
||||||
export class ServiceWorkerClient extends Client {
|
export class ServiceWorkerClient extends Client {
|
||||||
constructor(msg: MessageSend) {
|
constructor(msg: MessageSend) {
|
||||||
@ -150,3 +155,56 @@ export class PermissionClient extends Client {
|
|||||||
return this.do("getInfo", uuid);
|
return this.do("getInfo", uuid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class SynchronizeClient extends Client {
|
||||||
|
constructor(msg: MessageSend) {
|
||||||
|
super(msg, "serviceWorker/synchronize");
|
||||||
|
}
|
||||||
|
|
||||||
|
export(uuids?: string[]) {
|
||||||
|
return this.do("export", uuids);
|
||||||
|
}
|
||||||
|
|
||||||
|
backupToCloud(type: FileSystemType, params: any) {
|
||||||
|
return this.do("backupToCloud", { type, params });
|
||||||
|
}
|
||||||
|
|
||||||
|
async openImportWindow(filename: string, file: File | Blob) {
|
||||||
|
// 打开导入窗口,用cache实现数据交互
|
||||||
|
const url = URL.createObjectURL(file);
|
||||||
|
setTimeout(() => {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}, 60 * 1000);
|
||||||
|
const uuid = uuidv4();
|
||||||
|
await Cache.getInstance().set(CacheKey.importFile(uuid), {
|
||||||
|
filename: filename,
|
||||||
|
url: url,
|
||||||
|
});
|
||||||
|
// 打开导入窗口,用cache实现数据交互
|
||||||
|
chrome.tabs.create({
|
||||||
|
url: `/src/import.html?uuid=${uuid}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SubscribeClient extends Client {
|
||||||
|
constructor(msg: MessageSend) {
|
||||||
|
super(msg, "serviceWorker/subscribe");
|
||||||
|
}
|
||||||
|
|
||||||
|
install(subscribe: Subscribe) {
|
||||||
|
return this.do("install", { subscribe });
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(url: string) {
|
||||||
|
return this.do("delete", { url });
|
||||||
|
}
|
||||||
|
|
||||||
|
checkUpdate(url: string) {
|
||||||
|
return this.do("checkUpdate", { url });
|
||||||
|
}
|
||||||
|
|
||||||
|
enable(url: string, enable: boolean) {
|
||||||
|
return this.do("enable", { url, enable });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -347,7 +347,7 @@ export default class GMApi {
|
|||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
}, 6000);
|
}, 30 * 1000);
|
||||||
return { action: "onload", data: url };
|
return { action: "onload", data: url };
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
return { action: "error", data: { code: 5, error: e.message } };
|
return { action: "error", data: { code: 5, error: e.message } };
|
||||||
|
@ -7,6 +7,8 @@ import { RuntimeService } from "./runtime";
|
|||||||
import { ServiceWorkerMessageSend } from "@Packages/message/window_message";
|
import { ServiceWorkerMessageSend } from "@Packages/message/window_message";
|
||||||
import { PopupService } from "./popup";
|
import { PopupService } from "./popup";
|
||||||
import { SystemConfig } from "@App/pkg/config/config";
|
import { SystemConfig } from "@App/pkg/config/config";
|
||||||
|
import { SynchronizeService } from "./synchronize";
|
||||||
|
import { SubscribeService } from "./subscribe";
|
||||||
|
|
||||||
export type InstallSource = "user" | "system" | "sync" | "subscribe" | "vscode";
|
export type InstallSource = "user" | "system" | "sync" | "subscribe" | "vscode";
|
||||||
|
|
||||||
@ -24,6 +26,7 @@ export default class ServiceWorkerManager {
|
|||||||
await this.sender.init();
|
await this.sender.init();
|
||||||
this.mq.emit("preparationOffscreen", {});
|
this.mq.emit("preparationOffscreen", {});
|
||||||
});
|
});
|
||||||
|
this.sender.init();
|
||||||
|
|
||||||
const systemConfig = new SystemConfig(this.mq);
|
const systemConfig = new SystemConfig(this.mq);
|
||||||
|
|
||||||
@ -32,11 +35,31 @@ export default class ServiceWorkerManager {
|
|||||||
const value = new ValueService(this.api.group("value"), this.sender);
|
const value = new ValueService(this.api.group("value"), this.sender);
|
||||||
const script = new ScriptService(systemConfig, this.api.group("script"), this.mq, value, resource);
|
const script = new ScriptService(systemConfig, this.api.group("script"), this.mq, value, resource);
|
||||||
script.init();
|
script.init();
|
||||||
const runtime = new RuntimeService(systemConfig, this.api.group("runtime"), this.sender, this.mq, value, script);
|
const runtime = new RuntimeService(
|
||||||
|
systemConfig,
|
||||||
|
this.api.group("runtime"),
|
||||||
|
this.sender,
|
||||||
|
this.mq,
|
||||||
|
value,
|
||||||
|
script,
|
||||||
|
resource
|
||||||
|
);
|
||||||
runtime.init();
|
runtime.init();
|
||||||
const popup = new PopupService(this.api.group("popup"), this.mq, runtime);
|
const popup = new PopupService(this.api.group("popup"), this.mq, runtime);
|
||||||
popup.init();
|
popup.init();
|
||||||
value.init(runtime, popup);
|
value.init(runtime, popup);
|
||||||
|
const synchronize = new SynchronizeService(
|
||||||
|
this.sender,
|
||||||
|
this.api.group("synchronize"),
|
||||||
|
script,
|
||||||
|
value,
|
||||||
|
resource,
|
||||||
|
this.mq,
|
||||||
|
systemConfig
|
||||||
|
);
|
||||||
|
synchronize.init();
|
||||||
|
const subscribe = new SubscribeService(systemConfig, this.api.group("subscribe"), this.mq, script);
|
||||||
|
subscribe.init();
|
||||||
|
|
||||||
// 定时器处理
|
// 定时器处理
|
||||||
chrome.alarms.onAlarm.addListener((alarm) => {
|
chrome.alarms.onAlarm.addListener((alarm) => {
|
||||||
@ -44,7 +67,33 @@ export default class ServiceWorkerManager {
|
|||||||
case "checkScriptUpdate":
|
case "checkScriptUpdate":
|
||||||
script.checkScriptUpdate();
|
script.checkScriptUpdate();
|
||||||
break;
|
break;
|
||||||
|
case "cloudSync":
|
||||||
|
// 进行一次云同步
|
||||||
|
systemConfig.getCloudSync().then((config) => {
|
||||||
|
synchronize.buildFileSystem(config).then((fs) => {
|
||||||
|
synchronize.syncOnce(fs);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "checkSubscribeUpdate":
|
||||||
|
subscribe.checkSubscribeUpdate();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 监听配置变化
|
||||||
|
this.mq.subscribe("systemConfigChange", (msg) => {
|
||||||
|
console.log("systemConfigChange", msg);
|
||||||
|
switch (msg.key) {
|
||||||
|
case "cloud_sync": {
|
||||||
|
synchronize.cloudSyncConfigChange(msg.value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 启动一次云同步
|
||||||
|
systemConfig.getCloudSync().then((config) => {
|
||||||
|
synchronize.cloudSyncConfigChange(config);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,12 +36,22 @@ export class ResourceService {
|
|||||||
return Promise.resolve(undefined);
|
return Promise.resolve(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cache: Map<string, { [key: string]: Resource }> = new Map();
|
||||||
|
|
||||||
public async getScriptResources(script: Script): Promise<{ [key: string]: Resource }> {
|
public async getScriptResources(script: Script): Promise<{ [key: string]: Resource }> {
|
||||||
return Promise.resolve({
|
// 优先从内存中获取
|
||||||
|
if (this.cache.has(script.uuid)) {
|
||||||
|
return Promise.resolve(this.cache.get(script.uuid) || {});
|
||||||
|
}
|
||||||
|
// 资源不存在,重新加载
|
||||||
|
const res = await Promise.resolve({
|
||||||
...((await this.getResourceByType(script, "require")) || {}),
|
...((await this.getResourceByType(script, "require")) || {}),
|
||||||
...((await this.getResourceByType(script, "require-css")) || {}),
|
...((await this.getResourceByType(script, "require-css")) || {}),
|
||||||
...((await this.getResourceByType(script, "resource")) || {}),
|
...((await this.getResourceByType(script, "resource")) || {}),
|
||||||
});
|
});
|
||||||
|
// 缓存到内存
|
||||||
|
this.cache.set(script.uuid, res);
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getResourceByType(script: Script, type: ResourceType): Promise<{ [key: string]: Resource }> {
|
async getResourceByType(script: Script, type: ResourceType): Promise<{ [key: string]: Resource }> {
|
||||||
@ -159,6 +169,8 @@ export class ResourceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async addResource(url: string, uuid: string, type: ResourceType): Promise<Resource> {
|
public async addResource(url: string, uuid: string, type: ResourceType): Promise<Resource> {
|
||||||
|
// 删除缓存
|
||||||
|
this.cache.delete(uuid);
|
||||||
const u = this.parseUrl(url);
|
const u = this.parseUrl(url);
|
||||||
let result = await this.getResourceModel(u.url);
|
let result = await this.getResourceModel(u.url);
|
||||||
// 资源不存在,重新加载
|
// 资源不存在,重新加载
|
||||||
|
@ -26,8 +26,9 @@ import Logger from "@App/app/logger/logger";
|
|||||||
import LoggerCore from "@App/app/logger/core";
|
import LoggerCore from "@App/app/logger/core";
|
||||||
import PermissionVerify from "./permission_verify";
|
import PermissionVerify from "./permission_verify";
|
||||||
import { SystemConfig } from "@App/pkg/config/config";
|
import { SystemConfig } from "@App/pkg/config/config";
|
||||||
|
import { ResourceService } from "./resource";
|
||||||
|
|
||||||
// 为了优化性能,存储到缓存时删除了code与value
|
// 为了优化性能,存储到缓存时删除了code、value与resource
|
||||||
export interface ScriptMatchInfo extends ScriptRunResouce {
|
export interface ScriptMatchInfo extends ScriptRunResouce {
|
||||||
matches: string[];
|
matches: string[];
|
||||||
excludeMatches: string[];
|
excludeMatches: string[];
|
||||||
@ -54,7 +55,8 @@ export class RuntimeService {
|
|||||||
private sender: MessageSend,
|
private sender: MessageSend,
|
||||||
private mq: MessageQueue,
|
private mq: MessageQueue,
|
||||||
private value: ValueService,
|
private value: ValueService,
|
||||||
private script: ScriptService
|
private script: ScriptService,
|
||||||
|
private resource: ResourceService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
@ -196,33 +198,36 @@ export class RuntimeService {
|
|||||||
// 匹配当前页面的脚本
|
// 匹配当前页面的脚本
|
||||||
const matchScriptUuid = await this.getPageScriptUuidByUrl(chromeSender.url!);
|
const matchScriptUuid = await this.getPageScriptUuidByUrl(chromeSender.url!);
|
||||||
|
|
||||||
const scripts = await Promise.all(
|
const scripts = matchScriptUuid.map((uuid) => {
|
||||||
matchScriptUuid.map(async (uuid): Promise<undefined | ScriptRunResouce> => {
|
const scriptRes = Object.assign({}, this.scriptMatchCache?.get(uuid));
|
||||||
const scriptRes = Object.assign({}, this.scriptMatchCache?.get(uuid));
|
// 判断脚本是否开启
|
||||||
// 判断脚本是否开启
|
if (scriptRes.status === SCRIPT_STATUS_DISABLE) {
|
||||||
if (scriptRes.status === SCRIPT_STATUS_DISABLE) {
|
return undefined;
|
||||||
|
}
|
||||||
|
// 如果是iframe,判断是否允许在iframe里运行
|
||||||
|
if (chromeSender.frameId !== undefined) {
|
||||||
|
if (scriptRes.metadata.noframes) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
// 如果是iframe,判断是否允许在iframe里运行
|
}
|
||||||
if (chromeSender.frameId !== undefined) {
|
// 获取value
|
||||||
if (scriptRes.metadata.noframes) {
|
return scriptRes;
|
||||||
return undefined;
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
// 获取value
|
|
||||||
return scriptRes;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const enableScript = scripts.filter((item) => item);
|
const enableScript = scripts.filter((item) => item) as ScriptMatchInfo[];
|
||||||
|
|
||||||
// 加载value
|
await Promise.all([
|
||||||
await Promise.all(
|
// 加载value
|
||||||
enableScript.map(async (script) => {
|
...enableScript.map(async (script) => {
|
||||||
const value = await this.value.getScriptValue(script!);
|
const value = await this.value.getScriptValue(script!);
|
||||||
script!.value = value;
|
script.value = value;
|
||||||
})
|
}),
|
||||||
);
|
// 加载resource
|
||||||
|
...enableScript.map(async (script) => {
|
||||||
|
const resource = await this.resource.getScriptResources(script);
|
||||||
|
script.resource = resource;
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
this.mq.emit("pageLoad", {
|
this.mq.emit("pageLoad", {
|
||||||
tabId: chromeSender.tab?.id,
|
tabId: chromeSender.tab?.id,
|
||||||
@ -325,8 +330,10 @@ export class RuntimeService {
|
|||||||
this.scriptMatchCache.forEach((val, key) => {
|
this.scriptMatchCache.forEach((val, key) => {
|
||||||
scriptMatch[key] = val;
|
scriptMatch[key] = val;
|
||||||
// 优化性能,将不需要的信息去掉
|
// 优化性能,将不需要的信息去掉
|
||||||
|
// 而且可能会超过缓存的存储限制
|
||||||
scriptMatch[key].code = "";
|
scriptMatch[key].code = "";
|
||||||
scriptMatch[key].value = {};
|
scriptMatch[key].value = {};
|
||||||
|
scriptMatch[key].resource = {};
|
||||||
});
|
});
|
||||||
return await Cache.getInstance().set("scriptMatch", scriptMatch);
|
return await Cache.getInstance().set("scriptMatch", scriptMatch);
|
||||||
}
|
}
|
||||||
|
@ -59,7 +59,7 @@ export class ScriptService {
|
|||||||
// 读取脚本url内容, 进行安装
|
// 读取脚本url内容, 进行安装
|
||||||
const logger = this.logger.with({ url: targetUrl });
|
const logger = this.logger.with({ url: targetUrl });
|
||||||
logger.debug("install script");
|
logger.debug("install script");
|
||||||
this.openInstallPageByUrl(targetUrl).catch((e) => {
|
this.openInstallPageByUrl(targetUrl, "user").catch((e) => {
|
||||||
logger.error("install script error", Logger.E(e));
|
logger.error("install script error", Logger.E(e));
|
||||||
// 如果打开失败, 则重定向到安装页
|
// 如果打开失败, 则重定向到安装页
|
||||||
chrome.scripting.executeScript({
|
chrome.scripting.executeScript({
|
||||||
@ -135,18 +135,31 @@ export class ScriptService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public openInstallPageByUrl(url: string) {
|
public openInstallPageByUrl(url: string, source: InstallSource) {
|
||||||
const uuid = uuidv4();
|
const uuid = uuidv4();
|
||||||
return fetchScriptInfo(url, "user", false, uuidv4()).then((info) => {
|
return fetchScriptInfo(url, source, false, uuidv4()).then((info) => {
|
||||||
Cache.getInstance().set(CacheKey.scriptInstallInfo(uuid), info);
|
Cache.getInstance().set(CacheKey.scriptInstallInfo(uuid), info);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// 清理缓存
|
// 清理缓存
|
||||||
Cache.getInstance().del(CacheKey.scriptInstallInfo(uuid));
|
Cache.getInstance().del(CacheKey.scriptInstallInfo(uuid));
|
||||||
}, 60 * 1000);
|
}, 30 * 1000);
|
||||||
openInCurrentTab(`/src/install.html?uuid=${uuid}`);
|
openInCurrentTab(`/src/install.html?uuid=${uuid}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 直接通过url静默安装脚本
|
||||||
|
async installByUrl(url: string, source: InstallSource, subscribeUrl?: string) {
|
||||||
|
const info = await fetchScriptInfo(url, source, false, uuidv4());
|
||||||
|
const prepareScript = await prepareScriptByCode(info.code, url, info.uuid);
|
||||||
|
prepareScript.script.subscribeUrl = subscribeUrl;
|
||||||
|
this.installScript({
|
||||||
|
script: prepareScript.script,
|
||||||
|
code: info.code,
|
||||||
|
upsertBy: source,
|
||||||
|
});
|
||||||
|
return Promise.resolve(prepareScript.script);
|
||||||
|
}
|
||||||
|
|
||||||
// 获取安装信息
|
// 获取安装信息
|
||||||
getInstallInfo(uuid: string) {
|
getInstallInfo(uuid: string) {
|
||||||
return Cache.getInstance().get(CacheKey.scriptInstallInfo(uuid));
|
return Cache.getInstance().get(CacheKey.scriptInstallInfo(uuid));
|
||||||
@ -179,7 +192,7 @@ export class ScriptService {
|
|||||||
});
|
});
|
||||||
logger.info("install success");
|
logger.info("install success");
|
||||||
// 广播一下
|
// 广播一下
|
||||||
this.mq.publish("installScript", { script, update });
|
this.mq.publish("installScript", { script, update, upsertBy });
|
||||||
return Promise.resolve({ update });
|
return Promise.resolve({ update });
|
||||||
})
|
})
|
||||||
.catch((e: any) => {
|
.catch((e: any) => {
|
||||||
@ -330,7 +343,7 @@ export class ScriptService {
|
|||||||
}
|
}
|
||||||
const newVersion = metadata.version && metadata.version[0];
|
const newVersion = metadata.version && metadata.version[0];
|
||||||
if (!newVersion) {
|
if (!newVersion) {
|
||||||
logger.error("parse version failed", { version: "" });
|
logger.error("parse version failed", { version: metadata.version });
|
||||||
return Promise.resolve(false);
|
return Promise.resolve(false);
|
||||||
}
|
}
|
||||||
let oldVersion = script.metadata.version && script.metadata.version[0];
|
let oldVersion = script.metadata.version && script.metadata.version[0];
|
||||||
@ -393,16 +406,16 @@ export class ScriptService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
checkScriptUpdate() {
|
async checkScriptUpdate() {
|
||||||
|
const checkCycle = await this.systemConfig.getCheckScriptUpdateCycle();
|
||||||
|
if (!checkCycle) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.scriptDAO.all().then(async (scripts) => {
|
this.scriptDAO.all().then(async (scripts) => {
|
||||||
const checkCycle = await this.systemConfig.getCheckScriptUpdateCycle();
|
const checkDisableScript = await this.systemConfig.getUpdateDisableScript();
|
||||||
if (!checkCycle) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const check = await this.systemConfig.getUpdateDisableScript();
|
|
||||||
scripts.forEach(async (script) => {
|
scripts.forEach(async (script) => {
|
||||||
// 是否检查禁用脚本
|
// 是否检查禁用脚本
|
||||||
if (!check && script.status === SCRIPT_STATUS_DISABLE) {
|
if (!checkDisableScript && script.status === SCRIPT_STATUS_DISABLE) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 检查是否符合
|
// 检查是否符合
|
||||||
|
280
src/app/service/service_worker/subscribe.ts
Normal file
280
src/app/service/service_worker/subscribe.ts
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
import LoggerCore from "@App/app/logger/core";
|
||||||
|
import Logger from "@App/app/logger/logger";
|
||||||
|
import { ScriptDAO } from "@App/app/repo/scripts";
|
||||||
|
import {
|
||||||
|
Subscribe,
|
||||||
|
SUBSCRIBE_STATUS_DISABLE,
|
||||||
|
SUBSCRIBE_STATUS_ENABLE,
|
||||||
|
SubscribeDAO,
|
||||||
|
SubscribeScript,
|
||||||
|
} from "@App/app/repo/subscribe";
|
||||||
|
import { SystemConfig } from "@App/pkg/config/config";
|
||||||
|
import { MessageQueue } from "@Packages/message/message_queue";
|
||||||
|
import { Group } from "@Packages/message/server";
|
||||||
|
import { InstallSource } from ".";
|
||||||
|
import { publishSubscribeInstall, subscribeSubscribeInstall } from "../queue";
|
||||||
|
import { ScriptService } from "./script";
|
||||||
|
import { checkSilenceUpdate, InfoNotification, ltever } from "@App/pkg/utils/utils";
|
||||||
|
import { fetchScriptInfo, prepareSubscribeByCode, ScriptInfo } from "@App/pkg/utils/script";
|
||||||
|
import Cache from "@App/app/cache";
|
||||||
|
import CacheKey from "@App/app/cache_key";
|
||||||
|
|
||||||
|
export class SubscribeService {
|
||||||
|
logger: Logger;
|
||||||
|
subscribeDAO = new SubscribeDAO();
|
||||||
|
scriptDAO = new ScriptDAO();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private systemConfig: SystemConfig,
|
||||||
|
private group: Group,
|
||||||
|
private mq: MessageQueue,
|
||||||
|
private scriptService: ScriptService
|
||||||
|
) {
|
||||||
|
this.logger = LoggerCore.logger().with({ service: "subscribe" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async install(param: { subscribe: Subscribe }) {
|
||||||
|
const logger = this.logger.with({
|
||||||
|
subscribeUrl: param.subscribe.url,
|
||||||
|
name: param.subscribe.name,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await this.subscribeDAO.save(param.subscribe);
|
||||||
|
logger.info("upsert subscribe success");
|
||||||
|
publishSubscribeInstall(this.mq, {
|
||||||
|
subscribe: param.subscribe,
|
||||||
|
});
|
||||||
|
return Promise.resolve(param.subscribe.url);
|
||||||
|
} catch (e) {
|
||||||
|
logger.error("upsert subscribe error", Logger.E(e));
|
||||||
|
return Promise.reject(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(param: { url: string }) {
|
||||||
|
const logger = this.logger.with({
|
||||||
|
subscribeUrl: param.url,
|
||||||
|
});
|
||||||
|
const subscribe = await this.subscribeDAO.get(param.url);
|
||||||
|
if (!subscribe) {
|
||||||
|
logger.warn("subscribe not found");
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// 删除相关脚本
|
||||||
|
const scripts = await this.scriptDAO.find((_, value) => {
|
||||||
|
return value.subscribeUrl === param.url;
|
||||||
|
});
|
||||||
|
scripts.forEach((script) => {
|
||||||
|
this.scriptService.deleteScript(script.uuid);
|
||||||
|
});
|
||||||
|
// 删除订阅
|
||||||
|
await this.subscribeDAO.delete(param.url);
|
||||||
|
logger.info("delete subscribe success");
|
||||||
|
return Promise.resolve(true);
|
||||||
|
} catch (e) {
|
||||||
|
logger.error("uninstall subscribe error", Logger.E(e));
|
||||||
|
return Promise.reject(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新订阅的脚本
|
||||||
|
async upsertScript(subscribe: Subscribe) {
|
||||||
|
const logger = this.logger.with({
|
||||||
|
url: subscribe.url,
|
||||||
|
name: subscribe.name,
|
||||||
|
});
|
||||||
|
// 对比脚本是否有变化
|
||||||
|
const addScript: string[] = [];
|
||||||
|
const removeScript: SubscribeScript[] = [];
|
||||||
|
const scriptUrl = subscribe.metadata.scripturl || [];
|
||||||
|
const scripts = Object.keys(subscribe.scripts);
|
||||||
|
scriptUrl.forEach((url) => {
|
||||||
|
// 不存在于已安装的脚本中, 则添加
|
||||||
|
if (!scripts.includes(url)) {
|
||||||
|
addScript.push(url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
scripts.forEach((url) => {
|
||||||
|
// 不存在于订阅的脚本中, 则删除
|
||||||
|
if (!scriptUrl.includes(url)) {
|
||||||
|
removeScript.push(subscribe.scripts[url]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const notification: string[][] = [[], []];
|
||||||
|
const result: Promise<any>[] = [];
|
||||||
|
// 添加脚本
|
||||||
|
addScript.forEach((url) => {
|
||||||
|
result.push(
|
||||||
|
(async () => {
|
||||||
|
const script = await this.scriptService.installByUrl(url, "subscribe", subscribe.url);
|
||||||
|
subscribe.scripts[url] = {
|
||||||
|
url,
|
||||||
|
uuid: script.uuid,
|
||||||
|
};
|
||||||
|
notification[0].push(script.name);
|
||||||
|
return Promise.resolve(true);
|
||||||
|
})().catch((e) => {
|
||||||
|
logger.error("install script failed", Logger.E(e));
|
||||||
|
return Promise.resolve(false);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
// 删除脚本
|
||||||
|
removeScript.forEach((item) => {
|
||||||
|
// 通过uuid查询脚本id
|
||||||
|
result.push(
|
||||||
|
(async () => {
|
||||||
|
const script = await this.scriptDAO.findByUUID(item.uuid);
|
||||||
|
if (script) {
|
||||||
|
notification[1].push(script.name);
|
||||||
|
// 删除脚本
|
||||||
|
this.scriptService.deleteScript(script.uuid);
|
||||||
|
}
|
||||||
|
return Promise.resolve(true);
|
||||||
|
})().catch((e) => {
|
||||||
|
logger.error("delete script failed", Logger.E(e));
|
||||||
|
return Promise.resolve(false);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.allSettled(result);
|
||||||
|
|
||||||
|
await this.subscribeDAO.update(subscribe.url, subscribe);
|
||||||
|
|
||||||
|
InfoNotification("订阅更新", `安装了:${notification[0].join(",")}\n删除了:${notification[1].join("\n")}`);
|
||||||
|
|
||||||
|
logger.info("subscribe update", {
|
||||||
|
install: notification[0],
|
||||||
|
update: notification[1],
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查更新
|
||||||
|
async checkUpdate(url: string, source: InstallSource) {
|
||||||
|
const subscribe = await this.subscribeDAO.get(url);
|
||||||
|
if (!subscribe) {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
const logger = this.logger.with({
|
||||||
|
url: subscribe.url,
|
||||||
|
name: subscribe.name,
|
||||||
|
});
|
||||||
|
await this.subscribeDAO.update(url, { checktime: new Date().getTime() });
|
||||||
|
try {
|
||||||
|
const info = await fetchScriptInfo(subscribe.url, source, false, subscribe.url);
|
||||||
|
const { metadata } = info;
|
||||||
|
if (!metadata) {
|
||||||
|
logger.error("parse metadata failed");
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
const newVersion = metadata.version && metadata.version[0];
|
||||||
|
if (!newVersion) {
|
||||||
|
logger.error("parse version failed", { version: metadata.version });
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
let oldVersion = subscribe.metadata.version && subscribe.metadata.version[0];
|
||||||
|
if (!oldVersion) {
|
||||||
|
oldVersion = "0.0.0";
|
||||||
|
}
|
||||||
|
// 对比版本大小
|
||||||
|
if (ltever(newVersion, oldVersion, logger)) {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
// 进行更新
|
||||||
|
this.openUpdatePage(info);
|
||||||
|
return Promise.resolve(true);
|
||||||
|
} catch (e) {
|
||||||
|
logger.error("check update failed", Logger.E(e));
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async openUpdatePage(info: ScriptInfo) {
|
||||||
|
const logger = this.logger.with({
|
||||||
|
url: info.url,
|
||||||
|
});
|
||||||
|
// 是否静默更新
|
||||||
|
const silenceUpdate = await this.systemConfig.getSilenceUpdateScript();
|
||||||
|
if (silenceUpdate) {
|
||||||
|
try {
|
||||||
|
const newSubscribe = await prepareSubscribeByCode(info.code, info.url);
|
||||||
|
if (checkSilenceUpdate(newSubscribe.oldSubscribe!.metadata, newSubscribe.subscribe.metadata)) {
|
||||||
|
logger.info("silence update subscribe");
|
||||||
|
this.install({
|
||||||
|
subscribe: newSubscribe.subscribe,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.error("prepare script failed", Logger.E(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Cache.getInstance().set(CacheKey.scriptInstallInfo(info.uuid), info);
|
||||||
|
chrome.tabs.create({
|
||||||
|
url: `/src/install.html?uuid=${info.uuid}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkSubscribeUpdate() {
|
||||||
|
const checkCycle = await this.systemConfig.getCheckScriptUpdateCycle();
|
||||||
|
if (!checkCycle) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.logger.debug("start check update");
|
||||||
|
const checkDisable = await this.systemConfig.getUpdateDisableScript();
|
||||||
|
const list = await this.subscribeDAO.find((_, value) => {
|
||||||
|
return value.checktime + checkCycle * 1000 < Date.now();
|
||||||
|
});
|
||||||
|
|
||||||
|
list.forEach((subscribe) => {
|
||||||
|
if (!checkDisable && subscribe.status === SUBSCRIBE_STATUS_ENABLE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.checkUpdate(subscribe.url, "system");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
requestCheckUpdate(url: string) {
|
||||||
|
return this.checkUpdate(url, "user");
|
||||||
|
}
|
||||||
|
|
||||||
|
enable(param: { url: string; enable: boolean }) {
|
||||||
|
const logger = this.logger.with({
|
||||||
|
url: param.url,
|
||||||
|
});
|
||||||
|
return this.subscribeDAO
|
||||||
|
.update(param.url, {
|
||||||
|
status: param.enable ? SUBSCRIBE_STATUS_ENABLE : SUBSCRIBE_STATUS_DISABLE,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
logger.info("enable subscribe success");
|
||||||
|
return Promise.resolve(true);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
logger.error("enable subscribe error", Logger.E(e));
|
||||||
|
return Promise.reject(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.group.on("install", this.install.bind(this));
|
||||||
|
this.group.on("delete", this.delete.bind(this));
|
||||||
|
this.group.on("checkUpdate", this.requestCheckUpdate.bind(this));
|
||||||
|
this.group.on("enable", this.enable.bind(this));
|
||||||
|
|
||||||
|
subscribeSubscribeInstall(this.mq, (message) => {
|
||||||
|
this.upsertScript(message.subscribe);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 定时检查更新, 每10分钟检查一次
|
||||||
|
chrome.alarms.create("checkSubscribeUpdate", {
|
||||||
|
delayInMinutes: 10,
|
||||||
|
periodInMinutes: 10,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
551
src/app/service/service_worker/synchronize.ts
Normal file
551
src/app/service/service_worker/synchronize.ts
Normal file
@ -0,0 +1,551 @@
|
|||||||
|
import LoggerCore from "@App/app/logger/core";
|
||||||
|
import Logger from "@App/app/logger/logger";
|
||||||
|
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, { File } from "@Packages/filesystem/filesystem";
|
||||||
|
import ZipFileSystem from "@Packages/filesystem/zip/zip";
|
||||||
|
import { Group, MessageSend } from "@Packages/message/server";
|
||||||
|
import JSZip from "jszip";
|
||||||
|
import { ValueService } from "./value";
|
||||||
|
import { ResourceService } from "./resource";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { createObjectURL } from "../offscreen/client";
|
||||||
|
import FileSystemFactory, { FileSystemType } from "@Packages/filesystem/factory";
|
||||||
|
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;
|
||||||
|
|
||||||
|
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 mq: MessageQueue,
|
||||||
|
private systemConfig: SystemConfig
|
||||||
|
) {
|
||||||
|
this.logger = LoggerCore.logger().with({ service: "synchronize" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成备份文件到文件系统
|
||||||
|
async backup(fs: FileSystem, uuids?: string[]) {
|
||||||
|
// 生成导出数据
|
||||||
|
const data: BackupData = {
|
||||||
|
script: await this.getScriptBackupData(uuids),
|
||||||
|
subscribe: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
await new BackupExport(fs).export(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取脚本备份数据
|
||||||
|
async getScriptBackupData(uuids?: string[]) {
|
||||||
|
if (uuids) {
|
||||||
|
const rets: Promise<ScriptBackupData>[] = [];
|
||||||
|
uuids.forEach((uuid) => {
|
||||||
|
rets.push(
|
||||||
|
this.scriptDAO.get(uuid).then((script) => {
|
||||||
|
if (script) {
|
||||||
|
return this.generateScriptBackupData(script);
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(`Script ${uuid} not found`));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return Promise.all(rets);
|
||||||
|
}
|
||||||
|
// 获取所有脚本
|
||||||
|
const list = await this.scriptDAO.all();
|
||||||
|
return Promise.all(list.map(async (script): Promise<ScriptBackupData> => this.generateScriptBackupData(script)));
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateScriptBackupData(script: Script): Promise<ScriptBackupData> {
|
||||||
|
const code = await this.scriptCodeDAO.get(script.uuid);
|
||||||
|
if (!code) {
|
||||||
|
throw new Error(`Script ${script.uuid} code not found`);
|
||||||
|
}
|
||||||
|
const ret = {
|
||||||
|
code: code.code,
|
||||||
|
options: {
|
||||||
|
options: this.scriptOption(script),
|
||||||
|
settings: {
|
||||||
|
enabled: script.status === SCRIPT_STATUS_ENABLE,
|
||||||
|
position: script.sort,
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
name: script.name,
|
||||||
|
uuid: script.uuid,
|
||||||
|
sc_uuid: script.uuid,
|
||||||
|
modified: script.updatetime,
|
||||||
|
file_url: script.downloadUrl,
|
||||||
|
subscribe_url: script.subscribeUrl,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// storage,
|
||||||
|
requires: [],
|
||||||
|
requiresCss: [],
|
||||||
|
resources: [],
|
||||||
|
} as unknown as ScriptBackupData;
|
||||||
|
const storage: ValueStorage = {
|
||||||
|
data: {},
|
||||||
|
ts: new Date().getTime(),
|
||||||
|
};
|
||||||
|
const values = await this.value.getScriptValue(script);
|
||||||
|
Object.keys(values).forEach((key) => {
|
||||||
|
storage.data[key] = values[key];
|
||||||
|
});
|
||||||
|
|
||||||
|
const requires = await this.resource.getResourceByType(script, "require");
|
||||||
|
const requiresCss = await this.resource.getResourceByType(script, "require-css");
|
||||||
|
const resources = await this.resource.getResourceByType(script, "resource");
|
||||||
|
|
||||||
|
ret.requires = this.resourceToBackdata(requires);
|
||||||
|
ret.requiresCss = this.resourceToBackdata(requiresCss);
|
||||||
|
ret.resources = this.resourceToBackdata(resources);
|
||||||
|
|
||||||
|
ret.storage = storage;
|
||||||
|
return Promise.resolve(ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceToBackdata(resource: { [key: string]: Resource }) {
|
||||||
|
const ret: ResourceBackup[] = [];
|
||||||
|
Object.keys(resource).forEach((key) => {
|
||||||
|
ret.push({
|
||||||
|
meta: {
|
||||||
|
name: this.getUrlName(resource[key].url),
|
||||||
|
url: resource[key].url,
|
||||||
|
ts: resource[key].updatetime || resource[key].createtime,
|
||||||
|
mimetype: resource[key].contentType,
|
||||||
|
},
|
||||||
|
source: resource[key]!.content || undefined,
|
||||||
|
base64: resource[key]!.base64,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
getUrlName(url: string): string {
|
||||||
|
let index = url.indexOf("?");
|
||||||
|
if (index !== -1) {
|
||||||
|
url = url.substring(0, index);
|
||||||
|
}
|
||||||
|
index = url.lastIndexOf("/");
|
||||||
|
if (index !== -1) {
|
||||||
|
url = url.substring(index + 1);
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为了兼容tm
|
||||||
|
scriptOption(script: Script): ScriptOptions {
|
||||||
|
return {
|
||||||
|
check_for_updates: false,
|
||||||
|
comment: null,
|
||||||
|
compat_foreach: false,
|
||||||
|
compat_metadata: false,
|
||||||
|
compat_prototypes: false,
|
||||||
|
compat_wrappedjsobject: false,
|
||||||
|
compatopts_for_requires: true,
|
||||||
|
noframes: null,
|
||||||
|
override: {
|
||||||
|
merge_connects: true,
|
||||||
|
merge_excludes: true,
|
||||||
|
merge_includes: true,
|
||||||
|
merge_matches: true,
|
||||||
|
orig_connects: script.metadata.connect || [],
|
||||||
|
orig_excludes: script.metadata.exclude || [],
|
||||||
|
orig_includes: script.metadata.include || [],
|
||||||
|
orig_matches: script.metadata.match || [],
|
||||||
|
orig_noframes: script.metadata.noframe ? true : null,
|
||||||
|
orig_run_at: (script.metadata.run_at && script.metadata.run_at[0]) || "document-idle",
|
||||||
|
use_blockers: [],
|
||||||
|
use_connects: [],
|
||||||
|
use_excludes: [],
|
||||||
|
use_includes: [],
|
||||||
|
use_matches: [],
|
||||||
|
},
|
||||||
|
run_at: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 请求导出文件
|
||||||
|
async requestExport(uuids?: string[]) {
|
||||||
|
const zip = new JSZip();
|
||||||
|
const fs = new ZipFileSystem(zip);
|
||||||
|
await this.backup(fs, uuids);
|
||||||
|
// 生成文件,并下载
|
||||||
|
const files = await zip.generateAsync({
|
||||||
|
type: "blob",
|
||||||
|
compression: "DEFLATE",
|
||||||
|
compressionOptions: {
|
||||||
|
level: 9,
|
||||||
|
},
|
||||||
|
comment: "Created by Scriptcat",
|
||||||
|
});
|
||||||
|
const url = await createObjectURL(this.send, files);
|
||||||
|
chrome.downloads.download({
|
||||||
|
url,
|
||||||
|
saveAs: true,
|
||||||
|
filename: `scriptcat-backup-${dayjs().format("YYYY-MM-DDTHH-mm-ss")}.zip`,
|
||||||
|
});
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 备份到云端
|
||||||
|
async backupToCloud({ type, params }: { type: FileSystemType; params: any }) {
|
||||||
|
// 首先生成zip文件
|
||||||
|
const zip = new JSZip();
|
||||||
|
const fs = new ZipFileSystem(zip);
|
||||||
|
await this.backup(fs);
|
||||||
|
this.logger.info("backup to cloud");
|
||||||
|
// 然后创建云端文件系统
|
||||||
|
let cloudFs = await FileSystemFactory.create(type, params);
|
||||||
|
try {
|
||||||
|
await cloudFs.createDir("ScriptCat");
|
||||||
|
cloudFs = await cloudFs.openDir("ScriptCat");
|
||||||
|
// 云端文件系统写入文件
|
||||||
|
const file = await cloudFs.create(`scriptcat-backup-${dayjs().format("YYYY-MM-DDTHH-mm-ss")}.zip`);
|
||||||
|
await file.write(
|
||||||
|
await zip.generateAsync({
|
||||||
|
type: "blob",
|
||||||
|
compression: "DEFLATE",
|
||||||
|
compressionOptions: {
|
||||||
|
level: 9,
|
||||||
|
},
|
||||||
|
comment: "Created by Scriptcat",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.error("backup to cloud error", Logger.E(e));
|
||||||
|
return Promise.reject(e);
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始一次云同步
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 同步一次
|
||||||
|
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.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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
@ -364,5 +364,8 @@
|
|||||||
"menu_expand_num_before": "菜单项超过",
|
"menu_expand_num_before": "菜单项超过",
|
||||||
"menu_expand_num_after": "个时,自动隐藏",
|
"menu_expand_num_after": "个时,自动隐藏",
|
||||||
"script_name_cannot_be_set_to_empty": "脚本name不可以设置为空",
|
"script_name_cannot_be_set_to_empty": "脚本name不可以设置为空",
|
||||||
"eslint_config_format_error": "eslint配置格式错误"
|
"eslint_config_format_error": "eslint配置格式错误",
|
||||||
|
"export_success": "导出成功",
|
||||||
|
"get_backup_dir_url_failed": "获取备份目录地址失败",
|
||||||
|
"get_backup_files_failed": "获取备份文件失败"
|
||||||
}
|
}
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "__MSG_scriptcat__",
|
"name": "__MSG_scriptcat__",
|
||||||
"version": "0.17.0.1002",
|
"version": "0.17.0.1003",
|
||||||
"author": "CodFrm",
|
"author": "CodFrm",
|
||||||
"description": "__MSG_scriptcat_description__",
|
"description": "__MSG_scriptcat_description__",
|
||||||
"options_ui": {
|
"options_ui": {
|
||||||
|
@ -4,7 +4,6 @@ import DBWriter from "./app/logger/db_writer";
|
|||||||
import { LoggerDAO } from "./app/repo/logger";
|
import { LoggerDAO } from "./app/repo/logger";
|
||||||
import { OffscreenManager } from "./app/service/offscreen";
|
import { OffscreenManager } from "./app/service/offscreen";
|
||||||
|
|
||||||
// 初始化数据库
|
|
||||||
migrate();
|
migrate();
|
||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
|
@ -129,7 +129,7 @@ const CloudScriptPlan: React.FC<{
|
|||||||
const url = URL.createObjectURL(files);
|
const url = URL.createObjectURL(files);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
}, 60 * 1000);
|
}, 30 * 1000);
|
||||||
chrome.downloads.download({
|
chrome.downloads.download({
|
||||||
url,
|
url,
|
||||||
saveAs: true,
|
saveAs: true,
|
||||||
|
@ -8,16 +8,13 @@ import "@App/index.css";
|
|||||||
import { Provider } from "react-redux";
|
import { Provider } from "react-redux";
|
||||||
import { store } from "@App/pages/store/store.ts";
|
import { store } from "@App/pages/store/store.ts";
|
||||||
import LoggerCore from "@App/app/logger/core.ts";
|
import LoggerCore from "@App/app/logger/core.ts";
|
||||||
import migrate from "@App/app/migrate.ts";
|
|
||||||
import { LoggerDAO } from "@App/app/repo/logger.ts";
|
import { LoggerDAO } from "@App/app/repo/logger.ts";
|
||||||
import DBWriter from "@App/app/logger/db_writer.ts";
|
import DBWriter from "@App/app/logger/db_writer.ts";
|
||||||
|
|
||||||
// 初始化数据库
|
|
||||||
migrate();
|
|
||||||
// 初始化日志组件
|
// 初始化日志组件
|
||||||
const loggerCore = new LoggerCore({
|
const loggerCore = new LoggerCore({
|
||||||
writer: new DBWriter(new LoggerDAO()),
|
writer: new DBWriter(new LoggerDAO()),
|
||||||
labels: { env: "install" },
|
labels: { env: "confirm" },
|
||||||
});
|
});
|
||||||
|
|
||||||
loggerCore.logger().debug("page start");
|
loggerCore.logger().debug("page start");
|
||||||
|
256
src/pages/import/App.tsx
Normal file
256
src/pages/import/App.tsx
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { Button, Card, Checkbox, Divider, List, Message, Space, Switch, Typography } from "@arco-design/web-react";
|
||||||
|
import { useTranslation } from "react-i18next"; // 导入react-i18next的useTranslation钩子
|
||||||
|
import JSZip from "jszip";
|
||||||
|
import { ScriptBackupData, ScriptOptions, SubscribeBackupData } from "@App/pkg/backup/struct";
|
||||||
|
import { prepareScriptByCode } from "@App/pkg/utils/script";
|
||||||
|
import { Script, SCRIPT_STATUS_DISABLE, SCRIPT_STATUS_ENABLE } from "@App/app/repo/scripts";
|
||||||
|
import { Subscribe } from "@App/app/repo/subscribe";
|
||||||
|
import Cache from "@App/app/cache";
|
||||||
|
import CacheKey from "@App/app/cache_key";
|
||||||
|
import { parseBackupZipFile } from "@App/pkg/backup/utils";
|
||||||
|
import { scriptClient, valueClient } from "../store/features/script";
|
||||||
|
|
||||||
|
type ScriptData = ScriptBackupData & {
|
||||||
|
script?: { script: Script; oldScript?: Script };
|
||||||
|
install: boolean;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SubscribeData = SubscribeBackupData & {
|
||||||
|
subscribe?: Subscribe;
|
||||||
|
install: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [scripts, setScripts] = useState<ScriptData[]>([]);
|
||||||
|
const [subscribes, setSubscribe] = useState<SubscribeData[]>([]);
|
||||||
|
const [selectAll, setSelectAll] = useState([true, true]);
|
||||||
|
const [installNum, setInstallNum] = useState([0, 0]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
const uuid = url.searchParams.get("uuid") || "";
|
||||||
|
const { t } = useTranslation(); // 使用useTranslation钩子获取翻译函数
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
Cache.getInstance()
|
||||||
|
.get(CacheKey.importFile(uuid))
|
||||||
|
.then(async (resp: { filename: string; url: string }) => {
|
||||||
|
const filedata = await fetch(resp.url).then((resp) => resp.blob());
|
||||||
|
const zip = await JSZip.loadAsync(filedata);
|
||||||
|
const backData = await parseBackupZipFile(zip);
|
||||||
|
const backDataScript = backData.script as ScriptData[];
|
||||||
|
setScripts(backDataScript);
|
||||||
|
// 获取各个脚本现在已经存在的信息
|
||||||
|
const result = await Promise.all(
|
||||||
|
backDataScript.map(async (item) => {
|
||||||
|
try {
|
||||||
|
const prepareScript = await prepareScriptByCode(
|
||||||
|
item.code,
|
||||||
|
item.options?.meta.file_url || "",
|
||||||
|
item.options?.meta.sc_uuid || undefined,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
item.script = prepareScript;
|
||||||
|
} catch (e: any) {
|
||||||
|
item.error = e.toString();
|
||||||
|
return Promise.resolve(item);
|
||||||
|
}
|
||||||
|
if (!item.options) {
|
||||||
|
item.options = {
|
||||||
|
options: {} as ScriptOptions,
|
||||||
|
meta: {
|
||||||
|
name: item.script?.script.name,
|
||||||
|
// 此uuid是对tm的兼容处理
|
||||||
|
uuid: item.script?.script.uuid,
|
||||||
|
sc_uuid: item.script?.script.uuid,
|
||||||
|
file_url: item.script?.script.downloadUrl || "",
|
||||||
|
modified: item.script?.script.createtime,
|
||||||
|
subscribe_url: item.script?.script.subscribeUrl,
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
enabled:
|
||||||
|
item.enabled === false
|
||||||
|
? false
|
||||||
|
: !(item.script?.script.metadata.background || item.script?.script.metadata.crontab),
|
||||||
|
position: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
item.script.script.status =
|
||||||
|
item.enabled !== false && item.options.settings.enabled ? SCRIPT_STATUS_ENABLE : SCRIPT_STATUS_DISABLE;
|
||||||
|
item.install = true;
|
||||||
|
return Promise.resolve(item);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
setScripts(result);
|
||||||
|
setSelectAll([true, true]);
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
Message.error(`获取导入文件失败: ${e}`);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Card bordered={false} title={t("data_import")}>
|
||||||
|
<Space direction="vertical" style={{ width: "100%" }}>
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
loading={loading}
|
||||||
|
onClick={async () => {
|
||||||
|
setInstallNum((prev) => {
|
||||||
|
return [0, prev[1]];
|
||||||
|
});
|
||||||
|
setLoading(true);
|
||||||
|
const result = scripts.map(async (item) => {
|
||||||
|
const ok = true;
|
||||||
|
if (item.install && !item.error) {
|
||||||
|
await scriptClient.install(item.script?.script!, item.code);
|
||||||
|
// 导入数据
|
||||||
|
const { data } = item.storage;
|
||||||
|
Object.keys(data).forEach((key) => {
|
||||||
|
valueClient.setScriptValue(item.script?.script.uuid!, key, data[key]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setInstallNum((prev) => {
|
||||||
|
return [prev[0] + 1, prev[1]];
|
||||||
|
});
|
||||||
|
return Promise.resolve(ok);
|
||||||
|
});
|
||||||
|
await Promise.all(result);
|
||||||
|
setLoading(false);
|
||||||
|
Message.success(t("import_success")!);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("import")}
|
||||||
|
</Button>
|
||||||
|
<Button type="primary" status="danger" loading={loading} onClick={() => window.close()}>
|
||||||
|
{t("close")}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
<Typography.Text>
|
||||||
|
{t("select_scripts_to_import")}:{" "}
|
||||||
|
<Checkbox
|
||||||
|
checked={selectAll[0]}
|
||||||
|
onChange={() => {
|
||||||
|
setScripts((prev) => {
|
||||||
|
setSelectAll([!selectAll[0], selectAll[1]]);
|
||||||
|
return prev.map((item) => {
|
||||||
|
item.install = !selectAll[0];
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("select_all")}
|
||||||
|
</Checkbox>
|
||||||
|
<Divider type="vertical" />
|
||||||
|
{t("script_import_progress")}: {installNum[0]}/{scripts.length}
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text>
|
||||||
|
{t("select_subscribes_to_import")}:{" "}
|
||||||
|
<Checkbox
|
||||||
|
checked={selectAll[1]}
|
||||||
|
onChange={() => {
|
||||||
|
setSubscribe((prev) => {
|
||||||
|
setSelectAll([selectAll[0], !selectAll[1]]);
|
||||||
|
return prev.map((item) => {
|
||||||
|
item.install = !selectAll[1];
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("select_all")}
|
||||||
|
</Checkbox>
|
||||||
|
<Divider type="vertical" />
|
||||||
|
{t("subscribe_import_progress")}: {installNum[1]}/{subscribes.length}
|
||||||
|
</Typography.Text>
|
||||||
|
<List
|
||||||
|
className="import-list"
|
||||||
|
loading={loading}
|
||||||
|
bordered={false}
|
||||||
|
dataSource={scripts}
|
||||||
|
render={(item, index) => (
|
||||||
|
<div
|
||||||
|
className="flex flex-row justify-between p-2"
|
||||||
|
key={`e_${index}`}
|
||||||
|
style={{
|
||||||
|
background: item.error ? "rgb(var(--red-1))" : item.install ? "rgb(var(--arcoblue-1))" : "",
|
||||||
|
borderBottom: "1px solid rgb(var(--gray-3))",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
const install = item.install;
|
||||||
|
setScripts((prev) => {
|
||||||
|
prev[index].install = !install;
|
||||||
|
return [...prev];
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Space
|
||||||
|
direction="vertical"
|
||||||
|
size={1}
|
||||||
|
style={{
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography.Title
|
||||||
|
heading={6}
|
||||||
|
style={{
|
||||||
|
color: "rgb(var(--blue-5))",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.script?.script?.name || item.error || t("unknown")}
|
||||||
|
</Typography.Title>
|
||||||
|
<span className="text-sm color-gray-5">
|
||||||
|
{t("author")}: {item.script?.script?.metadata.author && item.script?.script?.metadata.author[0]}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm color-gray-5">
|
||||||
|
{t("description")}:{" "}
|
||||||
|
{item.script?.script?.metadata.description && item.script?.script?.metadata.description[0]}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm color-gray-5">
|
||||||
|
{t("source")}: {item.options?.meta.file_url || t("local_creation")}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm color-gray-5">
|
||||||
|
{t("operation")}:{" "}
|
||||||
|
{(item.install && (item.script?.oldScript ? t("update") : t("add_new"))) ||
|
||||||
|
(item.error
|
||||||
|
? `${t("error")}: ${item.options?.meta.name} - ${item.options?.meta.uuid}`
|
||||||
|
: t("no_operation"))}
|
||||||
|
</span>
|
||||||
|
</Space>
|
||||||
|
<div
|
||||||
|
className="flex flex-col justify-between"
|
||||||
|
style={{
|
||||||
|
minWidth: "80px",
|
||||||
|
textAlign: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="text-sm color-gray-5">{t("enable_script")}</span>
|
||||||
|
<div className="text-center">
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
checked={item.script?.script?.status === SCRIPT_STATUS_ENABLE}
|
||||||
|
onChange={(checked) => {
|
||||||
|
setScripts((prev) => {
|
||||||
|
prev[index].script!.script.status = checked ? SCRIPT_STATUS_ENABLE : SCRIPT_STATUS_DISABLE;
|
||||||
|
return [...prev];
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
3
src/pages/import/index.css
Normal file
3
src/pages/import/index.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.import-list .arco-typography {
|
||||||
|
margin: 0;
|
||||||
|
}
|
30
src/pages/import/main.tsx
Normal file
30
src/pages/import/main.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import App from "./App.tsx";
|
||||||
|
import MainLayout from "../components/layout/MainLayout.tsx";
|
||||||
|
import "@arco-design/web-react/dist/css/arco.css";
|
||||||
|
import "@App/locales/locales";
|
||||||
|
import "@App/index.css";
|
||||||
|
import { Provider } from "react-redux";
|
||||||
|
import { store } from "@App/pages/store/store.ts";
|
||||||
|
import LoggerCore from "@App/app/logger/core.ts";
|
||||||
|
import { LoggerDAO } from "@App/app/repo/logger.ts";
|
||||||
|
import DBWriter from "@App/app/logger/db_writer.ts";
|
||||||
|
|
||||||
|
// 初始化日志组件
|
||||||
|
const loggerCore = new LoggerCore({
|
||||||
|
writer: new DBWriter(new LoggerDAO()),
|
||||||
|
labels: { env: "import" },
|
||||||
|
});
|
||||||
|
|
||||||
|
loggerCore.logger().debug("page start");
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<Provider store={store}>
|
||||||
|
<MainLayout className="!flex-col !p-[10px] box-border h-auto overflow-auto">
|
||||||
|
<App />
|
||||||
|
</MainLayout>
|
||||||
|
</Provider>
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
@ -7,7 +7,7 @@ import { i18nDescription, i18nName } from "@App/locales/locales";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { prepareScriptByCode, prepareSubscribeByCode, ScriptInfo } from "@App/pkg/utils/script";
|
import { prepareScriptByCode, prepareSubscribeByCode, ScriptInfo } from "@App/pkg/utils/script";
|
||||||
import { nextTime } from "@App/pkg/utils/utils";
|
import { nextTime } from "@App/pkg/utils/utils";
|
||||||
import { scriptClient } from "../store/features/script";
|
import { scriptClient, subscribeClient } from "../store/features/script";
|
||||||
|
|
||||||
type Permission = { label: string; color?: string; value: string[] }[];
|
type Permission = { label: string; color?: string; value: string[] }[];
|
||||||
|
|
||||||
@ -253,18 +253,18 @@ function App() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (scriptInfo?.userSubscribe) {
|
if (scriptInfo?.userSubscribe) {
|
||||||
// subscribeCtrl
|
subscribeClient
|
||||||
// .upsert(upsertScript as Subscribe)
|
.install(upsertScript as Subscribe)
|
||||||
// .then(() => {
|
.then(() => {
|
||||||
// Message.success(t("subscribe_success")!);
|
Message.success(t("subscribe_success")!);
|
||||||
// setBtnText(t("subscribe_success")!);
|
setBtnText(t("subscribe_success")!);
|
||||||
// setTimeout(() => {
|
setTimeout(() => {
|
||||||
// closeWindow();
|
closeWindow();
|
||||||
// }, 200);
|
}, 500);
|
||||||
// })
|
})
|
||||||
// .catch((e) => {
|
.catch((e) => {
|
||||||
// Message.error(`${t("subscribe_failed")}: ${e}`);
|
Message.error(`${t("subscribe_failed")}: ${e}`);
|
||||||
// });
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
scriptClient
|
scriptClient
|
||||||
|
@ -8,12 +8,9 @@ import "@App/index.css";
|
|||||||
import { Provider } from "react-redux";
|
import { Provider } from "react-redux";
|
||||||
import { store } from "@App/pages/store/store.ts";
|
import { store } from "@App/pages/store/store.ts";
|
||||||
import LoggerCore from "@App/app/logger/core.ts";
|
import LoggerCore from "@App/app/logger/core.ts";
|
||||||
import migrate from "@App/app/migrate.ts";
|
|
||||||
import { LoggerDAO } from "@App/app/repo/logger.ts";
|
import { LoggerDAO } from "@App/app/repo/logger.ts";
|
||||||
import DBWriter from "@App/app/logger/db_writer.ts";
|
import DBWriter from "@App/app/logger/db_writer.ts";
|
||||||
|
|
||||||
// 初始化数据库
|
|
||||||
migrate();
|
|
||||||
// 初始化日志组件
|
// 初始化日志组件
|
||||||
const loggerCore = new LoggerCore({
|
const loggerCore = new LoggerCore({
|
||||||
writer: new DBWriter(new LoggerDAO()),
|
writer: new DBWriter(new LoggerDAO()),
|
||||||
|
@ -8,15 +8,12 @@ import "@arco-design/web-react/dist/css/arco.css";
|
|||||||
import "@App/locales/locales";
|
import "@App/locales/locales";
|
||||||
import "@App/index.css";
|
import "@App/index.css";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
import migrate from "@App/app/migrate.ts";
|
|
||||||
import LoggerCore from "@App/app/logger/core.ts";
|
import LoggerCore from "@App/app/logger/core.ts";
|
||||||
import { LoggerDAO } from "@App/app/repo/logger.ts";
|
import { LoggerDAO } from "@App/app/repo/logger.ts";
|
||||||
import DBWriter from "@App/app/logger/db_writer.ts";
|
import DBWriter from "@App/app/logger/db_writer.ts";
|
||||||
import registerEditor from "@App/pkg/utils/monaco-editor.ts";
|
import registerEditor from "@App/pkg/utils/monaco-editor.ts";
|
||||||
import storeSubscribe from "../store/subscribe.ts";
|
import storeSubscribe from "../store/subscribe.ts";
|
||||||
|
|
||||||
// 初始化数据库
|
|
||||||
migrate();
|
|
||||||
registerEditor();
|
registerEditor();
|
||||||
// 初始化日志组件
|
// 初始化日志组件
|
||||||
const loggerCore = new LoggerCore({
|
const loggerCore = new LoggerCore({
|
||||||
|
@ -83,7 +83,7 @@ import {
|
|||||||
scriptClient,
|
scriptClient,
|
||||||
} from "@App/pages/store/features/script";
|
} from "@App/pages/store/features/script";
|
||||||
import { message, systemConfig } from "@App/pages/store/global";
|
import { message, systemConfig } from "@App/pages/store/global";
|
||||||
import { ValueClient } from "@App/app/service/service_worker/client";
|
import { SynchronizeClient, ValueClient } from "@App/app/service/service_worker/client";
|
||||||
|
|
||||||
type ListType = Script & { loading?: boolean };
|
type ListType = Script & { loading?: boolean };
|
||||||
|
|
||||||
@ -719,7 +719,17 @@ function ScriptList() {
|
|||||||
select.forEach((item) => {
|
select.forEach((item) => {
|
||||||
uuids.push(item.uuid);
|
uuids.push(item.uuid);
|
||||||
});
|
});
|
||||||
synchronizeCtrl.backup(uuids);
|
Message.loading({
|
||||||
|
id: "export",
|
||||||
|
content: t("exporting"),
|
||||||
|
});
|
||||||
|
new SynchronizeClient(message).export(uuids).then(() => {
|
||||||
|
Message.success({
|
||||||
|
id: "export",
|
||||||
|
content: t("export_success"),
|
||||||
|
duration: 3000,
|
||||||
|
});
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "delete":
|
case "delete":
|
||||||
if (confirm(t("list.confirm_delete")!)) {
|
if (confirm(t("list.confirm_delete")!)) {
|
||||||
|
@ -170,14 +170,15 @@ function Setting() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const params = { ...systemConfig.backup.params };
|
const cloudSync = await systemConfig.getCloudSync();
|
||||||
|
const params = { ...cloudSync.params };
|
||||||
params[fileSystemType] = fileSystemParams;
|
params[fileSystemType] = fileSystemParams;
|
||||||
systemConfig.cloudSync = {
|
systemConfig.setCloudSync({
|
||||||
enable: enableCloudSync,
|
enable: enableCloudSync || false,
|
||||||
syncDelete,
|
syncDelete: syncDelete || false,
|
||||||
filesystem: fileSystemType,
|
filesystem: fileSystemType,
|
||||||
params,
|
params,
|
||||||
};
|
});
|
||||||
Message.success(t("save_success")!);
|
Message.success(t("save_success")!);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -1,47 +1,27 @@
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import Text from "@arco-design/web-react/es/Typography/text";
|
import Text from "@arco-design/web-react/es/Typography/text";
|
||||||
import {
|
import { Button, Card, Input, Message, Popconfirm, Switch, Table, Tag, Tooltip } from "@arco-design/web-react";
|
||||||
Button,
|
import { Subscribe, SUBSCRIBE_STATUS_DISABLE, SUBSCRIBE_STATUS_ENABLE, SubscribeDAO } from "@App/app/repo/subscribe";
|
||||||
Card,
|
|
||||||
Input,
|
|
||||||
Message,
|
|
||||||
Popconfirm,
|
|
||||||
Switch,
|
|
||||||
Table,
|
|
||||||
Tag,
|
|
||||||
Tooltip,
|
|
||||||
} from "@arco-design/web-react";
|
|
||||||
import {
|
|
||||||
Subscribe,
|
|
||||||
SUBSCRIBE_STATUS_DISABLE,
|
|
||||||
SUBSCRIBE_STATUS_ENABLE,
|
|
||||||
SubscribeDAO,
|
|
||||||
} from "@App/app/repo/subscribe";
|
|
||||||
import { ColumnProps } from "@arco-design/web-react/es/Table";
|
import { ColumnProps } from "@arco-design/web-react/es/Table";
|
||||||
import { IconSearch, IconUserAdd } from "@arco-design/web-react/icon";
|
import { IconSearch, IconUserAdd } from "@arco-design/web-react/icon";
|
||||||
import { RefInputType } from "@arco-design/web-react/es/Input/interface";
|
import { RefInputType } from "@arco-design/web-react/es/Input/interface";
|
||||||
import { semTime } from "@App/pkg/utils/utils";
|
import { semTime } from "@App/pkg/utils/utils";
|
||||||
import { RiDeleteBin5Fill } from "react-icons/ri";
|
import { RiDeleteBin5Fill } from "react-icons/ri";
|
||||||
import { useTranslation } from "react-i18next"; // 添加了 react-i18next 的引用
|
import { useTranslation } from "react-i18next"; // 添加了 react-i18next 的引用
|
||||||
|
import { subscribeClient } from "@App/pages/store/features/script";
|
||||||
|
|
||||||
type ListType = Subscribe & { loading?: boolean };
|
type ListType = Subscribe & { loading?: boolean };
|
||||||
|
|
||||||
function SubscribeList() {
|
function SubscribeList() {
|
||||||
const dao = new SubscribeDAO();
|
const dao = new SubscribeDAO();
|
||||||
const subscribeCtrl = IoC.instance(
|
|
||||||
SubscribeController
|
|
||||||
) as SubscribeController;
|
|
||||||
const [list, setList] = useState<ListType[]>([]);
|
const [list, setList] = useState<ListType[]>([]);
|
||||||
const inputRef = useRef<RefInputType>(null);
|
const inputRef = useRef<RefInputType>(null);
|
||||||
const { t } = useTranslation(); // 使用 useTranslation hook
|
const { t } = useTranslation(); // 使用 useTranslation hook
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dao.table
|
dao.all().then((subscribes) => {
|
||||||
.orderBy("id")
|
setList(subscribes);
|
||||||
.toArray()
|
});
|
||||||
.then((subscribes) => {
|
|
||||||
setList(subscribes);
|
|
||||||
});
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const columns: ColumnProps[] = [
|
const columns: ColumnProps[] = [
|
||||||
@ -50,7 +30,13 @@ function SubscribeList() {
|
|||||||
dataIndex: "id",
|
dataIndex: "id",
|
||||||
width: 70,
|
width: 70,
|
||||||
key: "#",
|
key: "#",
|
||||||
sorter: (a, b) => a.id - b.id,
|
sorter: (a: Subscribe, b) => a.createtime - b.createtime,
|
||||||
|
render(col) {
|
||||||
|
if (col < 0) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
return col + 1;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("enable"),
|
title: t("enable"),
|
||||||
@ -79,22 +65,18 @@ function SubscribeList() {
|
|||||||
onChange={(checked) => {
|
onChange={(checked) => {
|
||||||
list[index].loading = true;
|
list[index].loading = true;
|
||||||
setList([...list]);
|
setList([...list]);
|
||||||
let p: Promise<any>;
|
subscribeClient
|
||||||
if (checked) {
|
.enable(item.url, checked)
|
||||||
p = subscribeCtrl.enable(item.id).then(() => {
|
.then(() => {
|
||||||
list[index].status = SUBSCRIBE_STATUS_ENABLE;
|
list[index].status = checked ? SUBSCRIBE_STATUS_ENABLE : SUBSCRIBE_STATUS_DISABLE;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
Message.error(err);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
list[index].loading = false;
|
||||||
|
setList([...list]);
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
p = subscribeCtrl.disable(item.id).then(() => {
|
|
||||||
list[index].status = SUBSCRIBE_STATUS_DISABLE;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
p.catch((err) => {
|
|
||||||
Message.error(err);
|
|
||||||
}).finally(() => {
|
|
||||||
list[index].loading = false;
|
|
||||||
setList([...list]);
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -169,14 +151,7 @@ function SubscribeList() {
|
|||||||
return <div />;
|
return <div />;
|
||||||
}
|
}
|
||||||
return (item.metadata.connect as string[]).map((val) => {
|
return (item.metadata.connect as string[]).map((val) => {
|
||||||
return (
|
return <img src={`https://${val}/favicon.ico`} alt={val} height={16} width={16} />;
|
||||||
<img
|
|
||||||
src={`https://${val}/favicon.ico`}
|
|
||||||
alt={val}
|
|
||||||
height={16}
|
|
||||||
width={16}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -227,8 +202,8 @@ function SubscribeList() {
|
|||||||
id: "checkupdate",
|
id: "checkupdate",
|
||||||
content: t("checking_for_updates"),
|
content: t("checking_for_updates"),
|
||||||
});
|
});
|
||||||
subscribeCtrl
|
subscribeClient
|
||||||
.checkUpdate(subscribe.id)
|
.checkUpdate(subscribe.url)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res) {
|
if (res) {
|
||||||
Message.warning({
|
Message.warning({
|
||||||
@ -267,10 +242,15 @@ function SubscribeList() {
|
|||||||
title={t("confirm_delete_subscription")}
|
title={t("confirm_delete_subscription")}
|
||||||
icon={<RiDeleteBin5Fill />}
|
icon={<RiDeleteBin5Fill />}
|
||||||
onOk={() => {
|
onOk={() => {
|
||||||
setList(list.filter((val) => val.id !== item.id));
|
subscribeClient
|
||||||
subscribeCtrl.delete(item.id).catch((e) => {
|
.delete(item.url)
|
||||||
Message.error(`${t("delete_failed")}: ${e}`);
|
.then(() => {
|
||||||
});
|
setList(list.filter((val) => val.url !== item.url));
|
||||||
|
Message.success(t("delete_success"));
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
Message.error(`${t("delete_failed")}: ${e}`);
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { Button, Card, Checkbox, Drawer, Empty, Input, List, Message, Modal, Space } from "@arco-design/web-react";
|
import { Button, Card, Checkbox, Drawer, Empty, Input, List, Message, Modal, Space } from "@arco-design/web-react";
|
||||||
import Title from "@arco-design/web-react/es/Typography/title";
|
import Title from "@arco-design/web-react/es/Typography/title";
|
||||||
import { formatUnixTime } from "@App/pkg/utils/utils";
|
import { formatUnixTime } from "@App/pkg/utils/utils";
|
||||||
@ -6,8 +6,13 @@ import FileSystemParams from "@App/pages/components/FileSystemParams";
|
|||||||
import { IconQuestionCircleFill } from "@arco-design/web-react/icon";
|
import { IconQuestionCircleFill } from "@arco-design/web-react/icon";
|
||||||
import { RefInputType } from "@arco-design/web-react/es/Input/interface";
|
import { RefInputType } from "@arco-design/web-react/es/Input/interface";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { FileSystemType } from "@Packages/filesystem/factory";
|
import FileSystemFactory, { FileSystemType } from "@Packages/filesystem/factory";
|
||||||
import { systemConfig } from "@App/pages/store/global";
|
import { File, FileReader } from "@Packages/filesystem/filesystem";
|
||||||
|
import { message, systemConfig } from "@App/pages/store/global";
|
||||||
|
import { SynchronizeClient } from "@App/app/service/service_worker/client";
|
||||||
|
import { set } from "node_modules/yaml/dist/schema/yaml-1.1/set";
|
||||||
|
|
||||||
|
const synchronizeClient = new SynchronizeClient(message);
|
||||||
|
|
||||||
function Tools() {
|
function Tools() {
|
||||||
const [loading, setLoading] = useState<{ [key: string]: boolean }>({});
|
const [loading, setLoading] = useState<{ [key: string]: boolean }>({});
|
||||||
@ -16,16 +21,26 @@ function Tools() {
|
|||||||
const [fileSystemParams, setFilesystemParam] = useState<{
|
const [fileSystemParams, setFilesystemParam] = useState<{
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}>({});
|
}>({});
|
||||||
|
const [vscodeUrl, setVscodeUrl] = useState<string>("");
|
||||||
|
const [vscodeReconnect, setVscodeReconnect] = useState<boolean>(false);
|
||||||
const [backupFileList, setBackupFileList] = useState<File[]>([]);
|
const [backupFileList, setBackupFileList] = useState<File[]>([]);
|
||||||
const vscodeRef = useRef<RefInputType>(null);
|
const vscodeRef = useRef<RefInputType>(null);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 获取配置
|
// 获取配置
|
||||||
systemConfig.getBackup().then((backup) => {
|
const loadConfig = async () => {
|
||||||
|
const [backup, vscodeUrl] = await Promise.all([
|
||||||
|
systemConfig.getBackup(),
|
||||||
|
systemConfig.getVscodeUrl(),
|
||||||
|
systemConfig.getVscodeReconnect(),
|
||||||
|
]);
|
||||||
setFilesystemType(backup.filesystem);
|
setFilesystemType(backup.filesystem);
|
||||||
setFilesystemParam(backup.params[backup.filesystem] || {});
|
setFilesystemParam(backup.params[backup.filesystem] || {});
|
||||||
});
|
setVscodeUrl(vscodeUrl);
|
||||||
|
setVscodeReconnect(systemConfig.vscodeReconnect);
|
||||||
|
};
|
||||||
|
loadConfig();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -49,7 +64,7 @@ function Tools() {
|
|||||||
loading={loading.local}
|
loading={loading.local}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setLoading((prev) => ({ ...prev, local: true }));
|
setLoading((prev) => ({ ...prev, local: true }));
|
||||||
await syncCtrl.backup();
|
await synchronizeClient.export();
|
||||||
setLoading((prev) => ({ ...prev, local: false }));
|
setLoading((prev) => ({ ...prev, local: false }));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -58,14 +73,24 @@ function Tools() {
|
|||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
syncCtrl
|
const el = fileRef.current!;
|
||||||
.openImportFile(fileRef.current!)
|
el.onchange = async () => {
|
||||||
.then(() => {
|
const { files } = el;
|
||||||
|
if (!files) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const file = files[0];
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await synchronizeClient.openImportWindow(file.name, file);
|
||||||
Message.success(t("select_import_script")!);
|
Message.success(t("select_import_script")!);
|
||||||
})
|
} catch (e) {
|
||||||
.then((e) => {
|
Message.error(`${t("import_error")}: ${e}`);
|
||||||
Message.error(`${t("import_error")}${e}`);
|
}
|
||||||
});
|
};
|
||||||
|
el.click();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("import_file")}
|
{t("import_file")}
|
||||||
@ -95,7 +120,7 @@ function Tools() {
|
|||||||
});
|
});
|
||||||
setLoading((prev) => ({ ...prev, cloud: true }));
|
setLoading((prev) => ({ ...prev, cloud: true }));
|
||||||
Message.info(t("preparing_backup")!);
|
Message.info(t("preparing_backup")!);
|
||||||
syncCtrl
|
synchronizeClient
|
||||||
.backupToCloud(fileSystemType, fileSystemParams)
|
.backupToCloud(fileSystemType, fileSystemParams)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
Message.success(t("backup_success")!);
|
Message.success(t("backup_success")!);
|
||||||
@ -113,9 +138,11 @@ function Tools() {
|
|||||||
<Button
|
<Button
|
||||||
key="list"
|
key="list"
|
||||||
type="primary"
|
type="primary"
|
||||||
|
loading={loading.cloud}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
let fs = await FileSystemFactory.create(fileSystemType, fileSystemParams);
|
setLoading((prev) => ({ ...prev, cloud: true }));
|
||||||
try {
|
try {
|
||||||
|
let fs = await FileSystemFactory.create(fileSystemType, fileSystemParams);
|
||||||
fs = await fs.openDir("ScriptCat");
|
fs = await fs.openDir("ScriptCat");
|
||||||
let list = await fs.list();
|
let list = await fs.list();
|
||||||
list.sort((a, b) => b.updatetime - a.updatetime);
|
list.sort((a, b) => b.updatetime - a.updatetime);
|
||||||
@ -129,6 +156,7 @@ function Tools() {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
Message.error(`${t("get_backup_files_failed")}: ${e}`);
|
Message.error(`${t("get_backup_files_failed")}: ${e}`);
|
||||||
}
|
}
|
||||||
|
setLoading((prev) => ({ ...prev, cloud: false }));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("backup_list")}
|
{t("backup_list")}
|
||||||
@ -193,12 +221,8 @@ function Tools() {
|
|||||||
Message.error(`${t("pull_failed")}: ${e}`);
|
Message.error(`${t("pull_failed")}: ${e}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const url = URL.createObjectURL(data);
|
synchronizeClient
|
||||||
setTimeout(() => {
|
.openImportWindow(item.name, data)
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
}, 60 * 100000);
|
|
||||||
syncCtrl
|
|
||||||
.openImportWindow(item.name, url)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
Message.success(t("select_import_script")!);
|
Message.success(t("select_import_script")!);
|
||||||
})
|
})
|
||||||
@ -271,22 +295,24 @@ function Tools() {
|
|||||||
<Title heading={6}>{t("vscode_url")}</Title>
|
<Title heading={6}>{t("vscode_url")}</Title>
|
||||||
<Input
|
<Input
|
||||||
ref={vscodeRef}
|
ref={vscodeRef}
|
||||||
defaultValue={systemConfig.vscodeUrl}
|
value={vscodeUrl}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
systemConfig.vscodeUrl = value;
|
setVscodeUrl(value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
checked={vscodeReconnect}
|
||||||
onChange={(checked) => {
|
onChange={(checked) => {
|
||||||
systemConfig.vscodeReconnect = checked;
|
setVscodeReconnect(checked);
|
||||||
}}
|
}}
|
||||||
defaultChecked={systemConfig.vscodeReconnect}
|
|
||||||
>
|
>
|
||||||
{t("auto_connect_vscode_service")}
|
{t("auto_connect_vscode_service")}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
systemConfig.setVscodeUrl(vscodeUrl);
|
||||||
|
systemConfig.setVscodeReconnect(vscodeReconnect);
|
||||||
const ctrl = IoC.instance(SystemController) as SystemController;
|
const ctrl = IoC.instance(SystemController) as SystemController;
|
||||||
ctrl
|
ctrl
|
||||||
.connectVSCode()
|
.connectVSCode()
|
||||||
|
@ -2,7 +2,6 @@ import React from "react";
|
|||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import App from "./App.tsx";
|
import App from "./App.tsx";
|
||||||
import LoggerCore from "@App/app/logger/core.ts";
|
import LoggerCore from "@App/app/logger/core.ts";
|
||||||
import migrate from "@App/app/migrate.ts";
|
|
||||||
import { LoggerDAO } from "@App/app/repo/logger.ts";
|
import { LoggerDAO } from "@App/app/repo/logger.ts";
|
||||||
import DBWriter from "@App/app/logger/db_writer.ts";
|
import DBWriter from "@App/app/logger/db_writer.ts";
|
||||||
import "@arco-design/web-react/dist/css/arco.css";
|
import "@arco-design/web-react/dist/css/arco.css";
|
||||||
@ -12,8 +11,6 @@ import "./index.css";
|
|||||||
import { Provider } from "react-redux";
|
import { Provider } from "react-redux";
|
||||||
import { store } from "../store/store.ts";
|
import { store } from "../store/store.ts";
|
||||||
|
|
||||||
// 初始化数据库
|
|
||||||
migrate();
|
|
||||||
// 初始化日志组件
|
// 初始化日志组件
|
||||||
const loggerCore = new LoggerCore({
|
const loggerCore = new LoggerCore({
|
||||||
writer: new DBWriter(new LoggerDAO()),
|
writer: new DBWriter(new LoggerDAO()),
|
||||||
|
@ -8,13 +8,22 @@ import {
|
|||||||
ScriptDAO,
|
ScriptDAO,
|
||||||
} from "@App/app/repo/scripts";
|
} from "@App/app/repo/scripts";
|
||||||
import { arrayMove } from "@dnd-kit/sortable";
|
import { arrayMove } from "@dnd-kit/sortable";
|
||||||
import { PermissionClient, PopupClient, RuntimeClient, ScriptClient } from "@App/app/service/service_worker/client";
|
import {
|
||||||
|
PermissionClient,
|
||||||
|
PopupClient,
|
||||||
|
RuntimeClient,
|
||||||
|
ScriptClient,
|
||||||
|
SubscribeClient,
|
||||||
|
ValueClient,
|
||||||
|
} from "@App/app/service/service_worker/client";
|
||||||
import { message } from "../global";
|
import { message } from "../global";
|
||||||
|
|
||||||
export const scriptClient = new ScriptClient(message);
|
export const scriptClient = new ScriptClient(message);
|
||||||
|
export const subscribeClient = new SubscribeClient(message);
|
||||||
export const runtimeClient = new RuntimeClient(message);
|
export const runtimeClient = new RuntimeClient(message);
|
||||||
export const popupClient = new PopupClient(message);
|
export const popupClient = new PopupClient(message);
|
||||||
export const permissionClient = new PermissionClient(message);
|
export const permissionClient = new PermissionClient(message);
|
||||||
|
export const valueClient = new ValueClient(message);
|
||||||
|
|
||||||
export const fetchAndSortScriptList = createAsyncThunk("script/fetchScriptList", async () => {
|
export const fetchAndSortScriptList = createAsyncThunk("script/fetchScriptList", async () => {
|
||||||
// 排序
|
// 排序
|
||||||
|
114
src/pkg/backup/backup.test.ts
Normal file
114
src/pkg/backup/backup.test.ts
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import JSZip from "jszip";
|
||||||
|
import BackupExport from "./export";
|
||||||
|
import BackupImport from "./import";
|
||||||
|
import { BackupData } from "./struct";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { initTestEnv } from "@Tests/utils";
|
||||||
|
import ZipFileSystem from "@Packages/filesystem/zip/zip";
|
||||||
|
|
||||||
|
initTestEnv();
|
||||||
|
|
||||||
|
describe("backup", () => {
|
||||||
|
const zipFile = new JSZip();
|
||||||
|
const fs = new ZipFileSystem(zipFile);
|
||||||
|
it("empty", async () => {
|
||||||
|
await new BackupExport(fs).export({
|
||||||
|
script: [],
|
||||||
|
subscribe: [],
|
||||||
|
});
|
||||||
|
const resp = await new BackupImport(fs).parse();
|
||||||
|
expect(resp).toEqual({
|
||||||
|
script: [],
|
||||||
|
subscribe: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("export and import script", async () => {
|
||||||
|
const data: BackupData = {
|
||||||
|
script: [
|
||||||
|
{
|
||||||
|
code: `// ==UserScript==
|
||||||
|
// @name New Userscript
|
||||||
|
// @namespace https://bbs.tampermonkey.net.cn/
|
||||||
|
// @version 0.1.0
|
||||||
|
// @description try to take over the world!
|
||||||
|
// @author You
|
||||||
|
// @match {{match}}
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
console.log('hello world')`,
|
||||||
|
options: {
|
||||||
|
options: {},
|
||||||
|
meta: {
|
||||||
|
name: "test",
|
||||||
|
modified: 1,
|
||||||
|
file_url: "",
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
enabled: true,
|
||||||
|
position: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
resources: [
|
||||||
|
{
|
||||||
|
meta: { name: "test1", mimetype: "text/plain" },
|
||||||
|
base64: "data:text/plain;base64,aGVsbG8gd29ybGQ=",
|
||||||
|
source: "hello world",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
requires: [
|
||||||
|
{
|
||||||
|
meta: { name: "test2", mimetype: "text/plain" },
|
||||||
|
base64: "data:text/plain;base64,aGVsbG8gd29ybGQ=",
|
||||||
|
source: "hello world",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
requiresCss: [
|
||||||
|
{
|
||||||
|
meta: { name: "test3", mimetype: "application/javascript" },
|
||||||
|
base64: "data:application/javascript;base64,aGVsbG8gd29ybGQ=",
|
||||||
|
source: "hello world",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
storage: {
|
||||||
|
ts: 1,
|
||||||
|
data: {
|
||||||
|
num: 1,
|
||||||
|
str: "data",
|
||||||
|
bool: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
subscribe: [
|
||||||
|
{
|
||||||
|
source: `// ==UserSubscribe==
|
||||||
|
// @name New Usersubscribe
|
||||||
|
// @namespace https://bbs.tampermonkey.net.cn/
|
||||||
|
// @version 0.1.0
|
||||||
|
// @description try to take over the world!
|
||||||
|
// @author You
|
||||||
|
// ==/UserSubscribe==
|
||||||
|
|
||||||
|
console.log('hello world')`,
|
||||||
|
options: {
|
||||||
|
meta: {
|
||||||
|
name: "test",
|
||||||
|
modified: 1,
|
||||||
|
url: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as unknown as BackupData;
|
||||||
|
await new BackupExport(fs).export(data);
|
||||||
|
expect(data.script[0].storage.data.num).toEqual("n1");
|
||||||
|
expect(data.script[0].storage.data.str).toEqual("sdata");
|
||||||
|
expect(data.script[0].storage.data.bool).toEqual("bfalse");
|
||||||
|
const resp = await new BackupImport(fs).parse();
|
||||||
|
data.script[0].storage.data.num = 1;
|
||||||
|
data.script[0].storage.data.str = "data";
|
||||||
|
data.script[0].storage.data.bool = false;
|
||||||
|
expect(resp).toEqual(data);
|
||||||
|
});
|
||||||
|
});
|
94
src/pkg/backup/export.ts
Normal file
94
src/pkg/backup/export.ts
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import FileSystem from "@Pkg/filesystem/filesystem";
|
||||||
|
import crypto from "crypto-js";
|
||||||
|
import { base64ToBlob } from "../utils/script";
|
||||||
|
import { toStorageValueStr } from "../utils/utils";
|
||||||
|
import {
|
||||||
|
BackupData,
|
||||||
|
ResourceBackup,
|
||||||
|
ScriptBackupData,
|
||||||
|
SubscribeBackupData,
|
||||||
|
} from "./struct";
|
||||||
|
|
||||||
|
export default class BackupExport {
|
||||||
|
fs: FileSystem;
|
||||||
|
|
||||||
|
constructor(fileSystem: FileSystem) {
|
||||||
|
this.fs = fileSystem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出备份数据
|
||||||
|
export(data: BackupData): Promise<void> {
|
||||||
|
// 写入脚本备份
|
||||||
|
const results: Promise<void>[] = [];
|
||||||
|
data.script.forEach((item) => {
|
||||||
|
results.push(this.writeScript(item));
|
||||||
|
});
|
||||||
|
data.subscribe.forEach((item) => {
|
||||||
|
results.push(this.writeSubscribe(item));
|
||||||
|
});
|
||||||
|
return Promise.all(results).then(() => undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
async writeScript(script: ScriptBackupData) {
|
||||||
|
const { name } = script.options!.meta;
|
||||||
|
// 写脚本文件
|
||||||
|
await (await this.fs.create(`${name}.user.js`)).write(script.code);
|
||||||
|
// 写入脚本options.json
|
||||||
|
await (
|
||||||
|
await this.fs.create(`${name}.options.json`)
|
||||||
|
).write(JSON.stringify(script.options));
|
||||||
|
// 写入脚本storage.json
|
||||||
|
// 不想兼容tm的导出规则了,直接写入storage.json
|
||||||
|
const storage = { ...script.storage };
|
||||||
|
Object.keys(storage.data).forEach((key: string) => {
|
||||||
|
storage.data[key] = toStorageValueStr(storage.data[key]);
|
||||||
|
});
|
||||||
|
await (
|
||||||
|
await this.fs.create(`${name}.storage.json`)
|
||||||
|
).write(JSON.stringify(storage));
|
||||||
|
// 写入脚本资源文件
|
||||||
|
await this.writeResource(name, script.resources, "resources");
|
||||||
|
await this.writeResource(name, script.requires, "requires");
|
||||||
|
await this.writeResource(name, script.requiresCss, "requires.css");
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
async writeResource(
|
||||||
|
name: string,
|
||||||
|
resources: ResourceBackup[],
|
||||||
|
type: "resources" | "requires" | "requires.css"
|
||||||
|
): Promise<void[]> {
|
||||||
|
const results: Promise<void>[] = resources.map(async (item) => {
|
||||||
|
// md5是tm的导出规则
|
||||||
|
const md5 = crypto.MD5(`${type}{val.meta.url}`).toString();
|
||||||
|
if (item.source) {
|
||||||
|
await (
|
||||||
|
await this.fs.create(`${name}.user.js-${md5}-${item.meta.name}`)
|
||||||
|
).write(item.source!);
|
||||||
|
} else {
|
||||||
|
await (
|
||||||
|
await this.fs.create(`${name}.user.js-${md5}-${item.meta.name}`)
|
||||||
|
).write(base64ToBlob(item.base64));
|
||||||
|
}
|
||||||
|
(
|
||||||
|
await this.fs.create(
|
||||||
|
`${name}.user.js-${md5}-${item.meta.name}.${type}.json`
|
||||||
|
)
|
||||||
|
).write(JSON.stringify(item.meta));
|
||||||
|
});
|
||||||
|
return Promise.all(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
async writeSubscribe(subscribe: SubscribeBackupData) {
|
||||||
|
const { name } = subscribe.options!.meta;
|
||||||
|
// 写入订阅文件
|
||||||
|
await (await this.fs.create(`${name}.user.sub.js`)).write(subscribe.source);
|
||||||
|
// 写入订阅options.json
|
||||||
|
await (
|
||||||
|
await this.fs.create(`${name}.user.sub.options.json`)
|
||||||
|
).write(JSON.stringify(subscribe.options));
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
}
|
238
src/pkg/backup/import.ts
Normal file
238
src/pkg/backup/import.ts
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
import LoggerCore from "@App/app/logger/core";
|
||||||
|
import Logger from "@App/app/logger/logger";
|
||||||
|
import FileSystem, { File } from "@Pkg/filesystem/filesystem";
|
||||||
|
import { isText } from "../utils/istextorbinary";
|
||||||
|
import { blobToBase64 } from "../utils/script";
|
||||||
|
import { parseStorageValue } from "../utils/utils";
|
||||||
|
import {
|
||||||
|
BackupData,
|
||||||
|
ResourceBackup,
|
||||||
|
ResourceMeta,
|
||||||
|
ScriptBackupData,
|
||||||
|
ScriptOptionsFile,
|
||||||
|
SubscribeBackupData,
|
||||||
|
SubscribeOptionsFile,
|
||||||
|
ValueStorage,
|
||||||
|
} from "./struct";
|
||||||
|
import JSZip from "jszip";
|
||||||
|
import ZipFileSystem from "@Packages/filesystem/zip/zip";
|
||||||
|
|
||||||
|
type ViolentmonkeyFile = {
|
||||||
|
scripts: {
|
||||||
|
[key: string]: {
|
||||||
|
config: {
|
||||||
|
enabled: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 备份导入工具
|
||||||
|
|
||||||
|
export default class BackupImport {
|
||||||
|
fs: FileSystem;
|
||||||
|
|
||||||
|
logger: Logger;
|
||||||
|
|
||||||
|
constructor(fileSystem: FileSystem) {
|
||||||
|
this.fs = fileSystem;
|
||||||
|
this.logger = LoggerCore.logger({ component: "backupImport" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析出备份数据
|
||||||
|
async parse(): Promise<BackupData> {
|
||||||
|
const map = new Map<string, ScriptBackupData>();
|
||||||
|
const subscribe = new Map<string, SubscribeBackupData>();
|
||||||
|
let files = await this.fs.list();
|
||||||
|
|
||||||
|
// 处理订阅
|
||||||
|
files = await this.dealFile(files, async (file) => {
|
||||||
|
const { name } = file;
|
||||||
|
if (!name.endsWith(".user.sub.js")) {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
const key = name.substring(0, name.length - 12);
|
||||||
|
const subData = {
|
||||||
|
source: await (await this.fs.open(file)).read(),
|
||||||
|
} as SubscribeBackupData;
|
||||||
|
subscribe.set(key, subData);
|
||||||
|
return Promise.resolve(true);
|
||||||
|
});
|
||||||
|
// 处理订阅options
|
||||||
|
files = await this.dealFile(files, async (file) => {
|
||||||
|
const { name } = file;
|
||||||
|
if (!name.endsWith(".user.sub.options.json")) {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
const key = name.substring(0, name.length - 22);
|
||||||
|
const data = <SubscribeOptionsFile>JSON.parse(await (await this.fs.open(file)).read());
|
||||||
|
subscribe.get(key)!.options = data;
|
||||||
|
return Promise.resolve(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 先处理*.user.js文件
|
||||||
|
files = await this.dealFile(files, async (file) => {
|
||||||
|
const { name } = file;
|
||||||
|
if (!name.endsWith(".user.js")) {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
// 遍历与脚本同名的文件
|
||||||
|
const key = name.substring(0, name.length - 8);
|
||||||
|
const backupData = {
|
||||||
|
code: await (await this.fs.open(file)).read(),
|
||||||
|
storage: { data: {}, ts: 0 },
|
||||||
|
requires: [],
|
||||||
|
requiresCss: [],
|
||||||
|
resources: [],
|
||||||
|
} as ScriptBackupData;
|
||||||
|
map.set(key, backupData);
|
||||||
|
return Promise.resolve(true);
|
||||||
|
});
|
||||||
|
// 处理options.json文件
|
||||||
|
files = await this.dealFile(files, async (file) => {
|
||||||
|
const { name } = file;
|
||||||
|
if (!name.endsWith(".options.json")) {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
const key = name.substring(0, name.length - 13);
|
||||||
|
const data = <ScriptOptionsFile>JSON.parse(await (await this.fs.open(file)).read());
|
||||||
|
map.get(key)!.options = data;
|
||||||
|
return Promise.resolve(true);
|
||||||
|
});
|
||||||
|
// 处理storage.json文件
|
||||||
|
files = await this.dealFile(files, async (file) => {
|
||||||
|
const { name } = file;
|
||||||
|
if (!name.endsWith(".storage.json")) {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
const key = name.substring(0, name.length - 13);
|
||||||
|
const data = <ValueStorage>JSON.parse(await (await this.fs.open(file)).read());
|
||||||
|
Object.keys(data.data).forEach((dataKey) => {
|
||||||
|
data.data[dataKey] = parseStorageValue(data.data[dataKey]);
|
||||||
|
});
|
||||||
|
map.get(key)!.storage = data;
|
||||||
|
return Promise.resolve(true);
|
||||||
|
});
|
||||||
|
// 处理各种资源文件
|
||||||
|
// 将期望的资源文件名储存到map中, 以便后续处理
|
||||||
|
const resourceFilenameMap = new Map<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
index: number;
|
||||||
|
key: string;
|
||||||
|
type: "resources" | "requires" | "requiresCss";
|
||||||
|
}
|
||||||
|
>();
|
||||||
|
files = await this.dealFile(files, async (file) => {
|
||||||
|
const { name } = file;
|
||||||
|
const userJsIndex = name.indexOf(".user.js-");
|
||||||
|
if (userJsIndex === -1) {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
const key = name.substring(0, userJsIndex);
|
||||||
|
let type: "resources" | "requires" | "requiresCss" | "" = "";
|
||||||
|
if (!name.endsWith(".resources.json")) {
|
||||||
|
if (!name.endsWith(".requires.json")) {
|
||||||
|
if (!name.endsWith(".requires.css.json")) {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
type = "requiresCss";
|
||||||
|
resourceFilenameMap.set(name.substring(0, name.length - 18), {
|
||||||
|
index: map.get(key)!.requiresCss.length,
|
||||||
|
key,
|
||||||
|
type,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
type = "requires";
|
||||||
|
resourceFilenameMap.set(name.substring(0, name.length - 14), {
|
||||||
|
index: map.get(key)!.requires.length,
|
||||||
|
key,
|
||||||
|
type,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
type = "resources";
|
||||||
|
resourceFilenameMap.set(name.substring(0, name.length - 15), {
|
||||||
|
index: map.get(key)!.resources.length,
|
||||||
|
key,
|
||||||
|
type,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const data = <ResourceMeta>JSON.parse(await (await this.fs.open(file)).read());
|
||||||
|
map.get(key)![type].push({
|
||||||
|
meta: data,
|
||||||
|
} as never as ResourceBackup);
|
||||||
|
return Promise.resolve(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理资源文件的内容
|
||||||
|
let violentmonkeyFile: File | undefined;
|
||||||
|
files = await this.dealFile(files, async (file) => {
|
||||||
|
if (file.name === "violentmonkey") {
|
||||||
|
violentmonkeyFile = file;
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
const info = resourceFilenameMap.get(file.name);
|
||||||
|
if (info === undefined) {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
const resource = map.get(info.key)![info.type][info.index];
|
||||||
|
resource.base64 = await blobToBase64(await (await this.fs.open(file)).read("blob"));
|
||||||
|
if (resource.meta) {
|
||||||
|
// 存在meta
|
||||||
|
// 替换base64前缀
|
||||||
|
if (resource.meta.mimetype) {
|
||||||
|
resource.base64 = resource.base64.replace(/^data:.*?;base64,/, `data:${resource.meta.mimetype};base64,`);
|
||||||
|
}
|
||||||
|
if (isText(await (await this.fs.open(file)).read("blob"))) {
|
||||||
|
resource.source = await (await this.fs.open(file)).read();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Promise.resolve(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
files.length &&
|
||||||
|
this.logger.warn("unhandled files", {
|
||||||
|
num: files.length,
|
||||||
|
files: files.map((f) => f.name),
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理暴力猴导入资源
|
||||||
|
if (violentmonkeyFile) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(await (await this.fs.open(violentmonkeyFile)).read("string")) as ViolentmonkeyFile;
|
||||||
|
// 设置开启状态
|
||||||
|
const keys = Object.keys(data.scripts);
|
||||||
|
keys.forEach((key) => {
|
||||||
|
const vioScript = data.scripts[key];
|
||||||
|
if (!vioScript.config.enabled) {
|
||||||
|
const script = map.get(key);
|
||||||
|
if (!script) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
script.enabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.error("violentmonkey file parse error", Logger.E(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将map转化为数组
|
||||||
|
return Promise.resolve({
|
||||||
|
script: Array.from(map.values()),
|
||||||
|
subscribe: Array.from(subscribe.values()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async dealFile(files: File[], handler: (file: File) => Promise<boolean>): Promise<File[]> {
|
||||||
|
const newFiles: File[] = [];
|
||||||
|
const results = await Promise.all(files.map(handler));
|
||||||
|
results.forEach((result, index) => {
|
||||||
|
if (!result) {
|
||||||
|
newFiles.push(files[index]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Promise.resolve(newFiles);
|
||||||
|
}
|
||||||
|
}
|
108
src/pkg/backup/struct.ts
Normal file
108
src/pkg/backup/struct.ts
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
/* eslint-disable camelcase */
|
||||||
|
|
||||||
|
export type ResourceMeta = {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
ts: number;
|
||||||
|
mimetype?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ResourceBackup = {
|
||||||
|
meta: ResourceMeta;
|
||||||
|
// text数据
|
||||||
|
source?: string;
|
||||||
|
// 二进制数据
|
||||||
|
base64: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ValueStorage = {
|
||||||
|
data: { [key: string]: any };
|
||||||
|
ts: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ScriptOptions = {
|
||||||
|
check_for_updates: boolean;
|
||||||
|
comment: string | null;
|
||||||
|
compat_foreach: boolean;
|
||||||
|
compat_metadata: boolean;
|
||||||
|
compat_prototypes: boolean;
|
||||||
|
compat_wrappedjsobject: boolean;
|
||||||
|
compatopts_for_requires: boolean;
|
||||||
|
noframes: boolean | null;
|
||||||
|
override: {
|
||||||
|
merge_connects: boolean;
|
||||||
|
merge_excludes: boolean;
|
||||||
|
merge_includes: boolean;
|
||||||
|
merge_matches: boolean;
|
||||||
|
orig_connects: Array<string>;
|
||||||
|
orig_excludes: Array<string>;
|
||||||
|
orig_includes: Array<string>;
|
||||||
|
orig_matches: Array<string>;
|
||||||
|
orig_noframes: boolean | null;
|
||||||
|
orig_run_at: string;
|
||||||
|
use_blockers: Array<string>;
|
||||||
|
use_connects: Array<string>;
|
||||||
|
use_excludes: Array<string>;
|
||||||
|
use_includes: Array<string>;
|
||||||
|
use_matches: Array<string>;
|
||||||
|
};
|
||||||
|
run_at: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ScriptMeta = {
|
||||||
|
name: string;
|
||||||
|
uuid: string; // 此uuid是对tm的兼容处理
|
||||||
|
sc_uuid: string; // 脚本猫uuid
|
||||||
|
modified: number;
|
||||||
|
file_url: string;
|
||||||
|
subscribe_url?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ScriptOptionsFile = {
|
||||||
|
options: ScriptOptions;
|
||||||
|
settings: { enabled: boolean; position: number };
|
||||||
|
meta: ScriptMeta;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ScriptInfo = {
|
||||||
|
name: string;
|
||||||
|
code: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ScriptBackupData = {
|
||||||
|
code: string;
|
||||||
|
options?: ScriptOptionsFile;
|
||||||
|
storage: ValueStorage;
|
||||||
|
requires: ResourceBackup[];
|
||||||
|
requiresCss: ResourceBackup[];
|
||||||
|
resources: ResourceBackup[];
|
||||||
|
// 为了兼容暴力猴而设置的字段
|
||||||
|
enabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SubscribeScript = {
|
||||||
|
uuid: string;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SubscribeMeta = {
|
||||||
|
name: string;
|
||||||
|
modified: number;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SubscribeOptionsFile = {
|
||||||
|
settings: { enabled: boolean };
|
||||||
|
scripts: { [key: string]: SubscribeScript };
|
||||||
|
meta: SubscribeMeta;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SubscribeBackupData = {
|
||||||
|
source: string;
|
||||||
|
options?: SubscribeOptionsFile;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BackupData = {
|
||||||
|
script: ScriptBackupData[];
|
||||||
|
subscribe: SubscribeBackupData[];
|
||||||
|
};
|
10
src/pkg/backup/utils.ts
Normal file
10
src/pkg/backup/utils.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import ZipFileSystem from "@Packages/filesystem/zip/zip";
|
||||||
|
import JSZip from "jszip";
|
||||||
|
import BackupImport from "./import";
|
||||||
|
|
||||||
|
// 解析备份文件
|
||||||
|
export function parseBackupZipFile(zip: JSZip) {
|
||||||
|
const fs = new ZipFileSystem(zip);
|
||||||
|
// 解析文件
|
||||||
|
return new BackupImport(fs).parse();
|
||||||
|
}
|
@ -33,6 +33,13 @@ export class SystemConfig {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addListener(key: string, callback: (value: any) => void) {
|
||||||
|
this.mq.subscribe(key, (msg) => {
|
||||||
|
const { value } = msg;
|
||||||
|
callback(value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async getAll(): Promise<{ [key: string]: any }> {
|
async getAll(): Promise<{ [key: string]: any }> {
|
||||||
const ret: { [key: string]: any } = {};
|
const ret: { [key: string]: any } = {};
|
||||||
const list = await this.storage.keys();
|
const list = await this.storage.keys();
|
||||||
@ -58,7 +65,9 @@ export class SystemConfig {
|
|||||||
|
|
||||||
public set(key: string, val: any) {
|
public set(key: string, val: any) {
|
||||||
this.cache.set(key, val);
|
this.cache.set(key, val);
|
||||||
this.storage.set(key, val);
|
this.storage.set(key, val).then(() => {
|
||||||
|
console.log(chrome.runtime.lastError, val);
|
||||||
|
});
|
||||||
// 发送消息通知更新
|
// 发送消息通知更新
|
||||||
this.mq.publish(SystamConfigChange, {
|
this.mq.publish(SystamConfigChange, {
|
||||||
key,
|
key,
|
||||||
|
@ -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 "";
|
||||||
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import ServiceWorkerManager from "./app/service/service_worker";
|
import ServiceWorkerManager from "./app/service/service_worker";
|
||||||
import migrate from "./app/migrate";
|
|
||||||
import LoggerCore from "./app/logger/core";
|
import LoggerCore from "./app/logger/core";
|
||||||
import DBWriter from "./app/logger/db_writer";
|
import DBWriter from "./app/logger/db_writer";
|
||||||
import { LoggerDAO } from "./app/repo/logger";
|
import { LoggerDAO } from "./app/repo/logger";
|
||||||
@ -7,8 +6,8 @@ import { ExtensionMessage } from "@Packages/message/extension_message";
|
|||||||
import { Server } from "@Packages/message/server";
|
import { Server } from "@Packages/message/server";
|
||||||
import { MessageQueue } from "@Packages/message/message_queue";
|
import { MessageQueue } from "@Packages/message/message_queue";
|
||||||
import { ServiceWorkerMessageSend } from "@Packages/message/window_message";
|
import { ServiceWorkerMessageSend } from "@Packages/message/window_message";
|
||||||
|
import migrate from "./app/migrate";
|
||||||
|
|
||||||
// 初始化数据库
|
|
||||||
migrate();
|
migrate();
|
||||||
|
|
||||||
const OFFSCREEN_DOCUMENT_PATH = "src/offscreen.html";
|
const OFFSCREEN_DOCUMENT_PATH = "src/offscreen.html";
|
||||||
|
Loading…
x
Reference in New Issue
Block a user