diff --git a/.yarnrc.yml b/.yarnrc.yml index 3186f3f..0563839 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -1 +1,3 @@ nodeLinker: node-modules + +yarnPath: .yarn/releases/yarn-3.6.3.cjs diff --git a/package.json b/package.json index ec6f30e..c4c556d 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "react-markdown": "^8.0.7", "react-router-dom": "^6.10.0", "react-scripts": "5.0.1", + "webdav": "^5.3.0", "webextension-polyfill": "^0.10.0" }, "scripts": { @@ -61,5 +62,6 @@ "@babel/preset-env": "^7.22.10", "react-app-rewired": "^2.2.1", "wrangler": "^3.4.0" - } + }, + "packageManager": "yarn@3.6.3" } diff --git a/src/config/i18n.js b/src/config/i18n.js index dcd4726..7986644 100644 --- a/src/config/i18n.js +++ b/src/config/i18n.js @@ -435,10 +435,18 @@ export const I18N = { zh: `重启浏览器时清除缓存`, en: `Clear cache when restarting browser`, }, + data_sync_type: { + zh: `数据同步方式`, + en: `Data Sync Type`, + }, data_sync_url: { zh: `数据同步接口`, en: `Data Sync API`, }, + data_sync_user: { + zh: `数据同步账户`, + en: `Data Sync User`, + }, data_sync_key: { zh: `数据同步密钥`, en: `Data Sync Key`, @@ -460,8 +468,8 @@ export const I18N = { en: `Sorry, something went wrong!`, }, error_sync_setting: { - zh: `您的同步设置未填写,无法在线分享。`, - en: `Your sync settings are missing and cannot be shared online.`, + zh: `您的同步类型必须为“KISS-Worker”,且需填写完整`, + en: `Your sync type must be "KISS-Worker" and must be filled in completely`, }, click_test: { zh: `点击测试`, diff --git a/src/libs/sync.js b/src/libs/sync.js index caef94d..747a686 100644 --- a/src/libs/sync.js +++ b/src/libs/sync.js @@ -1,4 +1,5 @@ import { + APP_LCNAME, KV_SETTING_KEY, KV_RULES_KEY, KV_RULES_SHARE_KEY, @@ -15,6 +16,45 @@ import { } from "./storage"; import { apiSyncData } from "../apis"; import { sha256 } from "./utils"; +import { createClient } from "webdav"; + +const syncByWebdav = async ({ + key, + value, + syncUrl, + syncUser, + syncKey, + updateAt = 0, + syncAt = 0, + isForce = false, +}) => { + const client = createClient(syncUrl, { + username: syncUser, + password: syncKey, + }); + const pathname = `/${APP_LCNAME}`; + const filename = `/${APP_LCNAME}/${key}`; + const data = JSON.stringify(value, null, " "); + + if ((await client.exists(pathname)) === false) { + await client.createDirectory(pathname); + } + + const isExist = await client.exists(filename); + if (isExist && !isForce) { + const { lastmod } = await client.stat(filename); + const fileUpdateAt = Date.parse(lastmod); + if (syncAt === 0 || fileUpdateAt > updateAt) { + const data = await client.getFileContents(filename, { format: "text" }); + return { updateAt: fileUpdateAt, value: JSON.parse(data) }; + } + } + + await client.putFileContents(filename, data); + const { lastmod } = await client.stat(filename); + const fileUpdateAt = Date.parse(lastmod); + return { updateAt: fileUpdateAt, value }; +}; const syncByWorker = async ({ key, @@ -59,16 +99,21 @@ const syncSetting = async (isBg = false, isForce = false) => { } const setting = await getSettingWithDefault(); - const res = await syncByWorker({ + const args = { key: KV_SETTING_KEY, value: setting, syncUrl, + syncUser, syncKey, updateAt: settingUpdateAt, syncAt: settingSyncAt, isBg, isForce, - }); + }; + const res = + syncType === OPT_SYNCTYPE_WEBDAV + ? await syncByWebdav(args) + : await syncByWorker(args); if (res.updateAt > settingUpdateAt) { await setSetting(res.value); @@ -107,16 +152,21 @@ const syncRules = async (isBg = false, isForce = false) => { } const rules = await getRulesWithDefault(); - const res = await syncByWorker({ + const args = { key: KV_RULES_KEY, value: rules, syncUrl, + syncUser, syncKey, updateAt: rulesUpdateAt, syncAt: rulesSyncAt, isBg, isForce, - }); + }; + const res = + syncType === OPT_SYNCTYPE_WEBDAV + ? await syncByWebdav(args) + : await syncByWorker(args); if (res.updateAt > rulesUpdateAt) { await setRules(res.value); @@ -143,14 +193,15 @@ export const trySyncRules = async (isBg = false, isForce = false) => { * @returns */ export const syncShareRules = async ({ rules, syncUrl, syncKey }) => { - await syncByWorker({ + const args = { key: KV_RULES_SHARE_KEY, value: rules, syncUrl, syncKey, updateAt: Date.now(), syncAt: Date.now(), - }); + }; + await syncByWorker(args); const psk = await sha256(syncKey, KV_SALT_SHARE); const shareUrl = `${syncUrl}/rules?psk=${psk}`; return shareUrl; diff --git a/src/views/Options/Rules.js b/src/views/Options/Rules.js index 514ba55..67160ad 100644 --- a/src/views/Options/Rules.js +++ b/src/views/Options/Rules.js @@ -14,6 +14,7 @@ import { OPT_STYLE_DIY, OPT_STYLE_USE_COLOR, URL_KISS_RULES_NEW_ISSUE, + OPT_SYNCTYPE_WORKER, } from "../../config"; import { useState, useRef, useEffect, useMemo } from "react"; import { useI18n } from "../../hooks/I18n"; @@ -445,8 +446,8 @@ function ShareButton({ rules, injectRules, selectedUrl }) { const i18n = useI18n(); const handleClick = async () => { try { - const { syncUrl, syncKey } = await getSyncWithDefault(); - if (!syncUrl || !syncKey) { + const { syncType, syncUrl, syncKey } = await getSyncWithDefault(); + if (syncType !== OPT_SYNCTYPE_WORKER || !syncUrl || !syncKey) { alert.warning(i18n("error_sync_setting")); return; } diff --git a/src/views/Options/SyncSetting.js b/src/views/Options/SyncSetting.js index 08d18e4..6bb4206 100644 --- a/src/views/Options/SyncSetting.js +++ b/src/views/Options/SyncSetting.js @@ -5,7 +5,13 @@ import { useI18n } from "../../hooks/I18n"; import { useSync } from "../../hooks/Sync"; import Alert from "@mui/material/Alert"; import Link from "@mui/material/Link"; -import { URL_KISS_WORKER } from "../../config"; +import MenuItem from "@mui/material/MenuItem"; +import { + URL_KISS_WORKER, + OPT_SYNCTYPE_ALL, + OPT_SYNCTYPE_WORKER, + OPT_SYNCTYPE_WEBDAV, +} from "../../config"; import { useState } from "react"; import { syncSettingAndRules } from "../../libs/sync"; import Button from "@mui/material/Button"; @@ -48,13 +54,33 @@ export default function SyncSetting() { return; } - const { syncUrl = "", syncKey = "" } = sync; + const { + syncType = OPT_SYNCTYPE_WORKER, + syncUrl = "", + syncUser = "", + syncKey = "", + } = sync; return ( {i18n("sync_warn")} + + {OPT_SYNCTYPE_ALL.map((item) => ( + + {item} + + ))} + + - {i18n("about_sync_api")} - + syncType === OPT_SYNCTYPE_WORKER && ( + + {i18n("about_sync_api")} + + ) } /> + {syncType === OPT_SYNCTYPE_WEBDAV && ( + + )} +