导入导出
This commit is contained in:
@ -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 {
|
||||||
|
@ -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,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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -150,3 +150,17 @@ 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");
|
||||||
|
}
|
||||||
|
|
||||||
|
backup(uuids?: string[]) {
|
||||||
|
return this.do("backup", uuids);
|
||||||
|
}
|
||||||
|
|
||||||
|
openImportWindow(filename: string, url: string) {
|
||||||
|
return this.do("openImportWindow", { filename, url });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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 } };
|
||||||
|
@ -25,6 +25,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);
|
||||||
|
|
||||||
@ -38,7 +39,7 @@ export default class ServiceWorkerManager {
|
|||||||
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.api.group("synchronize"));
|
const synchronize = new SynchronizeService(this.sender, this.api.group("synchronize"), value, resource);
|
||||||
synchronize.init();
|
synchronize.init();
|
||||||
|
|
||||||
// 定时器处理
|
// 定时器处理
|
||||||
|
@ -142,7 +142,7 @@ export class ScriptService {
|
|||||||
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}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,202 @@
|
|||||||
import LoggerCore from "@App/app/logger/core";
|
import LoggerCore from "@App/app/logger/core";
|
||||||
import Logger from "@App/app/logger/logger";
|
import Logger from "@App/app/logger/logger";
|
||||||
import { ScriptDAO } from "@App/app/repo/scripts";
|
import { Resource } from "@App/app/repo/resource";
|
||||||
import { Group } from "@Packages/message/server";
|
import { Script, SCRIPT_STATUS_ENABLE, ScriptCodeDAO, ScriptDAO } from "@App/app/repo/scripts";
|
||||||
|
import BackupExport from "@App/pkg/backup/export";
|
||||||
|
import { BackupData, ResourceBackup, ScriptBackupData, ScriptOptions, ValueStorage } from "@App/pkg/backup/struct";
|
||||||
|
import FileSystem from "@Packages/filesystem/filesystem";
|
||||||
|
import 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";
|
||||||
|
|
||||||
export class SynchronizeService {
|
export class SynchronizeService {
|
||||||
logger: Logger;
|
logger: Logger;
|
||||||
|
|
||||||
scriptDAO = new ScriptDAO();
|
scriptDAO = new ScriptDAO();
|
||||||
|
scriptCodeDAO = new ScriptCodeDAO();
|
||||||
|
|
||||||
constructor(private group: Group) {
|
constructor(
|
||||||
|
private send: MessageSend,
|
||||||
|
private group: Group,
|
||||||
|
private value: ValueService,
|
||||||
|
private resource: ResourceService
|
||||||
|
) {
|
||||||
this.logger = LoggerCore.logger().with({ service: "synchronize" });
|
this.logger = LoggerCore.logger().with({ service: "synchronize" });
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {}
|
// 生成备份文件到文件系统
|
||||||
|
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 requestBackup(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 openImportWindow(url: string) {}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.group.on("backup", this.requestBackup.bind(this));
|
||||||
|
this.group.on("import", this.openImportWindow.bind(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -364,5 +364,6 @@
|
|||||||
"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": "导出成功"
|
||||||
}
|
}
|
@ -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,
|
||||||
|
@ -17,7 +17,7 @@ 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;
|
||||||
|
}
|
33
src/pages/import/main.tsx
Normal file
33
src/pages/import/main.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
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 migrate from "@App/app/migrate.ts";
|
||||||
|
import { LoggerDAO } from "@App/app/repo/logger.ts";
|
||||||
|
import DBWriter from "@App/app/logger/db_writer.ts";
|
||||||
|
|
||||||
|
// 初始化数据库
|
||||||
|
migrate();
|
||||||
|
// 初始化日志组件
|
||||||
|
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>
|
||||||
|
);
|
@ -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).backup(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")!)) {
|
||||||
|
@ -7,7 +7,13 @@ 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 { FileSystemType } from "@Packages/filesystem/factory";
|
||||||
import { systemConfig } from "@App/pages/store/global";
|
import { message, systemConfig } from "@App/pages/store/global";
|
||||||
|
import { SynchronizeClient } from "@App/app/service/service_worker/client";
|
||||||
|
import Cache from "@App/app/cache";
|
||||||
|
import CacheKey from "@App/app/cache_key";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
const synchronizeClient = new SynchronizeClient(message);
|
||||||
|
|
||||||
function Tools() {
|
function Tools() {
|
||||||
const [loading, setLoading] = useState<{ [key: string]: boolean }>({});
|
const [loading, setLoading] = useState<{ [key: string]: boolean }>({});
|
||||||
@ -49,7 +55,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.backup();
|
||||||
setLoading((prev) => ({ ...prev, local: false }));
|
setLoading((prev) => ({ ...prev, local: false }));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -58,14 +64,36 @@ 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;
|
||||||
Message.success(t("select_import_script")!);
|
if (!files) {
|
||||||
})
|
return;
|
||||||
.then((e) => {
|
}
|
||||||
Message.error(`${t("import_error")}${e}`);
|
const file = files[0];
|
||||||
});
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = URL.createObjectURL(file);
|
||||||
|
try {
|
||||||
|
const uuid = uuidv4();
|
||||||
|
Cache.getInstance()
|
||||||
|
.set(CacheKey.importFile(uuid), {
|
||||||
|
filename: file.name,
|
||||||
|
url: url,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
Message.success(t("select_import_script")!);
|
||||||
|
// 打开导入窗口
|
||||||
|
chrome.tabs.create({
|
||||||
|
url: `/src/import.html?uuid=${uuid}`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
Message.error(`${t("import_error")}: ${e}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
el.click();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("import_file")}
|
{t("import_file")}
|
||||||
|
@ -8,13 +8,20 @@ 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,
|
||||||
|
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 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();
|
||||||
|
}
|
Reference in New Issue
Block a user