导入导出
This commit is contained in:
parent
e2832093f0
commit
07c4518cba
@ -1,5 +1,3 @@
|
||||
/* eslint-disable max-classes-per-file */
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import { calculateMd5 } from "@App/pkg/utils/utils";
|
||||
import { MD5 } from "crypto-js";
|
||||
import { File, FileReader, FileWriter } from "../filesystem";
|
||||
|
@ -1,9 +1,5 @@
|
||||
import JSZip from "jszip";
|
||||
import FileSystem, {
|
||||
File,
|
||||
FileReader,
|
||||
FileWriter,
|
||||
} from "@Pkg/filesystem/filesystem";
|
||||
import FileSystem, { File, FileReader, FileWriter } from "@Packages/filesystem/filesystem";
|
||||
import { ZipFileReader, ZipFileWriter } from "./rw";
|
||||
|
||||
export default class ZipFileSystem implements FileSystem {
|
||||
|
@ -36,6 +36,7 @@ export default defineConfig({
|
||||
popup: `${src}/pages/popup/main.tsx`,
|
||||
install: `${src}/pages/install/main.tsx`,
|
||||
confirm: `${src}/pages/confirm/main.tsx`,
|
||||
import: `${src}/pages/import/main.tsx`,
|
||||
options: `${src}/pages/options/main.tsx`,
|
||||
"editor.worker": "monaco-editor/esm/vs/editor/editor.worker.js",
|
||||
"ts.worker": "monaco-editor/esm/vs/language/typescript/ts.worker.js",
|
||||
@ -152,7 +153,6 @@ export default defineConfig({
|
||||
minify: true,
|
||||
chunks: ["install"],
|
||||
}),
|
||||
,
|
||||
new rspack.HtmlRspackPlugin({
|
||||
filename: `${dist}/ext/src/confirm.html`,
|
||||
template: `${src}/pages/template.html`,
|
||||
@ -161,6 +161,14 @@ export default defineConfig({
|
||||
minify: true,
|
||||
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({
|
||||
filename: `${dist}/ext/src/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 {
|
||||
// 加载脚本信息时的缓存
|
||||
@ -9,4 +9,9 @@ export default class CacheKey {
|
||||
static permissionConfirm(scriptUuid: string, confirm: ConfirmParam): string {
|
||||
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 { SCRIPT_RUN_STATUS, ScriptRunResouce } from "@App/app/repo/scripts";
|
||||
import { sendMessage } from "@Packages/message/client";
|
||||
import { MessageSend } from "@Packages/message/server";
|
||||
import { MessageSend } from "@Packages/message/server";
|
||||
|
||||
export function preparationSandbox(msg: WindowMessage) {
|
||||
return sendMessage(msg, "offscreen/preparationSandbox");
|
||||
@ -31,3 +31,7 @@ export function runScript(msg: MessageSend, data: ScriptRunResouce) {
|
||||
export function stopScript(msg: MessageSend, uuid: string) {
|
||||
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"));
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
setTimeout(() => {
|
||||
URL.revokeObjectURL(url);
|
||||
}, 6000);
|
||||
}, 30*1000);
|
||||
return { action: "onload", data: url };
|
||||
} catch (e: any) {
|
||||
return { action: "error", data: { code: 5, error: e.message } };
|
||||
|
@ -25,6 +25,7 @@ export default class ServiceWorkerManager {
|
||||
await this.sender.init();
|
||||
this.mq.emit("preparationOffscreen", {});
|
||||
});
|
||||
this.sender.init();
|
||||
|
||||
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);
|
||||
popup.init();
|
||||
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();
|
||||
|
||||
// 定时器处理
|
||||
|
@ -142,7 +142,7 @@ export class ScriptService {
|
||||
setTimeout(() => {
|
||||
// 清理缓存
|
||||
Cache.getInstance().del(CacheKey.scriptInstallInfo(uuid));
|
||||
}, 60 * 1000);
|
||||
}, 30 * 1000);
|
||||
openInCurrentTab(`/src/install.html?uuid=${uuid}`);
|
||||
});
|
||||
}
|
||||
|
@ -1,16 +1,202 @@
|
||||
import LoggerCore from "@App/app/logger/core";
|
||||
import Logger from "@App/app/logger/logger";
|
||||
import { ScriptDAO } from "@App/app/repo/scripts";
|
||||
import { Group } from "@Packages/message/server";
|
||||
import { Resource } from "@App/app/repo/resource";
|
||||
import { Script, SCRIPT_STATUS_ENABLE, ScriptCodeDAO, ScriptDAO } from "@App/app/repo/scripts";
|
||||
import BackupExport from "@App/pkg/backup/export";
|
||||
import { BackupData, ResourceBackup, ScriptBackupData, ScriptOptions, ValueStorage } from "@App/pkg/backup/struct";
|
||||
import FileSystem from "@Packages/filesystem/filesystem";
|
||||
import 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 {
|
||||
logger: Logger;
|
||||
|
||||
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" });
|
||||
}
|
||||
|
||||
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_after": "个时,自动隐藏",
|
||||
"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);
|
||||
setTimeout(() => {
|
||||
URL.revokeObjectURL(url);
|
||||
}, 60 * 1000);
|
||||
}, 30 * 1000);
|
||||
chrome.downloads.download({
|
||||
url,
|
||||
saveAs: true,
|
||||
|
@ -17,7 +17,7 @@ migrate();
|
||||
// 初始化日志组件
|
||||
const loggerCore = new LoggerCore({
|
||||
writer: new DBWriter(new LoggerDAO()),
|
||||
labels: { env: "install" },
|
||||
labels: { env: "confirm" },
|
||||
});
|
||||
|
||||
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,
|
||||
} from "@App/pages/store/features/script";
|
||||
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 };
|
||||
|
||||
@ -719,7 +719,17 @@ function ScriptList() {
|
||||
select.forEach((item) => {
|
||||
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;
|
||||
case "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 { useTranslation } from "react-i18next";
|
||||
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() {
|
||||
const [loading, setLoading] = useState<{ [key: string]: boolean }>({});
|
||||
@ -49,7 +55,7 @@ function Tools() {
|
||||
loading={loading.local}
|
||||
onClick={async () => {
|
||||
setLoading((prev) => ({ ...prev, local: true }));
|
||||
await syncCtrl.backup();
|
||||
await synchronizeClient.backup();
|
||||
setLoading((prev) => ({ ...prev, local: false }));
|
||||
}}
|
||||
>
|
||||
@ -58,14 +64,36 @@ function Tools() {
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
syncCtrl
|
||||
.openImportFile(fileRef.current!)
|
||||
.then(() => {
|
||||
Message.success(t("select_import_script")!);
|
||||
})
|
||||
.then((e) => {
|
||||
Message.error(`${t("import_error")}${e}`);
|
||||
});
|
||||
const el = fileRef.current!;
|
||||
el.onchange = async () => {
|
||||
const { files } = el;
|
||||
if (!files) {
|
||||
return;
|
||||
}
|
||||
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")}
|
||||
|
@ -8,13 +8,20 @@ import {
|
||||
ScriptDAO,
|
||||
} from "@App/app/repo/scripts";
|
||||
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";
|
||||
|
||||
export const scriptClient = new ScriptClient(message);
|
||||
export const runtimeClient = new RuntimeClient(message);
|
||||
export const popupClient = new PopupClient(message);
|
||||
export const permissionClient = new PermissionClient(message);
|
||||
export const valueClient = new ValueClient(message);
|
||||
|
||||
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();
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user