重构数据同步功能,新增客户端模式
parent
b42092884e
commit
3b70acc3ca
File diff suppressed because it is too large
Load Diff
|
@ -282,7 +282,6 @@
|
|||
"electron-log": "^4.4.8",
|
||||
"electron-store": "^8.1.0",
|
||||
"font-list": "^1.4.5",
|
||||
"http-terminator": "^3.2.0",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"image-size": "^1.0.2",
|
||||
"jschardet": "^3.0.0",
|
||||
|
@ -291,12 +290,12 @@
|
|||
"music-metadata": "^8.1.3",
|
||||
"needle": "github:lyswhut/needle#93299ac841b7e9a9f82ca7279b88aaaeda404060",
|
||||
"node-id3": "^0.2.5",
|
||||
"socket.io": "^4.6.0",
|
||||
"sortablejs": "^1.15.0",
|
||||
"tunnel": "^0.0.6",
|
||||
"utf-8-validate": "^6.0.2",
|
||||
"vue": "^3.2.47",
|
||||
"vue-router": "^4.1.6"
|
||||
"vue-router": "^4.1.6",
|
||||
"ws": "^8.12.1"
|
||||
},
|
||||
"overrides": {
|
||||
"got": "^11",
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
### 新增
|
||||
|
||||
- 重构数据同步功能,新增客户端模式
|
||||
|
||||
### 优化
|
||||
|
||||
|
|
|
@ -75,3 +75,24 @@ export const DOWNLOAD_STATUS = {
|
|||
} as const
|
||||
|
||||
export const QUALITYS = ['flac24bit', 'flac', 'wav', 'ape', '320k', '192k', '128k'] as const
|
||||
|
||||
export const SYNC_CODE = {
|
||||
helloMsg: 'Hello~::^-^::~v3~',
|
||||
idPrefix: 'OjppZDo6',
|
||||
authMsg: 'lx-music auth::',
|
||||
authFailed: 'Auth failed',
|
||||
missingAuthCode: 'Missing auth code',
|
||||
getServiceIdFailed: 'Get service id failed',
|
||||
connectServiceFailed: 'Connect service failed',
|
||||
connecting: 'Connecting...',
|
||||
unknownServiceAddress: 'Unknown service address',
|
||||
msgBlockedIp: 'Blocked IP',
|
||||
msgConnect: 'lx-music connect',
|
||||
msgAuthFailed: 'Auth failed',
|
||||
} as const
|
||||
|
||||
export const SYNC_CLOSE_CODE = {
|
||||
normal: 1000,
|
||||
failed: 4100,
|
||||
} as const
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import { join } from 'path'
|
|||
import { homedir } from 'os'
|
||||
|
||||
const defaultSetting: LX.AppSetting = {
|
||||
version: '2.0.0',
|
||||
version: '2.1.0',
|
||||
|
||||
'common.windowSizeId': 3,
|
||||
'common.fontSize': 16,
|
||||
|
@ -108,8 +108,11 @@ const defaultSetting: LX.AppSetting = {
|
|||
// 'tray.isToTray': false,
|
||||
'tray.themeId': 0,
|
||||
|
||||
'sync.mode': 'server',
|
||||
'sync.enable': false,
|
||||
'sync.port': '23332',
|
||||
'sync.server.port': '23332',
|
||||
'sync.server.maxSsnapshotNum': 5,
|
||||
'sync.client.host': '',
|
||||
|
||||
'theme.id': 'blue_plus',
|
||||
// 'theme.id': 'green',
|
||||
|
|
|
@ -499,6 +499,11 @@ declare global {
|
|||
*/
|
||||
'tray.themeId': number
|
||||
|
||||
/**
|
||||
* 同步服务模式
|
||||
*/
|
||||
'sync.mode': 'server' | 'client'
|
||||
|
||||
/**
|
||||
* 是否启用同步服务
|
||||
*/
|
||||
|
@ -507,7 +512,17 @@ declare global {
|
|||
/**
|
||||
* 同步服务端口号
|
||||
*/
|
||||
'sync.port': '23332' | string
|
||||
'sync.server.port': '23332' | string
|
||||
|
||||
/**
|
||||
* 最大备份快照数
|
||||
*/
|
||||
'sync.server.maxSsnapshotNum': number
|
||||
|
||||
/**
|
||||
* 同步服务地址
|
||||
*/
|
||||
'sync.client.host': string
|
||||
|
||||
/**
|
||||
* 是否在离开搜索界面时自动清空搜索框
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
declare namespace LX {
|
||||
namespace Sync {
|
||||
|
||||
interface Enable {
|
||||
interface EnableServer {
|
||||
enable: boolean
|
||||
port: string
|
||||
}
|
||||
interface EnableClient {
|
||||
enable: boolean
|
||||
host: string
|
||||
authCode?: string
|
||||
}
|
||||
|
||||
interface SyncActionBase <A> {
|
||||
action: A
|
||||
|
@ -14,14 +19,17 @@ declare namespace LX {
|
|||
}
|
||||
type SyncAction<A, D = undefined> = D extends undefined ? SyncActionBase<A> : SyncActionData<A, D>
|
||||
|
||||
type SyncMainWindowActions = SyncAction<'select_mode', KeyInfo>
|
||||
type SyncMainWindowActions = SyncAction<'select_mode', string>
|
||||
| SyncAction<'close_select_mode'>
|
||||
| SyncAction<'status', Status>
|
||||
| SyncAction<'client_status', ClientStatus>
|
||||
| SyncAction<'server_status', ServerStatus>
|
||||
|
||||
type SyncServiceActions = SyncAction<'select_mode', Mode>
|
||||
| SyncAction<'get_status'>
|
||||
| SyncAction<'get_server_status'>
|
||||
| SyncAction<'get_client_status'>
|
||||
| SyncAction<'generate_code'>
|
||||
| SyncAction<'enable', Enable>
|
||||
| SyncAction<'enable_server', EnableServer>
|
||||
| SyncAction<'enable_client', EnableClient>
|
||||
|
||||
type ActionList = SyncAction<'list_data_overwrite', LX.List.ListActionDataOverwrite>
|
||||
| SyncAction<'list_create', LX.List.ListActionAdd>
|
||||
|
@ -36,25 +44,55 @@ declare namespace LX {
|
|||
| SyncAction<'list_music_overwrite', LX.List.ListActionMusicOverwrite>
|
||||
| SyncAction<'list_music_clear', LX.List.ListActionMusicClear>
|
||||
|
||||
type ActionSync = SyncAction<'list:sync:list_sync_get_md5', string>
|
||||
| SyncAction<'list:sync:list_sync_get_list_data', ListData>
|
||||
| SyncAction<'list:sync:list_sync_get_sync_mode', Mode>
|
||||
| SyncAction<'list:sync:action', ActionList>
|
||||
// | SyncAction<'finished'>
|
||||
|
||||
type ActionSyncType = Actions<ActionSync>
|
||||
|
||||
type ActionSyncSend = SyncAction<'list:sync:list_sync_get_md5'>
|
||||
| SyncAction<'list:sync:list_sync_get_list_data'>
|
||||
| SyncAction<'list:sync:list_sync_get_sync_mode'>
|
||||
| SyncAction<'list:sync:list_sync_set_data', LX.Sync.ListData>
|
||||
| SyncAction<'list:sync:action', ActionList>
|
||||
| SyncAction<'list:sync:finished'>
|
||||
|
||||
type ActionSyncSendType = Actions<ActionSyncSend>
|
||||
|
||||
interface List {
|
||||
action: string
|
||||
data: any
|
||||
}
|
||||
|
||||
interface Status {
|
||||
interface ServerStatus {
|
||||
status: boolean
|
||||
message: string
|
||||
address: string[]
|
||||
code: string
|
||||
devices: KeyInfo[]
|
||||
devices: ServerKeyInfo[]
|
||||
}
|
||||
|
||||
interface KeyInfo {
|
||||
interface ClientStatus {
|
||||
status: boolean
|
||||
message: string
|
||||
address: string[]
|
||||
}
|
||||
|
||||
interface ClientKeyInfo {
|
||||
clientId: string
|
||||
key: string
|
||||
serverName: string
|
||||
}
|
||||
|
||||
interface ServerKeyInfo {
|
||||
clientId: string
|
||||
key: string
|
||||
deviceName: string
|
||||
connectionTime?: number
|
||||
lastSyncDate?: number
|
||||
snapshotKey: string
|
||||
isMobile: boolean
|
||||
}
|
||||
|
||||
type ListData = Omit<LX.List.ListDataFull, 'tempList'>
|
||||
|
@ -65,7 +103,7 @@ declare namespace LX {
|
|||
| 'overwrite_remote_local'
|
||||
| 'overwrite_local_remote_full'
|
||||
| 'overwrite_remote_local_full'
|
||||
| 'none'
|
||||
// | 'none'
|
||||
| 'cancel'
|
||||
|
||||
}
|
||||
|
|
|
@ -5,3 +5,8 @@ type DeepPartial<T> = {
|
|||
}
|
||||
|
||||
type Modify<T, R> = Omit<T, keyof R> & R
|
||||
|
||||
// type UndefinedOrNever = undefined
|
||||
type Actions<T extends { action: string, data?: any }> = {
|
||||
[U in T as U['action']]: 'data' extends keyof U ? U['data'] : undefined
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ const oldThemeMap = {
|
|||
export default (setting: any): Partial<LX.AppSetting> => {
|
||||
setting = { ...setting }
|
||||
|
||||
// 迁移 v2 之前的配置
|
||||
// 迁移 v2.0.0 之前的配置
|
||||
if (compareVer(setting.version, '2.0.0') < 0) {
|
||||
// 迁移列表滚动位置设置 ~0.18.3
|
||||
if (setting.list?.scroll) {
|
||||
|
@ -133,7 +133,16 @@ export default (setting: any): Partial<LX.AppSetting> => {
|
|||
|
||||
setting['odc.isAutoClearSearchInput'] = setting.odc?.isAutoClearSearchInput
|
||||
setting['odc.isAutoClearSearchList'] = setting.odc?.isAutoClearSearchList
|
||||
|
||||
setting.version = '2.0.0'
|
||||
}
|
||||
|
||||
// 迁移 v2.2.0 之前的设置数据
|
||||
if (compareVer(setting.version, '2.1.0') < 0) {
|
||||
setting['sync.erver.port'] = setting['sync.port']
|
||||
setting.version = '2.1.0'
|
||||
}
|
||||
|
||||
|
||||
return setting
|
||||
}
|
||||
|
|
|
@ -454,13 +454,24 @@
|
|||
"setting__setting__desktop_lyric_font_weight_font": "verbatim lyrics",
|
||||
"setting__setting__desktop_lyric_font_weight_line": "progressive lyrics",
|
||||
"setting__sync": "Data synchronization",
|
||||
"setting__sync_address": "Synchronization service address: {address}",
|
||||
"setting__sync_auth_code": "Connection code: {code}",
|
||||
"setting__sync_device": "Connected devices: {devices}",
|
||||
"setting__sync_enable": "Enable the synchronization function (because the data is transmitted in clear text, please use it under a trusted network)",
|
||||
"setting__sync_port": "Sync port settings",
|
||||
"setting__sync_port_tip": "Please enter the synchronization service port number",
|
||||
"setting__sync_refresh_code": "Refresh the connection code",
|
||||
"setting__sync_client_address": "Current device address: {address}",
|
||||
"setting__sync_client_host": "Synchronization service address",
|
||||
"setting__sync_client_host_tip": "Please enter the synchronization service address",
|
||||
"setting__sync_client_mode": "client mode",
|
||||
"setting__sync_client_status": "Status: {status}",
|
||||
"setting__sync_code_blocked_ip": "The IP of the current device has been blocked by the server!",
|
||||
"setting__sync_code_fail": "Invalid connection code",
|
||||
"setting__sync_enable": "Enable sync",
|
||||
"setting__sync_mode": "Synchronous mode",
|
||||
"setting__sync_mode_client": "client mode",
|
||||
"setting__sync_mode_server": "server mode",
|
||||
"setting__sync_server_address": "Synchronization service address: {address}",
|
||||
"setting__sync_server_auth_code": "Connection code: {code}",
|
||||
"setting__sync_server_device": "Connected devices: {devices}",
|
||||
"setting__sync_server_mode": "Server mode (since the data is transmitted in clear text, please use it under a trusted network)",
|
||||
"setting__sync_server_port": "Sync port settings",
|
||||
"setting__sync_server_port_tip": "Please enter the synchronization service port number",
|
||||
"setting__sync_server_refresh_code": "Refresh the connection code",
|
||||
"setting__sync_tip": "For how to use it, please see the \"Sync function\" section of the FAQ",
|
||||
"setting__update": "Update",
|
||||
"setting__update_checking": "Checking for updates...",
|
||||
|
@ -475,6 +486,7 @@
|
|||
"setting__update_show_change_log": "Show changelog on first boot after version update",
|
||||
"setting__update_try_auto_update": "Attempt to download updates automatically when a new version is found",
|
||||
"setting__update_unknown": "Unknown",
|
||||
"setting_sync_status_enabled": "connected",
|
||||
"song_list": "Playlists",
|
||||
"songlist__import_input_btn_confirm": "Open",
|
||||
"songlist__import_input_show_btn": "Open Playlist",
|
||||
|
@ -502,6 +514,8 @@
|
|||
"source_tx": "Tencent",
|
||||
"source_wy": "Netease",
|
||||
"source_xm": "Xiami",
|
||||
"sync__auth_code_input_tip": "Please enter the connection code",
|
||||
"sync__auth_code_title": "Need to enter the connection code",
|
||||
"sync__merge_btn_local_remote": "Local list merge remote list",
|
||||
"sync__merge_btn_remote_local": "Remote list merge local list",
|
||||
"sync__merge_label": "Merge",
|
||||
|
@ -519,6 +533,7 @@
|
|||
"sync__overwrite_tip": "Cover: ",
|
||||
"sync__overwrite_tip_desc": "The list with the same ID of the covered person and the covered list will be deleted and replaced with the list of the covered person (lists with different list IDs will be merged together). If you check Complete coverage, all lists of the covered person will be moved. \nDivide, and then replace with a list of overriders.",
|
||||
"sync__title": "Choose how to synchronize the list with {name}",
|
||||
"sync_status_disabled": "not connected",
|
||||
"tag__high_quality": "HQ",
|
||||
"tag__lossless": "SQ",
|
||||
"tag__lossless_24bit": "24bit",
|
||||
|
|
|
@ -457,13 +457,24 @@
|
|||
"setting__setting__desktop_lyric_font_weight_font": "逐字歌词",
|
||||
"setting__setting__desktop_lyric_font_weight_line": "逐行歌词",
|
||||
"setting__sync": "数据同步",
|
||||
"setting__sync_address": "同步服务地址:{address}",
|
||||
"setting__sync_auth_code": "连接码:{code}",
|
||||
"setting__sync_device": "已连接的设备:{devices}",
|
||||
"setting__sync_enable": "启用同步功能(由于数据是明文传输,请在受信任的网络下使用)",
|
||||
"setting__sync_port": "同步端口设置",
|
||||
"setting__sync_port_tip": "请输入同步服务端口号",
|
||||
"setting__sync_refresh_code": "刷新连接码",
|
||||
"setting__sync_client_address": "当前设备地址:{address}",
|
||||
"setting__sync_client_host": "同步服务地址",
|
||||
"setting__sync_client_host_tip": "请输入同步服务地址",
|
||||
"setting__sync_client_mode": "客户端模式",
|
||||
"setting__sync_client_status": "状态:{status}",
|
||||
"setting__sync_code_blocked_ip": "当前设备的IP已被服务端封禁!",
|
||||
"setting__sync_code_fail": "连接码无效",
|
||||
"setting__sync_enable": "启用同步功能",
|
||||
"setting__sync_mode": "同步模式",
|
||||
"setting__sync_mode_client": "客户端模式",
|
||||
"setting__sync_mode_server": "服务端模式",
|
||||
"setting__sync_server_address": "同步服务地址:{address}",
|
||||
"setting__sync_server_auth_code": "连接码:{code}",
|
||||
"setting__sync_server_device": "已连接的设备:{devices}",
|
||||
"setting__sync_server_mode": "服务端模式(由于数据是明文传输,请在受信任的网络下使用)",
|
||||
"setting__sync_server_port": "同步端口设置",
|
||||
"setting__sync_server_port_tip": "请输入同步服务端口号",
|
||||
"setting__sync_server_refresh_code": "刷新连接码",
|
||||
"setting__sync_tip": "使用方式请看常见问题“同步功能”部分",
|
||||
"setting__update": "软件更新",
|
||||
"setting__update_checking": "检查更新中...",
|
||||
|
@ -478,6 +489,7 @@
|
|||
"setting__update_show_change_log": "更新版本后的首次启动时显示更新日志",
|
||||
"setting__update_try_auto_update": "发现新版本时尝试自动下载更新",
|
||||
"setting__update_unknown": "未知",
|
||||
"setting_sync_status_enabled": "已连接",
|
||||
"song_list": "歌单",
|
||||
"songlist__import_input_btn_confirm": "打开",
|
||||
"songlist__import_input_show_btn": "打开歌单",
|
||||
|
@ -505,6 +517,8 @@
|
|||
"source_tx": "企鹅音乐",
|
||||
"source_wy": "网易音乐",
|
||||
"source_xm": "虾米音乐",
|
||||
"sync__auth_code_input_tip": "请输入连接码",
|
||||
"sync__auth_code_title": "需要输入连接码",
|
||||
"sync__merge_btn_local_remote": "本机列表 合并 远程列表",
|
||||
"sync__merge_btn_remote_local": "远程列表 合并 本机列表",
|
||||
"sync__merge_label": "合并",
|
||||
|
@ -522,6 +536,7 @@
|
|||
"sync__overwrite_tip": "覆盖:",
|
||||
"sync__overwrite_tip_desc": "被覆盖者与覆盖者列表ID相同的列表将被删除后替换成覆盖者的列表(列表ID不同的列表将被合并到一起),若勾选完全覆盖,则被覆盖者的所有列表将被移除,然后替换成覆盖者的列表。",
|
||||
"sync__title": "选择与 {name} 的列表同步方式",
|
||||
"sync_status_disabled": "未连接",
|
||||
"tag__high_quality": "HQ",
|
||||
"tag__lossless": "SQ",
|
||||
"tag__lossless_24bit": "24bit",
|
||||
|
|
|
@ -455,13 +455,24 @@
|
|||
"setting__setting__desktop_lyric_font_weight_font": "逐字歌詞",
|
||||
"setting__setting__desktop_lyric_font_weight_line": "逐行歌詞",
|
||||
"setting__sync": "數據同步",
|
||||
"setting__sync_address": "同步服務地址:{address}",
|
||||
"setting__sync_auth_code": "連接碼:{code}",
|
||||
"setting__sync_device": "已連接的設備:{devices}",
|
||||
"setting__sync_enable": "啟用同步功能(由於數據是明文傳輸,請在受信任的網絡下使用)",
|
||||
"setting__sync_port": "同步端口設置",
|
||||
"setting__sync_port_tip": "請輸入同步服務端口號",
|
||||
"setting__sync_refresh_code": "刷新連接碼",
|
||||
"setting__sync_client_address": "當前設備地址:{address}",
|
||||
"setting__sync_client_host": "同步服務地址",
|
||||
"setting__sync_client_host_tip": "請輸入同步服務地址",
|
||||
"setting__sync_client_mode": "客戶端模式",
|
||||
"setting__sync_client_status": "狀態:{status}",
|
||||
"setting__sync_code_blocked_ip": "當前設備的IP已被服務端封禁!",
|
||||
"setting__sync_code_fail": "連接碼無效",
|
||||
"setting__sync_enable": "啟用同步功能",
|
||||
"setting__sync_mode": "同步模式",
|
||||
"setting__sync_mode_client": "客戶端模式",
|
||||
"setting__sync_mode_server": "服務端模式",
|
||||
"setting__sync_server_address": "同步服務地址:{address}",
|
||||
"setting__sync_server_auth_code": "連接碼:{code}",
|
||||
"setting__sync_server_device": "已連接的設備:{devices}",
|
||||
"setting__sync_server_mode": "服務端模式(由於數據是明文傳輸,請在受信任的網絡下使用)",
|
||||
"setting__sync_server_port": "同步端口設置",
|
||||
"setting__sync_server_port_tip": "請輸入同步服務端口號",
|
||||
"setting__sync_server_refresh_code": "刷新連接碼",
|
||||
"setting__sync_tip": "使用方式請看常見問題“同步功能”部分",
|
||||
"setting__update": "軟件更新",
|
||||
"setting__update_checking": "檢查更新中...",
|
||||
|
@ -476,6 +487,7 @@
|
|||
"setting__update_show_change_log": "更新版本後的首次啟動時顯示更新日誌",
|
||||
"setting__update_try_auto_update": "發現新版本時嘗試自動下載更新",
|
||||
"setting__update_unknown": "未知",
|
||||
"setting_sync_status_enabled": "已連接",
|
||||
"song_list": "歌單",
|
||||
"songlist__import_input_show_btn": "打開歌單",
|
||||
"songlist__import_input_tip": "輸入歌單鏈接或歌單ID",
|
||||
|
@ -502,6 +514,8 @@
|
|||
"source_tx": "企鵝音樂",
|
||||
"source_wy": "網易音樂",
|
||||
"source_xm": "蝦米音樂",
|
||||
"sync__auth_code_input_tip": "請輸入連接碼",
|
||||
"sync__auth_code_title": "需要輸入連接碼",
|
||||
"sync__merge_btn_local_remote": "本機列表 合併 遠程列表",
|
||||
"sync__merge_btn_remote_local": "遠程列表 合併 本機列表",
|
||||
"sync__merge_label": "合併",
|
||||
|
@ -519,6 +533,7 @@
|
|||
"sync__overwrite_tip": "覆蓋:",
|
||||
"sync__overwrite_tip_desc": "被覆蓋者與覆蓋者列表ID相同的列表將被刪除後替換成覆蓋者的列表(列表ID不同的列表將被合併到一起),若勾選完全覆蓋,則被覆蓋者的所有列表將被移除,然後替換成覆蓋者的列表。",
|
||||
"sync__title": "選擇與 {name} 的列表同步方式",
|
||||
"sync_status_disabled": "未連接",
|
||||
"tag__high_quality": "HQ",
|
||||
"tag__lossless": "SQ",
|
||||
"tag__lossless_24bit": "24bit",
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
import { request, generateRsaKey } from './utils'
|
||||
import { getSyncAuthKey, setSyncAuthKey } from '../data'
|
||||
import { SYNC_CODE } from '@common/constants'
|
||||
import log from '../log'
|
||||
import { aesDecrypt, aesEncrypt, getComputerName, rsaDecrypt } from '../utils'
|
||||
|
||||
|
||||
const hello = async(urlInfo: LX.Sync.Client.UrlInfo) => request(`${urlInfo.httpProtocol}//${urlInfo.hostPath}/hello`)
|
||||
.then(({ text }) => text == SYNC_CODE.helloMsg)
|
||||
.catch((err: any) => {
|
||||
log.error('[auth] hello', err.message)
|
||||
console.log(err)
|
||||
return false
|
||||
})
|
||||
|
||||
const getServerId = async(urlInfo: LX.Sync.Client.UrlInfo) => request(`${urlInfo.httpProtocol}//${urlInfo.hostPath}/id`)
|
||||
.then(({ text }) => {
|
||||
if (!text.startsWith(SYNC_CODE.idPrefix)) return ''
|
||||
return text.replace(SYNC_CODE.idPrefix, '')
|
||||
})
|
||||
.catch((err: any) => {
|
||||
log.error('[auth] getServerId', err.message)
|
||||
console.log(err)
|
||||
throw err
|
||||
})
|
||||
|
||||
const codeAuth = async(urlInfo: LX.Sync.Client.UrlInfo, serverId: string, authCode: string) => {
|
||||
let key = ''.padStart(16, Buffer.from(authCode).toString('hex'))
|
||||
// const iv = Buffer.from(key.split('').reverse().join('')).toString('base64')
|
||||
key = Buffer.from(key).toString('base64')
|
||||
let { publicKey, privateKey } = await generateRsaKey()
|
||||
publicKey = publicKey.replace(/\n/g, '')
|
||||
.replace('-----BEGIN PUBLIC KEY-----', '')
|
||||
.replace('-----END PUBLIC KEY-----', '')
|
||||
const msg = aesEncrypt(`${SYNC_CODE.authMsg}\n${publicKey}\n${getComputerName()}\nlx_music_desktop`, key)
|
||||
// console.log(msg, key)
|
||||
return request(`${urlInfo.httpProtocol}//${urlInfo.hostPath}/ah`, { headers: { m: msg } }).then(async({ text, code }) => {
|
||||
// console.log(text)
|
||||
switch (text) {
|
||||
case SYNC_CODE.msgBlockedIp:
|
||||
throw new Error(SYNC_CODE.msgBlockedIp)
|
||||
case SYNC_CODE.authFailed:
|
||||
throw new Error(SYNC_CODE.authFailed)
|
||||
default:
|
||||
if (code != 200) throw new Error(SYNC_CODE.authFailed)
|
||||
}
|
||||
let msg
|
||||
try {
|
||||
msg = rsaDecrypt(Buffer.from(text, 'base64'), privateKey).toString()
|
||||
} catch (err: any) {
|
||||
log.error('[auth] codeAuth decryptMsg error', err.message)
|
||||
throw new Error(SYNC_CODE.authFailed)
|
||||
}
|
||||
// console.log(msg)
|
||||
if (!msg) return Promise.reject(new Error(SYNC_CODE.authFailed))
|
||||
const info = JSON.parse(msg) as LX.Sync.ClientKeyInfo
|
||||
void setSyncAuthKey(serverId, info)
|
||||
return info
|
||||
})
|
||||
}
|
||||
|
||||
const keyAuth = async(urlInfo: LX.Sync.Client.UrlInfo, keyInfo: LX.Sync.ClientKeyInfo) => {
|
||||
const msg = aesEncrypt(SYNC_CODE.authMsg + getComputerName(), keyInfo.key)
|
||||
return request(`${urlInfo.httpProtocol}//${urlInfo.hostPath}/ah`, { headers: { i: keyInfo.clientId, m: msg } }).then(({ text, code }) => {
|
||||
if (code != 200) throw new Error(SYNC_CODE.authFailed)
|
||||
|
||||
let msg
|
||||
try {
|
||||
msg = aesDecrypt(text, keyInfo.key)
|
||||
} catch (err: any) {
|
||||
log.error('[auth] keyAuth decryptMsg error', err.message)
|
||||
throw new Error(SYNC_CODE.authFailed)
|
||||
}
|
||||
if (msg != SYNC_CODE.helloMsg) return Promise.reject(new Error(SYNC_CODE.authFailed))
|
||||
})
|
||||
}
|
||||
|
||||
const auth = async(urlInfo: LX.Sync.Client.UrlInfo, serverId: string, authCode?: string) => {
|
||||
if (authCode) return codeAuth(urlInfo, serverId, authCode)
|
||||
const keyInfo = await getSyncAuthKey(serverId)
|
||||
if (!keyInfo) throw new Error(SYNC_CODE.missingAuthCode)
|
||||
await keyAuth(urlInfo, keyInfo)
|
||||
return keyInfo
|
||||
}
|
||||
|
||||
export default async(urlInfo: LX.Sync.Client.UrlInfo, authCode?: string) => {
|
||||
console.log('connect: ', urlInfo.href, authCode)
|
||||
if (!await hello(urlInfo)) throw new Error(SYNC_CODE.connectServiceFailed)
|
||||
const serverId = await getServerId(urlInfo)
|
||||
if (!serverId) throw new Error(SYNC_CODE.getServiceIdFailed)
|
||||
return auth(urlInfo, serverId, authCode)
|
||||
}
|
|
@ -0,0 +1,207 @@
|
|||
import WebSocket from 'ws'
|
||||
import { encryptMsg, decryptMsg } from './utils'
|
||||
import * as modules from './modules'
|
||||
// import { action as commonAction } from '@/store/modules/common'
|
||||
// import { getStore } from '@/store'
|
||||
import registerSyncListHandler from './syncList'
|
||||
import log from '../log'
|
||||
import { SYNC_CLOSE_CODE, SYNC_CODE } from '@common/constants'
|
||||
import { dateFormat } from '@common/utils/common'
|
||||
import { aesEncrypt, getAddress } from '../utils'
|
||||
import { sendClientStatus } from '@main/modules/winMain'
|
||||
|
||||
let status: LX.Sync.ClientStatus = {
|
||||
status: false,
|
||||
message: '',
|
||||
address: [],
|
||||
}
|
||||
|
||||
export const sendSyncStatus = (newStatus: Omit<LX.Sync.ClientStatus, 'address'>) => {
|
||||
status.status = newStatus.status
|
||||
status.message = newStatus.message
|
||||
if (status.status) {
|
||||
status.address = getAddress()
|
||||
}
|
||||
sendClientStatus(status)
|
||||
}
|
||||
|
||||
export const sendSyncMessage = (message: string) => {
|
||||
status.message = message
|
||||
sendClientStatus(status)
|
||||
}
|
||||
|
||||
const handleConnection = (socket: LX.Sync.Client.Socket) => {
|
||||
for (const moduleInit of Object.values(modules)) {
|
||||
moduleInit(socket)
|
||||
}
|
||||
}
|
||||
|
||||
const heartbeatTools = {
|
||||
failedNum: 0,
|
||||
pingTimeout: null as NodeJS.Timeout | null,
|
||||
delayRetryTimeout: null as NodeJS.Timeout | null,
|
||||
handleOpen() {
|
||||
console.log('open')
|
||||
this.failedNum = 0
|
||||
this.heartbeat()
|
||||
},
|
||||
heartbeat() {
|
||||
if (this.pingTimeout) clearTimeout(this.pingTimeout)
|
||||
|
||||
// Use `WebSocket#terminate()`, which immediately destroys the connection,
|
||||
// instead of `WebSocket#close()`, which waits for the close timer.
|
||||
// Delay should be equal to the interval at which your server
|
||||
// sends out pings plus a conservative assumption of the latency.
|
||||
this.pingTimeout = setTimeout(() => {
|
||||
client?.terminate()
|
||||
}, 30000 + 1000)
|
||||
},
|
||||
reConnnect() {
|
||||
if (this.pingTimeout) {
|
||||
clearTimeout(this.pingTimeout)
|
||||
this.pingTimeout = null
|
||||
}
|
||||
// client = null
|
||||
if (!client) return
|
||||
|
||||
if (this.failedNum > 3) throw new Error('connect error')
|
||||
|
||||
this.delayRetryTimeout = setTimeout(() => {
|
||||
this.delayRetryTimeout = null
|
||||
if (!client) return
|
||||
console.log(dateFormat(new Date()), 'reconnnect...')
|
||||
connect(client.data.urlInfo, client.data.keyInfo)
|
||||
}, 2000)
|
||||
|
||||
this.failedNum++
|
||||
},
|
||||
clearTimeout() {
|
||||
if (this.delayRetryTimeout) {
|
||||
clearTimeout(this.delayRetryTimeout)
|
||||
this.delayRetryTimeout = null
|
||||
}
|
||||
if (this.pingTimeout) {
|
||||
clearTimeout(this.pingTimeout)
|
||||
this.pingTimeout = null
|
||||
}
|
||||
},
|
||||
connect(socket: LX.Sync.Client.Socket) {
|
||||
console.log('heartbeatTools connect')
|
||||
socket.on('open', () => {
|
||||
this.handleOpen()
|
||||
})
|
||||
socket.on('ping', () => {
|
||||
this.heartbeat()
|
||||
})
|
||||
socket.on('close', (code) => {
|
||||
console.log(code)
|
||||
switch (code) {
|
||||
case SYNC_CLOSE_CODE.normal:
|
||||
case SYNC_CLOSE_CODE.failed:
|
||||
return
|
||||
}
|
||||
this.reConnnect()
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
let client: LX.Sync.Client.Socket | null
|
||||
// let listSyncPromise: Promise<void>
|
||||
export const connect = (urlInfo: LX.Sync.Client.UrlInfo, keyInfo: LX.Sync.ClientKeyInfo) => {
|
||||
client = new WebSocket(`${urlInfo.wsProtocol}//${urlInfo.hostPath}?i=${encodeURIComponent(keyInfo.clientId)}&t=${encodeURIComponent(aesEncrypt(SYNC_CODE.msgConnect, keyInfo.key))}`, {
|
||||
}) as LX.Sync.Client.Socket
|
||||
client.data = {
|
||||
keyInfo,
|
||||
urlInfo,
|
||||
}
|
||||
heartbeatTools.connect(client)
|
||||
|
||||
// listSyncPromise = registerSyncListHandler(socket)
|
||||
let events: Partial<{ [K in keyof LX.Sync.ActionSyncSendType]: Array<(data: LX.Sync.ActionSyncSendType[K]) => (void | Promise<void>)> }> = {}
|
||||
let closeEvents: Array<(err: Error) => (void | Promise<void>)> = []
|
||||
client.addEventListener('message', ({ data }) => {
|
||||
if (data == 'ping') return
|
||||
if (typeof data === 'string') {
|
||||
let syncData: LX.Sync.ActionSync
|
||||
try {
|
||||
syncData = JSON.parse(decryptMsg(keyInfo, data))
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
const handlers = events[syncData.action]
|
||||
if (handlers) {
|
||||
// @ts-expect-error
|
||||
for (const handler of handlers) void handler(syncData.data)
|
||||
}
|
||||
}
|
||||
})
|
||||
client.onRemoteEvent = function(eventName, handler) {
|
||||
let eventArr = events[eventName]
|
||||
if (!eventArr) events[eventName] = eventArr = []
|
||||
// let eventArr = events.get(eventName)
|
||||
// if (!eventArr) events.set(eventName, eventArr = [])
|
||||
eventArr.push(handler)
|
||||
|
||||
return () => {
|
||||
eventArr!.splice(eventArr!.indexOf(handler), 1)
|
||||
}
|
||||
}
|
||||
client.sendData = function(eventName, data, callback) {
|
||||
client?.send(encryptMsg(keyInfo, JSON.stringify({ action: eventName, data })), callback)
|
||||
}
|
||||
client.onClose = function(handler: typeof closeEvents[number]) {
|
||||
closeEvents.push(handler)
|
||||
return () => {
|
||||
closeEvents.splice(closeEvents.indexOf(handler), 1)
|
||||
}
|
||||
}
|
||||
|
||||
client.addEventListener('open', () => {
|
||||
log.info('connect')
|
||||
// const store = getStore()
|
||||
// global.lx.syncKeyInfo = keyInfo
|
||||
client!.isReady = false
|
||||
sendSyncStatus({
|
||||
status: false,
|
||||
message: 'Wait syncing...',
|
||||
})
|
||||
void registerSyncListHandler(client as LX.Sync.Client.Socket).then(() => {
|
||||
log.info('sync list success')
|
||||
handleConnection(client as LX.Sync.Client.Socket)
|
||||
log.info('register list sync service success')
|
||||
client!.isReady = true
|
||||
sendSyncStatus({
|
||||
status: true,
|
||||
message: '',
|
||||
})
|
||||
}).catch(err => {
|
||||
console.log(err)
|
||||
log.r_error(err.stack)
|
||||
sendSyncStatus({
|
||||
status: false,
|
||||
message: err.message,
|
||||
})
|
||||
})
|
||||
})
|
||||
client.addEventListener('close', () => {
|
||||
sendSyncStatus({
|
||||
status: false,
|
||||
message: '',
|
||||
})
|
||||
const err = new Error('closed')
|
||||
for (const handler of closeEvents) void handler(err)
|
||||
closeEvents = []
|
||||
events = {}
|
||||
})
|
||||
}
|
||||
|
||||
export const disconnect = async() => {
|
||||
if (!client) return
|
||||
log.info('disconnecting...')
|
||||
client.close(SYNC_CLOSE_CODE.normal)
|
||||
client = null
|
||||
heartbeatTools.clearTimeout()
|
||||
}
|
||||
|
||||
export const getStatus = (): LX.Sync.ClientStatus => status
|
|
@ -0,0 +1,69 @@
|
|||
import handleAuth from './auth'
|
||||
import { connect as socketConnect, disconnect as socketDisconnect, sendSyncStatus, sendSyncMessage } from './client'
|
||||
// import { getSyncHost } from '@/utils/data'
|
||||
import { SYNC_CODE } from '@common/constants'
|
||||
import log from '../log'
|
||||
import { parseUrl } from './utils'
|
||||
|
||||
const handleConnect = async(host: string, authCode?: string) => {
|
||||
// const hostInfo = await getSyncHost()
|
||||
// console.log(hostInfo)
|
||||
// if (!hostInfo || !hostInfo.host || !hostInfo.port) throw new Error(SYNC_CODE.unknownServiceAddress)
|
||||
const urlInfo = parseUrl(host)
|
||||
await disconnectServer(false)
|
||||
const keyInfo = await handleAuth(urlInfo, authCode)
|
||||
socketConnect(urlInfo, keyInfo)
|
||||
}
|
||||
const handleDisconnect = async() => {
|
||||
await socketDisconnect()
|
||||
}
|
||||
|
||||
const connectServer = async(host: string, authCode?: string) => {
|
||||
sendSyncStatus({
|
||||
status: false,
|
||||
message: SYNC_CODE.connecting,
|
||||
})
|
||||
return handleConnect(host, authCode).then(() => {
|
||||
sendSyncStatus({
|
||||
status: true,
|
||||
message: '',
|
||||
})
|
||||
}).catch(async err => {
|
||||
sendSyncStatus({
|
||||
status: false,
|
||||
message: err.message,
|
||||
})
|
||||
switch (err.message) {
|
||||
case SYNC_CODE.connectServiceFailed:
|
||||
case SYNC_CODE.missingAuthCode:
|
||||
break
|
||||
default:
|
||||
log.r_warn(err.message)
|
||||
break
|
||||
}
|
||||
|
||||
return Promise.reject(err)
|
||||
})
|
||||
}
|
||||
|
||||
const disconnectServer = async(isResetStatus = true) => handleDisconnect().then(() => {
|
||||
log.info('disconnect...')
|
||||
if (isResetStatus) {
|
||||
sendSyncStatus({
|
||||
status: false,
|
||||
message: '',
|
||||
})
|
||||
}
|
||||
}).catch((err: any) => {
|
||||
log.error(`disconnect error: ${err.message as string}`)
|
||||
sendSyncMessage(err.message)
|
||||
})
|
||||
|
||||
export {
|
||||
connectServer,
|
||||
disconnectServer,
|
||||
}
|
||||
|
||||
export {
|
||||
getStatus,
|
||||
} from './client'
|
|
@ -0,0 +1 @@
|
|||
export { default as list } from './list'
|
|
@ -0,0 +1,7 @@
|
|||
import initOn from './on'
|
||||
import initSend from './send'
|
||||
|
||||
export default (socket: LX.Sync.Client.Socket) => {
|
||||
initOn(socket)
|
||||
initSend(socket)
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { handleRemoteListAction } from '@main/modules/sync/utils'
|
||||
|
||||
export default (socket: LX.Sync.Client.Socket) => {
|
||||
socket.onRemoteEvent('list:sync:action', (action) => {
|
||||
if (!socket.isReady) return
|
||||
void handleRemoteListAction(action)
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { registerListActionEvent } from '@main/modules/sync/utils'
|
||||
|
||||
let socket: LX.Sync.Client.Socket | null
|
||||
let unregisterLocalListAction: (() => void) | null
|
||||
|
||||
|
||||
const sendListAction = (action: LX.Sync.ActionList) => {
|
||||
// console.log('sendListAction', action.action)
|
||||
if (!socket?.isReady) return
|
||||
socket.sendData('list:sync:action', action)
|
||||
}
|
||||
|
||||
export default (_socket: LX.Sync.Client.Socket) => {
|
||||
socket = _socket
|
||||
socket.onClose(() => {
|
||||
socket = null
|
||||
unregisterLocalListAction?.()
|
||||
unregisterLocalListAction = null
|
||||
})
|
||||
unregisterLocalListAction = registerListActionEvent(sendListAction)
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
import log from '../log'
|
||||
import { getLocalListData, setLocalListData } from '../utils'
|
||||
import { toMD5 } from '@common/utils/nodejs'
|
||||
import { removeSelectModeListener, sendCloseSelectMode, sendSelectMode } from '@main/modules/winMain'
|
||||
import { SYNC_CLOSE_CODE } from '@common/constants'
|
||||
|
||||
const logInfo = (eventName: keyof LX.Sync.ActionSyncSendType, success = false) => {
|
||||
log.info(`[${eventName as string}]${eventName.replace('list:sync:list_sync_', '').replaceAll('_', ' ')}${success ? ' success' : ''}`)
|
||||
}
|
||||
const logError = (eventName: keyof LX.Sync.ActionSyncSendType, err: Error) => {
|
||||
log.error(`[${eventName as string}]${eventName.replace('list:sync:list_sync_', '').replaceAll('_', ' ')} error: ${err.message}`)
|
||||
}
|
||||
|
||||
export default async(socket: LX.Sync.Client.Socket) => new Promise<void>((resolve, reject) => {
|
||||
let listenEvents: Array<() => void> = []
|
||||
const unregisterEvents = () => {
|
||||
while (listenEvents.length) listenEvents.shift()?.()
|
||||
}
|
||||
|
||||
socket.onClose(() => {
|
||||
unregisterEvents()
|
||||
sendCloseSelectMode()
|
||||
removeSelectModeListener()
|
||||
reject(new Error('closed'))
|
||||
})
|
||||
listenEvents.push(socket.onRemoteEvent('list:sync:list_sync_get_md5', async() => {
|
||||
logInfo('list:sync:list_sync_get_md5')
|
||||
const md5 = toMD5(JSON.stringify(await getLocalListData()))
|
||||
socket?.sendData('list:sync:list_sync_get_md5', md5, (err) => {
|
||||
if (err) {
|
||||
logError('list:sync:list_sync_get_md5', err)
|
||||
socket.close(SYNC_CLOSE_CODE.failed)
|
||||
return
|
||||
}
|
||||
logInfo('list:sync:list_sync_get_md5', true)
|
||||
})
|
||||
}))
|
||||
listenEvents.push(socket.onRemoteEvent('list:sync:list_sync_get_list_data', async() => {
|
||||
logInfo('list:sync:list_sync_get_list_data')
|
||||
socket?.sendData('list:sync:list_sync_get_list_data', await getLocalListData(), (err) => {
|
||||
if (err) {
|
||||
logError('list:sync:list_sync_get_list_data', err)
|
||||
socket.close(SYNC_CLOSE_CODE.failed)
|
||||
return
|
||||
}
|
||||
logInfo('list:sync:list_sync_get_list_data', true)
|
||||
})
|
||||
}))
|
||||
listenEvents.push(socket.onRemoteEvent('list:sync:list_sync_get_sync_mode', async() => {
|
||||
logInfo('list:sync:list_sync_get_sync_mode')
|
||||
sendSelectMode(socket.data.keyInfo.serverName, (mode) => {
|
||||
removeSelectModeListener()
|
||||
socket?.sendData('list:sync:list_sync_get_sync_mode', mode, (err) => {
|
||||
if (err) {
|
||||
logError('list:sync:list_sync_get_sync_mode', err)
|
||||
socket.close(SYNC_CLOSE_CODE.failed)
|
||||
return
|
||||
}
|
||||
logInfo('list:sync:list_sync_get_sync_mode', true)
|
||||
})
|
||||
})
|
||||
}))
|
||||
listenEvents.push(socket.onRemoteEvent('list:sync:list_sync_set_data', async(data) => {
|
||||
logInfo('list:sync:list_sync_set_data')
|
||||
await setLocalListData(data)
|
||||
logInfo('list:sync:list_sync_set_data', true)
|
||||
}))
|
||||
listenEvents.push(socket.onRemoteEvent('list:sync:finished', async() => {
|
||||
unregisterEvents()
|
||||
resolve()
|
||||
logInfo('list:sync:finished', true)
|
||||
}))
|
||||
})
|
|
@ -0,0 +1,115 @@
|
|||
import { generateKeyPair } from 'node:crypto'
|
||||
import { httpFetch, type RequestOptions } from '@main/utils/request'
|
||||
|
||||
export const request = async(url: string, options: RequestOptions = { }) => {
|
||||
return httpFetch(url, {
|
||||
...options,
|
||||
timeout: options.timeout ?? 10000,
|
||||
}).then(response => {
|
||||
return {
|
||||
text: response.body,
|
||||
code: response.statusCode,
|
||||
}
|
||||
})
|
||||
// const controller = new AbortController()
|
||||
// let id: number | null = setTimeout(() => {
|
||||
// id = null
|
||||
// controller.abort()
|
||||
// }, timeout)
|
||||
// return fetch(url, {
|
||||
// ...options,
|
||||
// signal: controller.signal,
|
||||
// // eslint-disable-next-line @typescript-eslint/promise-function-async
|
||||
// }).then(async(response) => {
|
||||
// const text = await response.text()
|
||||
// return {
|
||||
// text,
|
||||
// code: response.status,
|
||||
// }
|
||||
// }).catch(err => {
|
||||
// // console.log(err, err.code, err.message)
|
||||
// throw err
|
||||
// }).finally(() => {
|
||||
// if (id == null) return
|
||||
// clearTimeout(id)
|
||||
// })
|
||||
}
|
||||
|
||||
// export const aesEncrypt = (text: string, key: string, iv: string) => {
|
||||
// const cipher = createCipheriv('aes-128-cbc', Buffer.from(key, 'base64'), Buffer.from(iv, 'base64'))
|
||||
// return Buffer.concat([cipher.update(Buffer.from(text)), cipher.final()]).toString('base64')
|
||||
// }
|
||||
|
||||
// export const aesDecrypt = (text: string, key: string, iv: string) => {
|
||||
// const decipher = createDecipheriv('aes-128-cbc', Buffer.from(key, 'base64'), Buffer.from(iv, 'base64'))
|
||||
// return Buffer.concat([decipher.update(Buffer.from(text, 'base64')), decipher.final()]).toString()
|
||||
// }
|
||||
|
||||
export const generateRsaKey = async() => new Promise<{ publicKey: string, privateKey: string }>((resolve, reject) => {
|
||||
generateKeyPair(
|
||||
'rsa',
|
||||
{
|
||||
modulusLength: 2048, // It holds a number. It is the key size in bits and is applicable for RSA, and DSA algorithm only.
|
||||
publicKeyEncoding: {
|
||||
type: 'spki', // Note the type is pkcs1 not spki
|
||||
format: 'pem',
|
||||
},
|
||||
privateKeyEncoding: {
|
||||
type: 'pkcs8', // Note again the type is set to pkcs1
|
||||
format: 'pem',
|
||||
// cipher: "aes-256-cbc", //Optional
|
||||
// passphrase: "", //Optional
|
||||
},
|
||||
},
|
||||
(err, publicKey, privateKey) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
resolve({
|
||||
publicKey,
|
||||
privateKey,
|
||||
})
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
export const encryptMsg = (keyInfo: LX.Sync.ClientKeyInfo, msg: string): string => {
|
||||
return msg
|
||||
// if (!keyInfo) return ''
|
||||
// return aesEncrypt(msg, keyInfo.key, keyInfo.iv)
|
||||
}
|
||||
|
||||
export const decryptMsg = (keyInfo: LX.Sync.ClientKeyInfo, enMsg: string): string => {
|
||||
return enMsg
|
||||
// if (!keyInfo) return ''
|
||||
// let msg = ''
|
||||
// try {
|
||||
// msg = aesDecrypt(enMsg, keyInfo.key, keyInfo.iv)
|
||||
// } catch (err) {
|
||||
// console.log(err)
|
||||
// }
|
||||
// return msg
|
||||
}
|
||||
|
||||
|
||||
export const parseUrl = (host: string): LX.Sync.Client.UrlInfo => {
|
||||
const url = new URL(host)
|
||||
let hostPath = url.host + url.pathname
|
||||
let href = url.href
|
||||
if (hostPath.endsWith('/')) hostPath = hostPath.replace(/\/$/, '')
|
||||
if (href.endsWith('/')) href = href.replace(/\/$/, '')
|
||||
|
||||
return {
|
||||
wsProtocol: url.protocol == 'https:' ? 'wss:' : 'ws:',
|
||||
httpProtocol: url.protocol,
|
||||
hostPath,
|
||||
href,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const sendStatus = (status: LX.Sync.ClientStatus) => {
|
||||
// syncLog.log(JSON.stringify(status))
|
||||
}
|
|
@ -0,0 +1,180 @@
|
|||
import { randomBytes } from 'node:crypto'
|
||||
import { STORE_NAMES } from '@common/constants'
|
||||
import getStore from '@main/utils/store'
|
||||
import { throttle } from '@common/utils/common'
|
||||
import path from 'node:path'
|
||||
import fs from 'node:fs'
|
||||
import log from './log'
|
||||
|
||||
|
||||
export const getSyncAuthKey = async(serverId: string) => {
|
||||
const store = getStore(STORE_NAMES.SYNC)
|
||||
const keys = store.get('syncAuthKey') as Record<string, LX.Sync.ClientKeyInfo> | null
|
||||
if (!keys) return null
|
||||
return keys[serverId] ?? null
|
||||
}
|
||||
export const setSyncAuthKey = async(serverId: string, info: LX.Sync.ClientKeyInfo) => {
|
||||
const store = getStore(STORE_NAMES.SYNC)
|
||||
let keys: Record<string, LX.Sync.ClientKeyInfo> = (store.get('syncAuthKey') as Record<string, LX.Sync.ClientKeyInfo> | null) ?? {}
|
||||
keys[serverId] = info
|
||||
store.set('syncAuthKey', keys)
|
||||
}
|
||||
|
||||
let syncHost: string
|
||||
export const getSyncHost = async() => {
|
||||
if (syncHost === undefined) {
|
||||
const store = getStore(STORE_NAMES.SYNC)
|
||||
syncHost = (store.get('syncHost') as typeof syncHost | null) ?? ''
|
||||
}
|
||||
return syncHost
|
||||
}
|
||||
export const setSyncHost = async(host: string) => {
|
||||
// let hostInfo = await getData(syncHostPrefix) || {}
|
||||
// hostInfo.host = host
|
||||
// hostInfo.port = port
|
||||
syncHost = host
|
||||
const store = getStore(STORE_NAMES.SYNC)
|
||||
store.set('syncHost', syncHost)
|
||||
}
|
||||
let syncHostHistory: string[]
|
||||
export const getSyncHostHistory = async() => {
|
||||
if (syncHostHistory === undefined) {
|
||||
const store = getStore(STORE_NAMES.SYNC)
|
||||
syncHostHistory = (store.get('syncHostHistory') as string[]) ?? []
|
||||
}
|
||||
return syncHostHistory
|
||||
}
|
||||
export const addSyncHostHistory = async(host: string) => {
|
||||
let syncHostHistory = await getSyncHostHistory()
|
||||
if (syncHostHistory.some(h => h == host)) return
|
||||
syncHostHistory.unshift(host)
|
||||
if (syncHostHistory.length > 20) syncHostHistory = syncHostHistory.slice(0, 20) // 最多存储20个
|
||||
const store = getStore(STORE_NAMES.SYNC)
|
||||
store.set('syncHostHistory', syncHostHistory)
|
||||
}
|
||||
export const removeSyncHostHistory = async(index: number) => {
|
||||
syncHostHistory.splice(index, 1)
|
||||
const store = getStore(STORE_NAMES.SYNC)
|
||||
store.set('syncHostHistory', syncHostHistory)
|
||||
}
|
||||
|
||||
export interface SnapshotInfo {
|
||||
latest: string | null
|
||||
time: number
|
||||
list: string[]
|
||||
}
|
||||
interface DevicesInfo {
|
||||
serverId: string
|
||||
clients: Record<string, LX.Sync.ServerKeyInfo>
|
||||
snapshotInfo: SnapshotInfo
|
||||
}
|
||||
// const devicesFilePath = path.join(global.lx.dataPath, 'devices.json')
|
||||
const devicesInfo: DevicesInfo = { serverId: '', clients: {}, snapshotInfo: { latest: null, time: 0, list: [] } }
|
||||
let deviceKeys: string[] = []
|
||||
const saveDevicesInfoThrottle = throttle(() => {
|
||||
const store = getStore(STORE_NAMES.SYNC)
|
||||
store.set('keys', devicesInfo.clients)
|
||||
})
|
||||
|
||||
const initDeviceInfo = () => {
|
||||
const store = getStore(STORE_NAMES.SYNC)
|
||||
const serverId = store.get('serverId') as string | undefined
|
||||
if (serverId) devicesInfo.serverId = serverId
|
||||
else {
|
||||
devicesInfo.serverId = randomBytes(4 * 4).toString('base64')
|
||||
saveDevicesInfoThrottle()
|
||||
}
|
||||
const devices = store.get('clients') as DevicesInfo['clients'] | undefined
|
||||
if (devices) devicesInfo.clients = devices
|
||||
deviceKeys = Object.values(devicesInfo.clients).map(device => device.key).filter(k => k)
|
||||
const snapshotInfo = store.get('snapshotInfo') as DevicesInfo['snapshotInfo'] | undefined
|
||||
if (snapshotInfo) devicesInfo.snapshotInfo = snapshotInfo
|
||||
}
|
||||
|
||||
export const createClientKeyInfo = (deviceName: string, isMobile: boolean): LX.Sync.ServerKeyInfo => {
|
||||
const keyInfo: LX.Sync.ServerKeyInfo = {
|
||||
clientId: randomBytes(4 * 4).toString('base64'),
|
||||
key: randomBytes(16).toString('base64'),
|
||||
deviceName,
|
||||
isMobile,
|
||||
snapshotKey: '',
|
||||
lastSyncDate: 0,
|
||||
}
|
||||
saveClientKeyInfo(keyInfo)
|
||||
return keyInfo
|
||||
}
|
||||
export const saveClientKeyInfo = (keyInfo: LX.Sync.ServerKeyInfo) => {
|
||||
if (devicesInfo.clients[keyInfo.clientId] == null && Object.keys(devicesInfo.clients).length > 101) throw new Error('max keys')
|
||||
devicesInfo.clients[keyInfo.clientId] = keyInfo
|
||||
saveDevicesInfoThrottle()
|
||||
}
|
||||
export const getClientKeyInfo = (clientId?: string): LX.Sync.ServerKeyInfo | null => {
|
||||
if (!clientId) return null
|
||||
return devicesInfo.clients[clientId] ?? null
|
||||
}
|
||||
export const getServerId = (): string => {
|
||||
if (!devicesInfo.serverId) initDeviceInfo()
|
||||
return devicesInfo.serverId
|
||||
}
|
||||
export const isIncluedsDevice = (name: string) => {
|
||||
return deviceKeys.includes(name)
|
||||
}
|
||||
export const clearOldSnapshot = async() => {
|
||||
if (!devicesInfo.snapshotInfo) return
|
||||
const snapshotList = devicesInfo.snapshotInfo.list.filter(name => !isIncluedsDevice(name))
|
||||
let requiredSave = snapshotList.length > global.lx.appSetting['sync.server.maxSsnapshotNum']
|
||||
while (snapshotList.length > global.lx.appSetting['sync.server.maxSsnapshotNum']) {
|
||||
const name = snapshotList.pop()
|
||||
if (name) {
|
||||
await removeSnapshot(name)
|
||||
devicesInfo.snapshotInfo.list.splice(devicesInfo.snapshotInfo.list.indexOf(name), 1)
|
||||
} else break
|
||||
}
|
||||
if (requiredSave) saveSnapshotInfo(devicesInfo.snapshotInfo)
|
||||
}
|
||||
export const updateDeviceSnapshotKey = (keyInfo: LX.Sync.ServerKeyInfo, key: string) => {
|
||||
if (keyInfo.snapshotKey) deviceKeys.splice(deviceKeys.indexOf(keyInfo.snapshotKey), 1)
|
||||
keyInfo.snapshotKey = key
|
||||
keyInfo.lastSyncDate = Date.now()
|
||||
saveClientKeyInfo(keyInfo)
|
||||
deviceKeys.push(key)
|
||||
saveDevicesInfoThrottle()
|
||||
void clearOldSnapshot()
|
||||
}
|
||||
|
||||
const saveSnapshotInfoThrottle = throttle(() => {
|
||||
const store = getStore(STORE_NAMES.SYNC)
|
||||
store.set('snapshotInfo', devicesInfo.snapshotInfo)
|
||||
})
|
||||
export const getSnapshotInfo = (): SnapshotInfo => {
|
||||
return devicesInfo.snapshotInfo
|
||||
}
|
||||
export const saveSnapshotInfo = (info: SnapshotInfo) => {
|
||||
devicesInfo.snapshotInfo = info
|
||||
saveSnapshotInfoThrottle()
|
||||
}
|
||||
|
||||
export const getSnapshot = async(name: string) => {
|
||||
const filePath = path.join(global.lxDataPath, `snapshot_${name}`)
|
||||
let listData: LX.Sync.ListData
|
||||
try {
|
||||
listData = JSON.parse((await fs.promises.readFile(filePath)).toString('utf-8'))
|
||||
} catch (err) {
|
||||
log.warn(err)
|
||||
return null
|
||||
}
|
||||
return listData
|
||||
}
|
||||
export const saveSnapshot = async(name: string, data: string) => {
|
||||
const filePath = path.join(global.lxDataPath, `snapshot_${name}`)
|
||||
return fs.promises.writeFile(filePath, data).catch((err) => {
|
||||
log.error(err)
|
||||
throw err
|
||||
})
|
||||
}
|
||||
export const removeSnapshot = async(name: string) => {
|
||||
const filePath = path.join(global.lxDataPath, `snapshot_${name}`)
|
||||
return fs.promises.unlink(filePath).catch((err) => {
|
||||
log.error(err)
|
||||
})
|
||||
}
|
|
@ -1,21 +1,28 @@
|
|||
// import Event from './event/event'
|
||||
|
||||
import { disconnectServer } from './client'
|
||||
import { stopServer } from './server'
|
||||
|
||||
// import eventNames from './event/name'
|
||||
import * as modules from './modules'
|
||||
import { startServer, stopServer, getStatus, generateCode } from './server/server'
|
||||
|
||||
|
||||
export {
|
||||
startServer,
|
||||
stopServer,
|
||||
getStatus,
|
||||
getStatus as getServerStatus,
|
||||
generateCode,
|
||||
// Event,
|
||||
// eventNames,
|
||||
modules,
|
||||
}
|
||||
} from './server'
|
||||
|
||||
export {
|
||||
connectServer,
|
||||
disconnectServer,
|
||||
getStatus as getClientStatus,
|
||||
} from './client'
|
||||
|
||||
export default () => {
|
||||
global.lx.event_app.on('main_window_close', () => {
|
||||
void stopServer()
|
||||
if (global.lx.appSetting['sync.mode'] == 'server') {
|
||||
void stopServer()
|
||||
} else {
|
||||
void disconnectServer()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
import { log as writeLog } from '@common/utils'
|
||||
|
||||
export default {
|
||||
r_info(...params: any[]) {
|
||||
writeLog.info(...params)
|
||||
},
|
||||
r_warn(...params: any[]) {
|
||||
writeLog.warn(...params)
|
||||
},
|
||||
r_error(...params: any[]) {
|
||||
writeLog.error(...params)
|
||||
},
|
||||
info(...params: any[]) {
|
||||
// if (global.lx.isEnableSyncLog) writeLog.info(...params)
|
||||
console.log(...params)
|
||||
},
|
||||
warn(...params: any[]) {
|
||||
// if (global.lx.isEnableSyncLog) writeLog.warn(...params)
|
||||
console.warn(...params)
|
||||
},
|
||||
error(...params: any[]) {
|
||||
// if (global.lx.isEnableSyncLog) writeLog.error(...params)
|
||||
console.warn(...params)
|
||||
},
|
||||
}
|
|
@ -1,22 +1,17 @@
|
|||
import type http from 'http'
|
||||
import { SYNC_CODE } from './config'
|
||||
import {
|
||||
aesEncrypt,
|
||||
aesDecrypt,
|
||||
createClientKeyInfo,
|
||||
getClientKeyInfo,
|
||||
setClientKeyInfo,
|
||||
rsaEncrypt,
|
||||
} from './utils'
|
||||
import { SYNC_CODE } from '@common/constants'
|
||||
import querystring from 'node:querystring'
|
||||
import { getIP } from './utils'
|
||||
import { createClientKeyInfo, getClientKeyInfo, saveClientKeyInfo } from '../data'
|
||||
import { aesDecrypt, aesEncrypt, getComputerName, rsaEncrypt } from '../utils'
|
||||
|
||||
const requestIps = new Map<string, number>()
|
||||
|
||||
export const authCode = async(req: http.IncomingMessage, res: http.ServerResponse, authCode: string) => {
|
||||
export const authCode = async(req: http.IncomingMessage, res: http.ServerResponse, password: string) => {
|
||||
let code = 401
|
||||
let msg: string = SYNC_CODE.msgAuthFailed
|
||||
|
||||
let ip = req.socket.remoteAddress
|
||||
let ip = getIP(req)
|
||||
// console.log(req.headers)
|
||||
if (typeof req.headers.m == 'string') {
|
||||
if (ip && (requestIps.get(ip) ?? 0) < 10) {
|
||||
|
@ -38,12 +33,12 @@ export const authCode = async(req: http.IncomingMessage, res: http.ServerRespons
|
|||
const deviceName = text.replace(SYNC_CODE.authMsg, '') || 'Unknown'
|
||||
if (deviceName != keyInfo.deviceName) {
|
||||
keyInfo.deviceName = deviceName
|
||||
setClientKeyInfo(keyInfo)
|
||||
saveClientKeyInfo(keyInfo)
|
||||
}
|
||||
msg = aesEncrypt(SYNC_CODE.helloMsg, keyInfo.key)
|
||||
}
|
||||
} else { // 连接码验证
|
||||
let key = ''.padStart(16, Buffer.from(authCode).toString('hex'))
|
||||
let key = ''.padStart(16, Buffer.from(password).toString('hex'))
|
||||
// const iv = Buffer.from(key.split('').reverse().join('')).toString('base64')
|
||||
key = Buffer.from(key).toString('base64')
|
||||
// console.log(req.headers.m, authCode, key)
|
||||
|
@ -59,7 +54,13 @@ export const authCode = async(req: http.IncomingMessage, res: http.ServerRespons
|
|||
const data = text.split('\n')
|
||||
const publicKey = `-----BEGIN PUBLIC KEY-----\n${data[1]}\n-----END PUBLIC KEY-----`
|
||||
const deviceName = data[2] || 'Unknown'
|
||||
msg = rsaEncrypt(Buffer.from(JSON.stringify(createClientKeyInfo(deviceName))), publicKey)
|
||||
const isMobile = data[3] == 'lx_music_mobile'
|
||||
const keyInfo = createClientKeyInfo(deviceName, isMobile)
|
||||
msg = rsaEncrypt(Buffer.from(JSON.stringify({
|
||||
clientId: keyInfo.clientId,
|
||||
key: keyInfo.key,
|
||||
serverName: getComputerName(),
|
||||
})), publicKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
export const SYNC_CODE = {
|
||||
helloMsg: 'Hello~::^-^::~v2~',
|
||||
idPrefix: 'OjppZDo6',
|
||||
authMsg: 'lx-music auth::',
|
||||
msgAuthFailed: 'Auth failed',
|
||||
msgBlockedIp: 'Blocked IP',
|
||||
msgConnect: 'lx-music connect',
|
||||
} as const
|
|
@ -1,7 +1,16 @@
|
|||
import { startServer, stopServer, getStatus } from './server'
|
||||
import * as modules from './modules'
|
||||
import {
|
||||
startServer,
|
||||
stopServer,
|
||||
getStatus,
|
||||
generateCode,
|
||||
} from './server'
|
||||
|
||||
|
||||
export {
|
||||
startServer,
|
||||
stopServer,
|
||||
getStatus,
|
||||
generateCode,
|
||||
modules,
|
||||
}
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
// import { throttle } from '@common/utils/common'
|
||||
// import { sendSyncActionList } from '@main/modules/winMain'
|
||||
import { SYNC_CLOSE_CODE } from '@common/constants'
|
||||
import { updateDeviceSnapshotKey } from '../../data'
|
||||
import { handleRemoteListAction } from '../../utils'
|
||||
import { createSnapshot, encryptMsg } from '../utils'
|
||||
|
||||
let wss: LX.Sync.Server.SocketServer | null
|
||||
let removeListener: (() => void) | null
|
||||
|
||||
// type listAction = 'list:action'
|
||||
|
||||
// const addMusic = (orderId, callback) => {
|
||||
// // ...
|
||||
// }
|
||||
|
||||
const broadcast = async(key: string, data: any, excludeIds: string[] = []) => {
|
||||
if (!wss) return
|
||||
const dataStr = JSON.stringify({ action: 'list:sync:action', data })
|
||||
for (const socket of wss.clients) {
|
||||
if (excludeIds.includes(socket.keyInfo.clientId) || !socket.isReady) continue
|
||||
socket.send(encryptMsg(socket.keyInfo, dataStr), (err) => {
|
||||
if (err) {
|
||||
socket.close(SYNC_CLOSE_CODE.failed)
|
||||
return
|
||||
}
|
||||
updateDeviceSnapshotKey(socket.keyInfo, key)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const sendListAction = async(action: LX.Sync.ActionList) => {
|
||||
console.log('sendListAction', action.action)
|
||||
// io.sockets
|
||||
await broadcast('list:sync:action', action)
|
||||
}
|
||||
|
||||
export const registerListHandler = (_wss: LX.Sync.Server.SocketServer, socket: LX.Sync.Server.Socket) => {
|
||||
if (!wss) {
|
||||
wss = _wss
|
||||
// removeListener = registerListActionEvent()
|
||||
}
|
||||
|
||||
socket.onRemoteEvent('list:sync:action', (action) => {
|
||||
if (!socket.isReady) return
|
||||
// console.log(msg)
|
||||
void handleRemoteListAction(action).then(updated => {
|
||||
if (!updated) return
|
||||
void createSnapshot().then(key => {
|
||||
if (!key) return
|
||||
updateDeviceSnapshotKey(socket.keyInfo, key)
|
||||
void broadcast(key, action, [socket.keyInfo.clientId])
|
||||
})
|
||||
})
|
||||
// socket.broadcast.emit('list:action', { action: 'list_remove', data: { id: 'default', index: 0 } })
|
||||
})
|
||||
|
||||
// socket.on('list:add', addMusic)
|
||||
}
|
||||
export const unregisterListHandler = () => {
|
||||
wss = null
|
||||
|
||||
if (removeListener) {
|
||||
removeListener()
|
||||
removeListener = null
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,18 @@
|
|||
import http from 'http'
|
||||
import { Server, type Socket } from 'socket.io'
|
||||
import { createHttpTerminator, type HttpTerminator } from 'http-terminator'
|
||||
import * as modules from '../modules'
|
||||
import http, { type IncomingMessage } from 'node:http'
|
||||
import url from 'node:url'
|
||||
import { WebSocketServer } from 'ws'
|
||||
import * as modules from './modules'
|
||||
import { authCode, authConnect } from './auth'
|
||||
import { getAddress, getServerId, generateCode as handleGenerateCode, getClientKeyInfo, setClientKeyInfo } from './utils'
|
||||
import { syncList, removeSnapshot } from './syncList'
|
||||
import { log } from '@common/utils'
|
||||
import { sendStatus } from '@main/modules/winMain'
|
||||
import { SYNC_CODE } from './config'
|
||||
import syncList from './syncList'
|
||||
import log from '../log'
|
||||
import { SYNC_CLOSE_CODE, SYNC_CODE } from '@common/constants'
|
||||
import { decryptMsg, encryptMsg, generateCode as handleGenerateCode } from './utils'
|
||||
import { getAddress } from '../utils'
|
||||
import { sendServerStatus } from '@main/modules/winMain/index'
|
||||
import { getClientKeyInfo, getServerId, saveClientKeyInfo } from '../data'
|
||||
|
||||
|
||||
let status: LX.Sync.Status = {
|
||||
let status: LX.Sync.ServerStatus = {
|
||||
status: false,
|
||||
message: '',
|
||||
address: [],
|
||||
|
@ -27,7 +29,7 @@ const codeTools: {
|
|||
start() {
|
||||
this.stop()
|
||||
this.timeout = setInterval(() => {
|
||||
void generateCode()
|
||||
void handleGenerateCode()
|
||||
}, 60 * 3 * 1000)
|
||||
},
|
||||
stop() {
|
||||
|
@ -37,12 +39,45 @@ const codeTools: {
|
|||
},
|
||||
}
|
||||
|
||||
const handleConnection = (io: Server, socket: LX.Sync.Socket) => {
|
||||
console.log('connection')
|
||||
const handleConnection = async(socket: LX.Sync.Server.Socket, request: IncomingMessage) => {
|
||||
const queryData = url.parse(request.url as string, true).query as Record<string, string>
|
||||
|
||||
socket.onClose(() => {
|
||||
// console.log('disconnect', reason)
|
||||
status.devices.splice(status.devices.findIndex(k => k.clientId == keyInfo?.clientId), 1)
|
||||
sendServerStatus(status)
|
||||
})
|
||||
|
||||
|
||||
// // if (typeof socket.handshake.query.i != 'string') return socket.disconnect(true)
|
||||
const keyInfo = getClientKeyInfo(queryData.i)
|
||||
if (!keyInfo) {
|
||||
socket.close(SYNC_CLOSE_CODE.failed)
|
||||
return
|
||||
}
|
||||
keyInfo.lastSyncDate = Date.now()
|
||||
saveClientKeyInfo(keyInfo)
|
||||
// // socket.lx_keyInfo = keyInfo
|
||||
socket.keyInfo = keyInfo
|
||||
try {
|
||||
await syncList(wss as LX.Sync.Server.SocketServer, socket)
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
log.warn(err)
|
||||
return
|
||||
}
|
||||
status.devices.push(keyInfo)
|
||||
// handleConnection(io, socket)
|
||||
sendServerStatus(status)
|
||||
|
||||
// console.log('connection', keyInfo.deviceName)
|
||||
log.info('connection', keyInfo.deviceName)
|
||||
// console.log(socket.handshake.query)
|
||||
for (const module of Object.values(modules)) {
|
||||
module.registerListHandler(io, socket)
|
||||
module.registerListHandler(wss as LX.Sync.Server.SocketServer, socket)
|
||||
}
|
||||
|
||||
socket.isReady = true
|
||||
}
|
||||
|
||||
const handleUnconnection = () => {
|
||||
|
@ -64,11 +99,16 @@ const authConnection = (req: http.IncomingMessage, callback: (err: string | null
|
|||
})
|
||||
}
|
||||
|
||||
let httpTerminator: HttpTerminator | null = null
|
||||
let io: Server | null = null
|
||||
let wss: LX.Sync.Server.SocketServer | null
|
||||
let httpServer: http.Server
|
||||
|
||||
const handleStartServer = async(port = 9527) => await new Promise((resolve, reject) => {
|
||||
const httpServer = http.createServer((req, res) => {
|
||||
function noop() {}
|
||||
function onSocketError(err: Error) {
|
||||
console.error(err)
|
||||
}
|
||||
|
||||
const handleStartServer = async(port = 9527, ip = '0.0.0.0') => await new Promise((resolve, reject) => {
|
||||
httpServer = http.createServer((req, res) => {
|
||||
// console.log(req.url)
|
||||
let code
|
||||
let msg
|
||||
|
@ -93,46 +133,96 @@ const handleStartServer = async(port = 9527) => await new Promise((resolve, reje
|
|||
res.writeHead(code)
|
||||
res.end(msg)
|
||||
})
|
||||
httpTerminator = createHttpTerminator({
|
||||
server: httpServer,
|
||||
})
|
||||
io = new Server(httpServer, {
|
||||
path: '/sync',
|
||||
serveClient: false,
|
||||
connectTimeout: 10000,
|
||||
pingTimeout: 30000,
|
||||
maxHttpBufferSize: 1e9, // 1G
|
||||
allowRequest: authConnection,
|
||||
transports: ['websocket'],
|
||||
|
||||
wss = new WebSocketServer({
|
||||
noServer: true,
|
||||
})
|
||||
|
||||
io.on('connection', async(_socket: Socket) => {
|
||||
const socket = _socket as LX.Sync.Socket
|
||||
socket.on('disconnect', reason => {
|
||||
console.log('disconnect', reason)
|
||||
status.devices.splice(status.devices.findIndex(k => k.clientId == keyInfo?.clientId), 1)
|
||||
sendStatus(status)
|
||||
if (!status.devices.length) handleUnconnection()
|
||||
wss.on('connection', function(socket, request) {
|
||||
socket.isReady = false
|
||||
socket.on('pong', () => {
|
||||
socket.isAlive = true
|
||||
})
|
||||
if (typeof socket.handshake.query.i != 'string') return socket.disconnect(true)
|
||||
const keyInfo = getClientKeyInfo(socket.handshake.query.i)
|
||||
if (!keyInfo || !io) return socket.disconnect(true)
|
||||
keyInfo.connectionTime = Date.now()
|
||||
setClientKeyInfo(keyInfo)
|
||||
// socket.lx_keyInfo = keyInfo
|
||||
socket.data.keyInfo = keyInfo
|
||||
socket.data.isReady = false
|
||||
try {
|
||||
await syncList(io, socket)
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
log.warn(err)
|
||||
return
|
||||
|
||||
// const events = new Map<keyof ActionsType, Array<(err: Error | null, data: LX.Sync.ActionSyncType[keyof LX.Sync.ActionSyncType]) => void>>()
|
||||
// const events = new Map<keyof LX.Sync.ActionSyncType, Array<(err: Error | null, data: LX.Sync.ActionSyncType[keyof LX.Sync.ActionSyncType]) => void>>()
|
||||
let events: Partial<{ [K in keyof LX.Sync.ActionSyncType]: Array<(data: LX.Sync.ActionSyncType[K]) => void> }> = {}
|
||||
socket.addEventListener('message', ({ data }) => {
|
||||
if (typeof data === 'string') {
|
||||
let syncData: LX.Sync.ActionSync
|
||||
try {
|
||||
syncData = JSON.parse(decryptMsg(socket.keyInfo, data))
|
||||
} catch (err) {
|
||||
log.error('parse message error:', err)
|
||||
socket.close(SYNC_CLOSE_CODE.failed)
|
||||
return
|
||||
}
|
||||
const handlers = events[syncData.action]
|
||||
if (handlers) {
|
||||
// @ts-expect-error
|
||||
for (const handler of handlers) handler(syncData.data)
|
||||
}
|
||||
}
|
||||
})
|
||||
socket.addEventListener('close', () => {
|
||||
const err = new Error('closed')
|
||||
for (const handler of Object.values(events).flat()) {
|
||||
// @ts-expect-error
|
||||
handler(err, null)
|
||||
}
|
||||
events = {}
|
||||
if (!status.devices.length) handleUnconnection()
|
||||
log.info('deconnection', socket.keyInfo.deviceName)
|
||||
})
|
||||
socket.onRemoteEvent = function(eventName, handler) {
|
||||
let eventArr = events[eventName]
|
||||
if (!eventArr) events[eventName] = eventArr = []
|
||||
// let eventArr = events.get(eventName)
|
||||
// if (!eventArr) events.set(eventName, eventArr = [])
|
||||
eventArr.push(handler)
|
||||
|
||||
return () => {
|
||||
eventArr!.splice(eventArr!.indexOf(handler), 1)
|
||||
}
|
||||
}
|
||||
status.devices.push(keyInfo)
|
||||
handleConnection(io, socket)
|
||||
socket.data.isReady = true
|
||||
sendStatus(status)
|
||||
socket.sendData = function(eventName, data, callback) {
|
||||
socket.send(encryptMsg(socket.keyInfo, JSON.stringify({ action: eventName, data })), callback)
|
||||
}
|
||||
void handleConnection(socket, request)
|
||||
})
|
||||
|
||||
httpServer.on('upgrade', function upgrade(request, socket, head) {
|
||||
socket.on('error', onSocketError)
|
||||
// This function is not defined on purpose. Implement it with your own logic.
|
||||
authConnection(request, err => {
|
||||
if (err) {
|
||||
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n')
|
||||
socket.destroy()
|
||||
return
|
||||
}
|
||||
socket.removeListener('error', onSocketError)
|
||||
|
||||
wss?.handleUpgrade(request, socket, head, function done(ws) {
|
||||
wss?.emit('connection', ws, request)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const interval = setInterval(() => {
|
||||
wss?.clients.forEach(ws => {
|
||||
if (ws.isAlive == false) {
|
||||
ws.terminate()
|
||||
return
|
||||
}
|
||||
|
||||
ws.isAlive = false
|
||||
ws.ping(noop)
|
||||
if (ws.keyInfo.isMobile) ws.send('ping', noop)
|
||||
})
|
||||
}, 30000)
|
||||
|
||||
wss.on('close', function close() {
|
||||
clearInterval(interval)
|
||||
})
|
||||
|
||||
httpServer.on('error', error => {
|
||||
|
@ -142,35 +232,41 @@ const handleStartServer = async(port = 9527) => await new Promise((resolve, reje
|
|||
|
||||
httpServer.on('listening', () => {
|
||||
const addr = httpServer.address()
|
||||
// console.log(addr)
|
||||
if (!addr) {
|
||||
reject(new Error('address is null'))
|
||||
return
|
||||
}
|
||||
const bind = typeof addr == 'string' ? `pipe ${addr}` : `port ${addr.port}`
|
||||
console.info(`Listening on ${bind}`)
|
||||
log.info(`Listening on ${ip} ${bind}`)
|
||||
resolve(null)
|
||||
})
|
||||
|
||||
httpServer.listen(port)
|
||||
httpServer.listen(port, ip)
|
||||
})
|
||||
|
||||
const handleStopServer = async() => {
|
||||
if (!httpTerminator) return
|
||||
if (!io) return
|
||||
io.close()
|
||||
await httpTerminator.terminate().catch(() => {})
|
||||
io = null
|
||||
httpTerminator = null
|
||||
}
|
||||
const handleStopServer = async() => new Promise<void>((resolve, reject) => {
|
||||
if (!wss) return
|
||||
wss.close()
|
||||
wss = null
|
||||
httpServer.close((err) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
|
||||
export const stopServer = async() => {
|
||||
console.log('stop')
|
||||
codeTools.stop()
|
||||
if (!status.status) {
|
||||
status.status = false
|
||||
status.message = ''
|
||||
status.address = []
|
||||
status.code = ''
|
||||
sendStatus(status)
|
||||
sendServerStatus(status)
|
||||
return
|
||||
}
|
||||
console.log('stoping sync server...')
|
||||
|
@ -184,14 +280,15 @@ export const stopServer = async() => {
|
|||
console.log(err)
|
||||
status.message = err.message
|
||||
}).finally(() => {
|
||||
sendStatus(status)
|
||||
sendServerStatus(status)
|
||||
})
|
||||
}
|
||||
|
||||
export const startServer = async(port: number) => {
|
||||
console.log('status.status', status.status)
|
||||
if (status.status) await handleStopServer()
|
||||
|
||||
console.log('starting sync server...')
|
||||
log.info('starting sync server')
|
||||
await handleStartServer(port).then(() => {
|
||||
console.log('sync server started')
|
||||
status.status = true
|
||||
|
@ -207,18 +304,14 @@ export const startServer = async(port: number) => {
|
|||
status.address = []
|
||||
status.code = ''
|
||||
}).finally(() => {
|
||||
sendStatus(status)
|
||||
sendServerStatus(status)
|
||||
})
|
||||
}
|
||||
|
||||
export const getStatus = (): LX.Sync.Status => status
|
||||
export const getStatus = (): LX.Sync.ServerStatus => status
|
||||
|
||||
export const generateCode = async() => {
|
||||
status.code = handleGenerateCode()
|
||||
sendStatus(status)
|
||||
sendServerStatus(status)
|
||||
return status.code
|
||||
}
|
||||
|
||||
export {
|
||||
removeSnapshot,
|
||||
}
|
||||
|
|
|
@ -1,13 +1,18 @@
|
|||
import { promises as fsPromises } from 'fs'
|
||||
import { encryptMsg, decryptMsg, getSnapshotFilePath } from './utils'
|
||||
import { throttle } from '@common/utils'
|
||||
import { type Server } from 'socket.io'
|
||||
import { sendCloseSelectMode, sendSelectMode } from '@main/modules/winMain'
|
||||
import { LIST_IDS } from '@common/constants'
|
||||
import { SYNC_CLOSE_CODE } from '@common/constants'
|
||||
import { removeSelectModeListener, sendCloseSelectMode, sendSelectMode } from '@main/modules/winMain'
|
||||
import { getSnapshot, updateDeviceSnapshotKey } from '../data'
|
||||
import { getLocalListData, setLocalListData } from '../utils'
|
||||
import { createSnapshot, encryptMsg, getCurrentListInfoKey } from './utils'
|
||||
|
||||
|
||||
const handleSetLocalListData = async(listData: LX.Sync.ListData) => {
|
||||
await setLocalListData(listData)
|
||||
return createSnapshot()
|
||||
}
|
||||
|
||||
// type ListInfoType = LX.List.UserListInfoFull | LX.List.MyDefaultListInfoFull | LX.List.MyLoveListInfoFull
|
||||
|
||||
let io: Server | null
|
||||
let wss: LX.Sync.Server.SocketServer | null
|
||||
let syncingId: string | null = null
|
||||
const wait = async(time = 1000) => await new Promise((resolve, reject) => setTimeout(resolve, time))
|
||||
|
||||
|
@ -19,107 +24,93 @@ const patchListData = (listData: Partial<LX.Sync.ListData>): LX.Sync.ListData =>
|
|||
}, listData)
|
||||
}
|
||||
|
||||
const getRemoteListData = async(socket: LX.Sync.Socket): Promise<LX.Sync.ListData> => await new Promise((resolve, reject) => {
|
||||
const getRemoteListData = async(socket: LX.Sync.Server.Socket): Promise<LX.Sync.ListData> => await new Promise((resolve, reject) => {
|
||||
console.log('getRemoteListData')
|
||||
const handleError = (reason: string) => {
|
||||
socket.removeListener('list:sync', handleSuccess)
|
||||
socket.removeListener('disconnect', handleError)
|
||||
reject(new Error(reason))
|
||||
}
|
||||
const handleSuccess = (enData: string) => {
|
||||
socket.removeListener('disconnect', handleError)
|
||||
socket.removeListener('list:sync', handleSuccess)
|
||||
console.log('getRemoteListData', 'handleSuccess')
|
||||
const data: LX.Sync.Data | null = JSON.parse(decryptMsg(socket.data.keyInfo, enData))
|
||||
if (!data) {
|
||||
reject(new Error('Get remote list data failed'))
|
||||
return
|
||||
}
|
||||
if (data.action != 'getData') return
|
||||
resolve(patchListData(data.data))
|
||||
}
|
||||
|
||||
socket.on('disconnect', handleError)
|
||||
socket.on('list:sync', handleSuccess)
|
||||
socket.emit('list:sync', encryptMsg(socket.data.keyInfo, JSON.stringify({ action: 'getData', data: 'all' })))
|
||||
let removeEventClose = socket.onClose(reject)
|
||||
let removeEvent = socket.onRemoteEvent('list:sync:list_sync_get_list_data', (listData) => {
|
||||
resolve(patchListData(listData))
|
||||
removeEventClose()
|
||||
removeEvent()
|
||||
})
|
||||
socket.sendData('list:sync:list_sync_get_list_data', undefined, (err) => {
|
||||
if (!err) return
|
||||
reject(err)
|
||||
removeEventClose()
|
||||
removeEvent()
|
||||
})
|
||||
})
|
||||
|
||||
// const getAllLists = async() => {
|
||||
// const lists = []
|
||||
// lists.push(await getListMusics(defaultList.id).then(musics => ({ ...defaultList, list: toRaw(musics).map(m => toOldMusicInfo(m)) })))
|
||||
// lists.push(await getListMusics(loveList.id).then(musics => ({ ...loveList, list: toRaw(musics).map(m => toOldMusicInfo(m)) })))
|
||||
const getRemoteListMD5 = async(socket: LX.Sync.Server.Socket): Promise<string> => await new Promise((resolve, reject) => {
|
||||
let removeEventClose = socket.onClose(reject)
|
||||
let removeEvent = socket.onRemoteEvent('list:sync:list_sync_get_md5', (md5) => {
|
||||
resolve(md5)
|
||||
removeEventClose()
|
||||
removeEvent()
|
||||
})
|
||||
socket.sendData('list:sync:list_sync_get_md5', undefined, (err) => {
|
||||
if (!err) return
|
||||
reject(err)
|
||||
removeEventClose()
|
||||
removeEvent()
|
||||
})
|
||||
})
|
||||
|
||||
// for await (const list of userLists) {
|
||||
// lists.push(await getListMusics(list.id).then(musics => ({ ...toRaw(list), list: toRaw(musics).map(m => toOldMusicInfo(m)) })))
|
||||
// }
|
||||
|
||||
// return lists
|
||||
// }
|
||||
|
||||
const getLocalListData = async(): Promise<LX.Sync.ListData> => {
|
||||
const lists: LX.Sync.ListData = {
|
||||
defaultList: await global.lx.worker.dbService.getListMusics(LIST_IDS.DEFAULT),
|
||||
loveList: await global.lx.worker.dbService.getListMusics(LIST_IDS.LOVE),
|
||||
userList: [],
|
||||
}
|
||||
|
||||
const userListInfos = await global.lx.worker.dbService.getAllUserList()
|
||||
for await (const list of userListInfos) {
|
||||
lists.userList.push(await global.lx.worker.dbService.getListMusics(list.id)
|
||||
.then(musics => ({ ...list, list: musics })))
|
||||
}
|
||||
|
||||
return lists
|
||||
}
|
||||
const getSyncMode = async(socket: LX.Sync.Socket): Promise<LX.Sync.Mode> => await new Promise((resolve, reject) => {
|
||||
const handleDisconnect = () => {
|
||||
const getSyncMode = async(socket: LX.Sync.Server.Socket): Promise<LX.Sync.Mode> => new Promise((resolve, reject) => {
|
||||
const handleDisconnect = (err: Error) => {
|
||||
sendCloseSelectMode()
|
||||
reject(new Error('disconnect'))
|
||||
removeSelectModeListener()
|
||||
reject(err)
|
||||
}
|
||||
socket.on('disconnect', handleDisconnect)
|
||||
let removeListener = sendSelectMode(socket.data.keyInfo, (mode) => {
|
||||
removeListener()
|
||||
sendSelectMode(socket.keyInfo.deviceName, (mode) => {
|
||||
removeSelectModeListener()
|
||||
removeEventClose()
|
||||
resolve(mode)
|
||||
})
|
||||
let removeEventClose = socket.onClose(handleDisconnect)
|
||||
})
|
||||
|
||||
const finishedSync = (socket: LX.Sync.Socket) => {
|
||||
return socket.emit('list:sync', encryptMsg(socket.data.keyInfo, JSON.stringify({
|
||||
action: 'finished',
|
||||
})))
|
||||
}
|
||||
const finishedSync = async(socket: LX.Sync.Server.Socket) => new Promise<void>((resolve, reject) => {
|
||||
socket.sendData('list:sync:finished', undefined, (err) => {
|
||||
if (err) reject(err)
|
||||
else resolve()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
const setLocalList = (listData: LX.Sync.ListData) => {
|
||||
void global.lx.event_list.list_data_overwrite(listData, true)
|
||||
}
|
||||
const setRemotelList = async(socket: LX.Sync.Socket, listData: LX.Sync.ListData) => {
|
||||
if (!io) return
|
||||
|
||||
const sockets: LX.Sync.RemoteSocket[] = await io.fetchSockets()
|
||||
for (const socket of sockets) {
|
||||
// if (excludeIds.includes(socket.data.keyInfo.clientId)) continue
|
||||
socket.emit('list:sync', encryptMsg(socket.data.keyInfo, JSON.stringify({ action: 'setData', data: listData })))
|
||||
const sendDataPromise = async(socket: LX.Sync.Server.Socket, dataStr: string, key: string) => new Promise<void>((resolve, reject) => {
|
||||
socket.send(encryptMsg(socket.keyInfo, dataStr), (err) => {
|
||||
if (err) {
|
||||
socket.close(SYNC_CLOSE_CODE.failed)
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
updateDeviceSnapshotKey(socket.keyInfo, key)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
const overwriteRemoteListData = async(listData: LX.Sync.ListData, key: string, excludeIds: string[] = []) => {
|
||||
if (!wss) return
|
||||
const dataStr = JSON.stringify({ action: 'list:sync:action', data: { action: 'list_data_overwrite', data: listData } })
|
||||
const tasks: Array<Promise<void>> = []
|
||||
for (const socket of wss.clients) {
|
||||
if (excludeIds.includes(socket.keyInfo.clientId) || !socket.isReady) continue
|
||||
tasks.push(sendDataPromise(socket, dataStr, key))
|
||||
}
|
||||
if (!tasks.length) return
|
||||
await Promise.all(tasks)
|
||||
}
|
||||
|
||||
const writeFilePromises = new Map<string, Promise<void>>()
|
||||
const updateSnapshot = async(path: string, data: string) => {
|
||||
console.log('updateSnapshot', path)
|
||||
let writeFilePromise = writeFilePromises.get(path) ?? Promise.resolve()
|
||||
writeFilePromise = writeFilePromise.then(async() => {
|
||||
if (writeFilePromise !== writeFilePromises.get(path)) return
|
||||
await fsPromises.writeFile(path, data)
|
||||
const setRemotelList = async(socket: LX.Sync.Server.Socket, listData: LX.Sync.ListData, key: string): Promise<void> => new Promise((resolve, reject) => {
|
||||
socket.sendData('list:sync:list_sync_set_data', listData, (err) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
updateDeviceSnapshotKey(socket.keyInfo, key)
|
||||
resolve()
|
||||
})
|
||||
writeFilePromises.set(path, writeFilePromise)
|
||||
await writeFilePromise.finally(() => {
|
||||
if (writeFilePromise !== writeFilePromises.get(path)) return
|
||||
writeFilePromises.delete(path)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
type UserDataObj = Record<string, LX.List.UserListInfoFull>
|
||||
|
||||
const createUserListDataObj = (listData: LX.Sync.ListData): UserDataObj => {
|
||||
const userListDataObj: UserDataObj = {}
|
||||
for (const list of listData.userList) userListDataObj[list.id] = list
|
||||
|
@ -225,12 +216,14 @@ const overwriteList = (sourceListData: LX.Sync.ListData, targetListData: LX.Sync
|
|||
return newListData
|
||||
}
|
||||
|
||||
const handleMergeListData = async(socket: LX.Sync.Socket): Promise<LX.Sync.ListData | null> => {
|
||||
const handleMergeListData = async(socket: LX.Sync.Server.Socket): Promise<[LX.Sync.ListData, boolean, boolean]> => {
|
||||
const mode: LX.Sync.Mode = await getSyncMode(socket)
|
||||
|
||||
const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalListData()])
|
||||
console.log('handleMergeListData', 'remoteListData, localListData')
|
||||
let listData: LX.Sync.ListData
|
||||
let requiredUpdateLocalListData = true
|
||||
let requiredUpdateRemoteListData = true
|
||||
switch (mode) {
|
||||
case 'merge_local_remote':
|
||||
listData = mergeList(localListData, remoteListData)
|
||||
|
@ -246,63 +239,47 @@ const handleMergeListData = async(socket: LX.Sync.Socket): Promise<LX.Sync.ListD
|
|||
break
|
||||
case 'overwrite_local_remote_full':
|
||||
listData = localListData
|
||||
requiredUpdateLocalListData = false
|
||||
break
|
||||
case 'overwrite_remote_local_full':
|
||||
listData = remoteListData
|
||||
requiredUpdateRemoteListData = false
|
||||
break
|
||||
case 'none': return null
|
||||
// case 'none': return null
|
||||
case 'cancel':
|
||||
socket.disconnect(true)
|
||||
default:
|
||||
socket.close(SYNC_CLOSE_CODE.normal)
|
||||
throw new Error('cancel')
|
||||
}
|
||||
return listData
|
||||
return [listData, requiredUpdateLocalListData, requiredUpdateRemoteListData]
|
||||
}
|
||||
|
||||
const handleSyncList = async(socket: LX.Sync.Socket): Promise<LX.Sync.ListData | null> => {
|
||||
const handleSyncList = async(socket: LX.Sync.Server.Socket) => {
|
||||
const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalListData()])
|
||||
console.log('handleSyncList', 'remoteListData, localListData')
|
||||
const listData: LX.Sync.ListData = {
|
||||
defaultList: [],
|
||||
loveList: [],
|
||||
userList: [],
|
||||
}
|
||||
if (localListData.defaultList.length || localListData.loveList.length || localListData.userList.length) {
|
||||
if (remoteListData.defaultList.length || remoteListData.loveList.length || remoteListData.userList.length) {
|
||||
const mergedList = await handleMergeListData(socket)
|
||||
const [mergedList, requiredUpdateLocalListData, requiredUpdateRemoteListData] = await handleMergeListData(socket)
|
||||
console.log('handleMergeListData', 'mergedList')
|
||||
// console.log(mergedList)
|
||||
if (!mergedList) return null
|
||||
listData.defaultList = mergedList.defaultList
|
||||
listData.loveList = mergedList.loveList
|
||||
listData.userList = mergedList.userList
|
||||
setLocalList(mergedList)
|
||||
void setRemotelList(socket, mergedList)
|
||||
let key
|
||||
if (requiredUpdateLocalListData) {
|
||||
key = await handleSetLocalListData(mergedList)
|
||||
await overwriteRemoteListData(mergedList, key, [socket.keyInfo.clientId])
|
||||
}
|
||||
if (requiredUpdateRemoteListData) {
|
||||
if (!key) key = await getCurrentListInfoKey()
|
||||
await setRemotelList(socket, mergedList, key)
|
||||
}
|
||||
} else {
|
||||
void setRemotelList(socket, localListData)
|
||||
listData.defaultList = localListData.defaultList
|
||||
listData.loveList = localListData.loveList
|
||||
listData.userList = localListData.userList
|
||||
await setRemotelList(socket, localListData, await getCurrentListInfoKey())
|
||||
}
|
||||
} else {
|
||||
if (remoteListData.defaultList.length || remoteListData.loveList.length || remoteListData.userList.length) {
|
||||
setLocalList(remoteListData)
|
||||
listData.defaultList = remoteListData.defaultList
|
||||
listData.loveList = remoteListData.loveList
|
||||
listData.userList = remoteListData.userList
|
||||
} else {
|
||||
listData.defaultList = localListData.defaultList
|
||||
listData.loveList = localListData.loveList
|
||||
listData.userList = localListData.userList
|
||||
const key = await handleSetLocalListData(remoteListData)
|
||||
updateDeviceSnapshotKey(socket.keyInfo, key)
|
||||
await overwriteRemoteListData(remoteListData, key, [socket.keyInfo.clientId])
|
||||
}
|
||||
}
|
||||
return await updateSnapshot(socket.data.snapshotFilePath, JSON.stringify({
|
||||
defaultList: listData.defaultList,
|
||||
loveList: listData.loveList,
|
||||
userList: listData.userList,
|
||||
})).then(() => {
|
||||
socket.data.isCreatedSnapshot = true
|
||||
return listData
|
||||
})
|
||||
}
|
||||
|
||||
const mergeListDataFromSnapshot = (
|
||||
|
@ -347,7 +324,16 @@ const mergeListDataFromSnapshot = (
|
|||
}
|
||||
return ids.map(id => map.get(id)) as LX.Music.MusicInfo[]
|
||||
}
|
||||
const handleMergeListDataFromSnapshot = async(socket: LX.Sync.Socket, snapshot: LX.Sync.ListData): Promise<LX.Sync.ListData> => {
|
||||
const checkListLatest = async(socket: LX.Sync.Server.Socket) => {
|
||||
const remoteListMD5 = await getRemoteListMD5(socket)
|
||||
const currentListInfoKey = await getCurrentListInfoKey()
|
||||
const latest = remoteListMD5 == currentListInfoKey
|
||||
if (latest && socket.keyInfo.snapshotKey != currentListInfoKey) updateDeviceSnapshotKey(socket.keyInfo, currentListInfoKey)
|
||||
return latest
|
||||
}
|
||||
const handleMergeListDataFromSnapshot = async(socket: LX.Sync.Server.Socket, snapshot: LX.Sync.ListData) => {
|
||||
if (await checkListLatest(socket)) return
|
||||
|
||||
const addMusicLocationType = global.lx.appSetting['list.addMusicLocationType']
|
||||
const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalListData()])
|
||||
const newListData: LX.Sync.ListData = {
|
||||
|
@ -404,79 +390,37 @@ const handleMergeListDataFromSnapshot = async(socket: LX.Sync.Socket, snapshot:
|
|||
})
|
||||
|
||||
newListData.userList = newUserList
|
||||
setLocalList(newListData)
|
||||
void setRemotelList(socket, newListData)
|
||||
return await updateSnapshot(socket.data.snapshotFilePath, JSON.stringify({
|
||||
defaultList: newListData.defaultList,
|
||||
loveList: newListData.loveList,
|
||||
userList: newListData.userList,
|
||||
})).then(() => {
|
||||
socket.data.isCreatedSnapshot = true
|
||||
return newListData
|
||||
})
|
||||
const key = await handleSetLocalListData(newListData)
|
||||
await setRemotelList(socket, newListData, key)
|
||||
await overwriteRemoteListData(newListData, key, [socket.keyInfo.clientId])
|
||||
}
|
||||
|
||||
const registerUpdateSnapshotTask = (socket: LX.Sync.Socket, snapshot: LX.Sync.ListData) => {
|
||||
if (!socket.data.isCreatedSnapshot) return
|
||||
const handleUpdateSnapshot = throttle(() => {
|
||||
// TODO: 同步成功后再保存快照
|
||||
void getLocalListData().then(({ defaultList, loveList, userList }) => {
|
||||
if (defaultList != null) snapshot.defaultList = defaultList
|
||||
if (loveList != null) snapshot.loveList = loveList
|
||||
if (userList != null) snapshot.userList = userList
|
||||
void updateSnapshot(socket.data.snapshotFilePath, JSON.stringify(snapshot))
|
||||
})
|
||||
}, 2000)
|
||||
global.lx.event_list.on('list_changed', handleUpdateSnapshot)
|
||||
socket.on('disconnect', () => {
|
||||
global.lx.event_list.off('list_changed', handleUpdateSnapshot)
|
||||
})
|
||||
}
|
||||
|
||||
const syncList = async(socket: LX.Sync.Socket): Promise<LX.Sync.ListData | null> => {
|
||||
socket.data.snapshotFilePath = getSnapshotFilePath(socket.data.keyInfo)
|
||||
let fileData: any
|
||||
let isSyncRequired = false
|
||||
try {
|
||||
fileData = (await fsPromises.readFile(socket.data.snapshotFilePath)).toString()
|
||||
fileData = JSON.parse(fileData)
|
||||
} catch (error) {
|
||||
const err = error as NodeJS.ErrnoException
|
||||
if (err.code != 'ENOENT') throw err
|
||||
isSyncRequired = true
|
||||
const syncList = async(socket: LX.Sync.Server.Socket) => {
|
||||
// socket.data.snapshotFilePath = getSnapshotFilePath(socket.keyInfo)
|
||||
if (socket.keyInfo.snapshotKey) {
|
||||
const listData = await getSnapshot(socket.keyInfo.snapshotKey)
|
||||
if (listData) {
|
||||
await handleMergeListDataFromSnapshot(socket, listData)
|
||||
return
|
||||
}
|
||||
}
|
||||
console.log('isSyncRequired', isSyncRequired)
|
||||
if (isSyncRequired) return await handleSyncList(socket)
|
||||
return await handleMergeListDataFromSnapshot(socket, patchListData(fileData))
|
||||
await handleSyncList(socket)
|
||||
}
|
||||
|
||||
// const checkSyncQueue = async(): Promise<void> => {
|
||||
// if (!syncingId) return
|
||||
// console.log('sync queue...')
|
||||
// await wait()
|
||||
// await checkSyncQueue()
|
||||
// }
|
||||
|
||||
// export {
|
||||
// syncList = async(_io: Server, socket: LX.Sync.Socket) => {
|
||||
// io = _io
|
||||
// await checkSyncQueue()
|
||||
// syncingId = socket.data.keyInfo.clientId
|
||||
// return await syncList(socket).then(newListData => {
|
||||
// registerUpdateSnapshotTask(socket, { ...newListData })
|
||||
// return finishedSync(socket)
|
||||
// }).finally(() => {
|
||||
// syncingId = null
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
export default async(_wss: LX.Sync.Server.SocketServer, socket: LX.Sync.Server.Socket) => {
|
||||
if (!wss) {
|
||||
wss = _wss
|
||||
_wss.addListener('close', () => {
|
||||
wss = null
|
||||
})
|
||||
}
|
||||
|
||||
const _syncList = async(_io: Server, socket: LX.Sync.Socket) => {
|
||||
io = _io
|
||||
let disconnected = false
|
||||
socket.on('disconnect', () => {
|
||||
socket.onClose(() => {
|
||||
disconnected = true
|
||||
if (syncingId == socket.data.keyInfo.clientId) syncingId = null
|
||||
if (syncingId == socket.keyInfo.clientId) syncingId = null
|
||||
})
|
||||
|
||||
while (true) {
|
||||
|
@ -485,22 +429,16 @@ const _syncList = async(_io: Server, socket: LX.Sync.Socket) => {
|
|||
await wait()
|
||||
}
|
||||
|
||||
syncingId = socket.data.keyInfo.clientId
|
||||
return await syncList(socket).then(newListData => {
|
||||
if (newListData) registerUpdateSnapshotTask(socket, { ...newListData })
|
||||
syncingId = socket.keyInfo.clientId
|
||||
await syncList(socket).then(async() => {
|
||||
// if (newListData) registerUpdateSnapshotTask(socket, { ...newListData })
|
||||
return finishedSync(socket)
|
||||
}).finally(() => {
|
||||
syncingId = null
|
||||
})
|
||||
}
|
||||
|
||||
const removeSnapshot = async(keyInfo: LX.Sync.KeyInfo) => {
|
||||
const filePath = getSnapshotFilePath(keyInfo)
|
||||
await fsPromises.unlink(filePath)
|
||||
}
|
||||
|
||||
export {
|
||||
_syncList as syncList,
|
||||
removeSnapshot,
|
||||
}
|
||||
|
||||
// const removeSnapshot = async(keyInfo: LX.Sync.KeyInfo) => {
|
||||
// const filePath = getSnapshotFilePath(keyInfo)
|
||||
// await fsPromises.unlink(filePath)
|
||||
// }
|
||||
|
|
|
@ -1,97 +1,28 @@
|
|||
import { networkInterfaces } from 'os'
|
||||
import { randomBytes, createCipheriv, createDecipheriv, publicEncrypt, privateDecrypt, constants } from 'crypto'
|
||||
import { join } from 'path'
|
||||
import getStore from '@main/utils/store'
|
||||
|
||||
const STORE_NAME = 'sync'
|
||||
|
||||
type KeyInfos = Record<string, LX.Sync.KeyInfo>
|
||||
|
||||
export const getAddress = (): string[] => {
|
||||
const nets = networkInterfaces()
|
||||
const results: string[] = []
|
||||
// console.log(nets)
|
||||
|
||||
for (const interfaceInfos of Object.values(nets)) {
|
||||
if (!interfaceInfos) continue
|
||||
// Skip over non-IPv4 and internal (i.e. 127.0.0.1) addresses
|
||||
for (const interfaceInfo of interfaceInfos) {
|
||||
if (interfaceInfo.family === 'IPv4' && !interfaceInfo.internal) {
|
||||
results.push(interfaceInfo.address)
|
||||
}
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
let serverId: string | undefined
|
||||
export const getServerId = (): string => {
|
||||
if (serverId) return serverId
|
||||
const store = getStore(STORE_NAME)
|
||||
serverId = store.get('serverId') as string | undefined
|
||||
if (!serverId) {
|
||||
serverId = randomBytes(4 * 4).toString('base64')
|
||||
store.set('serverId', serverId)
|
||||
}
|
||||
return serverId
|
||||
}
|
||||
|
||||
let keyInfos: KeyInfos
|
||||
export const createClientKeyInfo = (deviceName: string): LX.Sync.KeyInfo => {
|
||||
const keyInfo: LX.Sync.KeyInfo = {
|
||||
clientId: randomBytes(4 * 4).toString('base64'),
|
||||
key: randomBytes(16).toString('base64'),
|
||||
deviceName,
|
||||
}
|
||||
const store = getStore(STORE_NAME)
|
||||
keyInfos ??= store.get('keys') as KeyInfos || {}
|
||||
if (Object.keys(keyInfos).length > 101) throw new Error('max keys')
|
||||
|
||||
keyInfos[keyInfo.clientId] = keyInfo
|
||||
store.set('keys', keyInfos)
|
||||
return keyInfo
|
||||
}
|
||||
export const setClientKeyInfo = (keyInfo: LX.Sync.KeyInfo) => {
|
||||
keyInfos[keyInfo.clientId] = keyInfo
|
||||
const store = getStore(STORE_NAME)
|
||||
store.set('keys', keyInfos)
|
||||
}
|
||||
export const getClientKeyInfo = (clientId: string): LX.Sync.KeyInfo | null => {
|
||||
if (!keyInfos) {
|
||||
const store = getStore(STORE_NAME)
|
||||
keyInfos = store.get('keys') as KeyInfos || {}
|
||||
}
|
||||
return keyInfos[clientId] ?? null
|
||||
}
|
||||
import { toMD5 } from '@common/utils/nodejs'
|
||||
import type http from 'node:http'
|
||||
import {
|
||||
getSnapshotInfo,
|
||||
saveSnapshot,
|
||||
saveSnapshotInfo,
|
||||
type SnapshotInfo,
|
||||
} from '../data'
|
||||
import { getLocalListData } from '../utils'
|
||||
|
||||
export const generateCode = (): string => {
|
||||
return Math.random().toString().substring(2, 8)
|
||||
}
|
||||
|
||||
export const aesEncrypt = (buffer: string | Buffer, key: string): string => {
|
||||
const cipher = createCipheriv('aes-128-ecb', Buffer.from(key, 'base64'), '')
|
||||
return Buffer.concat([cipher.update(buffer), cipher.final()]).toString('base64')
|
||||
export const getIP = (request: http.IncomingMessage) => {
|
||||
return request.socket.remoteAddress
|
||||
}
|
||||
|
||||
export const aesDecrypt = (text: string, key: string): string => {
|
||||
const decipher = createDecipheriv('aes-128-ecb', Buffer.from(key, 'base64'), '')
|
||||
return Buffer.concat([decipher.update(Buffer.from(text, 'base64')), decipher.final()]).toString()
|
||||
}
|
||||
|
||||
export const rsaEncrypt = (buffer: Buffer, key: string): string => {
|
||||
return publicEncrypt({ key, padding: constants.RSA_PKCS1_OAEP_PADDING }, buffer).toString('base64')
|
||||
}
|
||||
export const rsaDecrypt = (buffer: Buffer, key: string): Buffer => {
|
||||
return privateDecrypt({ key, padding: constants.RSA_PKCS1_OAEP_PADDING }, buffer)
|
||||
}
|
||||
|
||||
export const encryptMsg = (keyInfo: LX.Sync.KeyInfo, msg: string): string => {
|
||||
export const encryptMsg = (keyInfo: LX.Sync.ServerKeyInfo, msg: string): string => {
|
||||
return msg
|
||||
// if (!keyInfo) return ''
|
||||
// return aesEncrypt(msg, keyInfo.key, keyInfo.iv)
|
||||
}
|
||||
|
||||
export const decryptMsg = (keyInfo: LX.Sync.KeyInfo, enMsg: string): string => {
|
||||
export const decryptMsg = (keyInfo: LX.Sync.ServerKeyInfo, enMsg: string): string => {
|
||||
return enMsg
|
||||
// if (!keyInfo) return ''
|
||||
// let msg = ''
|
||||
|
@ -103,6 +34,29 @@ export const decryptMsg = (keyInfo: LX.Sync.KeyInfo, enMsg: string): string => {
|
|||
// return msg
|
||||
}
|
||||
|
||||
export const getSnapshotFilePath = (keyInfo: LX.Sync.KeyInfo): string => {
|
||||
return join(global.lxDataPath, `snapshot-${Buffer.from(keyInfo.clientId).toString('hex').substring(0, 10)}.json`)
|
||||
let snapshotInfo: SnapshotInfo
|
||||
export const createSnapshot = async() => {
|
||||
if (!snapshotInfo) snapshotInfo = getSnapshotInfo()
|
||||
const listData = JSON.stringify(await getLocalListData())
|
||||
const md5 = toMD5(listData)
|
||||
if (snapshotInfo.latest == md5) return md5
|
||||
if (snapshotInfo.list.includes(md5)) {
|
||||
snapshotInfo.list.splice(snapshotInfo.list.indexOf(md5), 1)
|
||||
} else await saveSnapshot(md5, listData)
|
||||
if (snapshotInfo.latest) snapshotInfo.list.unshift(snapshotInfo.latest)
|
||||
snapshotInfo.latest = md5
|
||||
snapshotInfo.time = Date.now()
|
||||
saveSnapshotInfo(snapshotInfo)
|
||||
return md5
|
||||
}
|
||||
|
||||
|
||||
export const getCurrentListInfoKey = async() => {
|
||||
if (!snapshotInfo) snapshotInfo = getSnapshotInfo()
|
||||
if (snapshotInfo.latest) {
|
||||
return snapshotInfo.latest
|
||||
}
|
||||
snapshotInfo.latest = toMD5(JSON.stringify(await getLocalListData()))
|
||||
saveSnapshotInfo(snapshotInfo)
|
||||
return snapshotInfo.latest
|
||||
}
|
||||
|
|
|
@ -1,59 +1,84 @@
|
|||
// import { throttle } from '@common/utils/common'
|
||||
import { type Server } from 'socket.io'
|
||||
// import { sendSyncActionList } from '@main/modules/winMain'
|
||||
import { encryptMsg, decryptMsg } from '../server/utils'
|
||||
import { createCipheriv, createDecipheriv, publicEncrypt, privateDecrypt, constants } from 'node:crypto'
|
||||
import os, { networkInterfaces } from 'node:os'
|
||||
import cp from 'node:child_process'
|
||||
import { LIST_IDS } from '@common/constants'
|
||||
|
||||
let io: Server | null
|
||||
let removeListener: (() => void) | null
|
||||
|
||||
type listAction = 'list:action'
|
||||
export const getAddress = (): string[] => {
|
||||
const nets = networkInterfaces()
|
||||
const results: string[] = []
|
||||
// console.log(nets)
|
||||
|
||||
const handleListAction = ({ action, data }: LX.Sync.ActionList) => {
|
||||
// console.log('handleListAction', action)
|
||||
|
||||
switch (action) {
|
||||
case 'list_data_overwrite':
|
||||
void global.lx.event_list.list_data_overwrite(data, true)
|
||||
break
|
||||
case 'list_create':
|
||||
void global.lx.event_list.list_create(data.position, data.listInfos, true)
|
||||
break
|
||||
case 'list_remove':
|
||||
void global.lx.event_list.list_remove(data, true)
|
||||
break
|
||||
case 'list_update':
|
||||
void global.lx.event_list.list_update(data, true)
|
||||
break
|
||||
case 'list_update_position':
|
||||
void global.lx.event_list.list_update_position(data.position, data.ids, true)
|
||||
break
|
||||
case 'list_music_add':
|
||||
void global.lx.event_list.list_music_add(data.id, data.musicInfos, data.addMusicLocationType, true)
|
||||
break
|
||||
case 'list_music_move':
|
||||
void global.lx.event_list.list_music_move(data.fromId, data.toId, data.musicInfos, data.addMusicLocationType, true)
|
||||
break
|
||||
case 'list_music_remove':
|
||||
void global.lx.event_list.list_music_remove(data.listId, data.ids, true)
|
||||
break
|
||||
case 'list_music_update':
|
||||
void global.lx.event_list.list_music_update(data, true)
|
||||
break
|
||||
case 'list_music_update_position':
|
||||
void global.lx.event_list.list_music_update_position(data.listId, data.position, data.ids, true)
|
||||
break
|
||||
case 'list_music_overwrite':
|
||||
void global.lx.event_list.list_music_overwrite(data.listId, data.musicInfos, true)
|
||||
break
|
||||
case 'list_music_clear':
|
||||
void global.lx.event_list.list_music_clear(data, true)
|
||||
break
|
||||
default:
|
||||
break
|
||||
for (const interfaceInfos of Object.values(nets)) {
|
||||
if (!interfaceInfos) continue
|
||||
// Skip over non-IPv4 and internal (i.e. 127.0.0.1) addresses
|
||||
for (const interfaceInfo of interfaceInfos) {
|
||||
if (interfaceInfo.family === 'IPv4' && !interfaceInfo.internal) {
|
||||
results.push(interfaceInfo.address)
|
||||
}
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
const registerListActionEvent = () => {
|
||||
|
||||
// https://stackoverflow.com/a/75309339
|
||||
export const getComputerName = () => {
|
||||
let name: string | undefined
|
||||
switch (process.platform) {
|
||||
case 'win32':
|
||||
name = process.env.COMPUTERNAME
|
||||
break
|
||||
case 'darwin':
|
||||
name = cp.execSync('scutil --get ComputerName').toString().trim()
|
||||
break
|
||||
case 'linux':
|
||||
name = cp.execSync('hostnamectl --pretty').toString().trim()
|
||||
break
|
||||
}
|
||||
if (!name) name = os.hostname()
|
||||
return name
|
||||
}
|
||||
|
||||
export const aesEncrypt = (text: string, key: string) => {
|
||||
const cipher = createCipheriv('aes-128-ecb', Buffer.from(key, 'base64'), '')
|
||||
return Buffer.concat([cipher.update(Buffer.from(text)), cipher.final()]).toString('base64')
|
||||
}
|
||||
|
||||
export const aesDecrypt = (text: string, key: string) => {
|
||||
const decipher = createDecipheriv('aes-128-ecb', Buffer.from(key, 'base64'), '')
|
||||
return Buffer.concat([decipher.update(Buffer.from(text, 'base64')), decipher.final()]).toString()
|
||||
}
|
||||
|
||||
export const rsaEncrypt = (buffer: Buffer, key: string): string => {
|
||||
return publicEncrypt({ key, padding: constants.RSA_PKCS1_OAEP_PADDING }, buffer).toString('base64')
|
||||
}
|
||||
export const rsaDecrypt = (buffer: Buffer, key: string): Buffer => {
|
||||
return privateDecrypt({ key, padding: constants.RSA_PKCS1_OAEP_PADDING }, buffer)
|
||||
}
|
||||
|
||||
export const getLocalListData = async(): Promise<LX.Sync.ListData> => {
|
||||
const lists: LX.Sync.ListData = {
|
||||
defaultList: await global.lx.worker.dbService.getListMusics(LIST_IDS.DEFAULT),
|
||||
loveList: await global.lx.worker.dbService.getListMusics(LIST_IDS.LOVE),
|
||||
userList: [],
|
||||
}
|
||||
|
||||
const userListInfos = await global.lx.worker.dbService.getAllUserList()
|
||||
for await (const list of userListInfos) {
|
||||
lists.userList.push(await global.lx.worker.dbService.getListMusics(list.id)
|
||||
.then(musics => ({ ...list, list: musics })))
|
||||
}
|
||||
|
||||
return lists
|
||||
}
|
||||
|
||||
export const setLocalListData = async(listData: LX.Sync.ListData) => {
|
||||
await global.lx.event_list.list_data_overwrite(listData, true)
|
||||
}
|
||||
|
||||
|
||||
export const registerListActionEvent = (sendListAction: (action: LX.Sync.ActionList) => (void | Promise<void>)) => {
|
||||
const list_data_overwrite = async(listData: MakeOptional<LX.List.ListDataFull, 'tempList'>, isRemote: boolean = false) => {
|
||||
if (isRemote) return
|
||||
await sendListAction({ action: 'list_data_overwrite', data: listData })
|
||||
|
@ -130,48 +155,48 @@ const registerListActionEvent = () => {
|
|||
}
|
||||
}
|
||||
|
||||
// const addMusic = (orderId, callback) => {
|
||||
// // ...
|
||||
// }
|
||||
export const handleRemoteListAction = async({ action, data }: LX.Sync.ActionList) => {
|
||||
// console.log('handleRemoteListAction', action)
|
||||
|
||||
const broadcast = async(action: listAction, data: any, excludeIds: string[] = []) => {
|
||||
if (!io) return
|
||||
const sockets: LX.Sync.RemoteSocket[] = await io.fetchSockets()
|
||||
for (const socket of sockets) {
|
||||
if (excludeIds.includes(socket.data.keyInfo.clientId) || !socket.data.isReady) continue
|
||||
socket.emit(action, encryptMsg(socket.data.keyInfo, data))
|
||||
}
|
||||
}
|
||||
|
||||
export const sendListAction = async(action: LX.Sync.ActionList) => {
|
||||
console.log('sendListAction', action.action)
|
||||
// io.sockets
|
||||
await broadcast('list:action', JSON.stringify(action))
|
||||
}
|
||||
|
||||
export const registerListHandler = (_io: Server, socket: LX.Sync.Socket) => {
|
||||
if (!io) {
|
||||
io = _io
|
||||
removeListener = registerListActionEvent()
|
||||
}
|
||||
|
||||
socket.on('list:action', msg => {
|
||||
if (!socket.data.isReady) return
|
||||
// console.log(msg)
|
||||
msg = decryptMsg(socket.data.keyInfo, msg)
|
||||
if (!msg) return
|
||||
handleListAction(JSON.parse(msg))
|
||||
void broadcast('list:action', msg, [socket.data.keyInfo.clientId])
|
||||
// socket.broadcast.emit('list:action', { action: 'list_remove', data: { id: 'default', index: 0 } })
|
||||
})
|
||||
|
||||
// socket.on('list:add', addMusic)
|
||||
}
|
||||
export const unregisterListHandler = () => {
|
||||
io = null
|
||||
|
||||
if (removeListener) {
|
||||
removeListener()
|
||||
removeListener = null
|
||||
switch (action) {
|
||||
case 'list_data_overwrite':
|
||||
void global.lx.event_list.list_data_overwrite(data, true)
|
||||
break
|
||||
case 'list_create':
|
||||
void global.lx.event_list.list_create(data.position, data.listInfos, true)
|
||||
break
|
||||
case 'list_remove':
|
||||
void global.lx.event_list.list_remove(data, true)
|
||||
break
|
||||
case 'list_update':
|
||||
void global.lx.event_list.list_update(data, true)
|
||||
break
|
||||
case 'list_update_position':
|
||||
void global.lx.event_list.list_update_position(data.position, data.ids, true)
|
||||
break
|
||||
case 'list_music_add':
|
||||
void global.lx.event_list.list_music_add(data.id, data.musicInfos, data.addMusicLocationType, true)
|
||||
break
|
||||
case 'list_music_move':
|
||||
void global.lx.event_list.list_music_move(data.fromId, data.toId, data.musicInfos, data.addMusicLocationType, true)
|
||||
break
|
||||
case 'list_music_remove':
|
||||
void global.lx.event_list.list_music_remove(data.listId, data.ids, true)
|
||||
break
|
||||
case 'list_music_update':
|
||||
void global.lx.event_list.list_music_update(data, true)
|
||||
break
|
||||
case 'list_music_update_position':
|
||||
void global.lx.event_list.list_music_update_position(data.listId, data.position, data.ids, true)
|
||||
break
|
||||
case 'list_music_overwrite':
|
||||
void global.lx.event_list.list_music_overwrite(data.listId, data.musicInfos, true)
|
||||
break
|
||||
case 'list_music_clear':
|
||||
void global.lx.event_list.list_music_clear(data, true)
|
||||
break
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import { mainHandle } from '@common/mainIpc'
|
||||
import { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames'
|
||||
import { startServer, stopServer, getStatus, generateCode } from '@main/modules/sync'
|
||||
import { startServer, stopServer, getServerStatus, generateCode, connectServer, disconnectServer, getClientStatus } from '@main/modules/sync'
|
||||
import { sendEvent } from '../main'
|
||||
|
||||
let selectModeListenr: ((mode: LX.Sync.Mode) => void) | null = null
|
||||
|
@ -8,13 +8,15 @@ let selectModeListenr: ((mode: LX.Sync.Mode) => void) | null = null
|
|||
export default () => {
|
||||
mainHandle<LX.Sync.SyncServiceActions, any>(WIN_MAIN_RENDERER_EVENT_NAME.sync_action, async({ params: data }) => {
|
||||
switch (data.action) {
|
||||
case 'enable':
|
||||
case 'enable_server':
|
||||
data.data.enable ? await startServer(parseInt(data.data.port)) : await stopServer()
|
||||
return
|
||||
case 'get_status':
|
||||
return getStatus()
|
||||
case 'generate_code':
|
||||
return await generateCode()
|
||||
case 'enable_client':
|
||||
data.data.enable ? await connectServer(data.data.host, data.data.authCode) : await disconnectServer()
|
||||
return
|
||||
case 'get_server_status': return getServerStatus()
|
||||
case 'get_client_status': return getClientStatus()
|
||||
case 'generate_code': return generateCode()
|
||||
case 'select_mode':
|
||||
if (selectModeListenr) selectModeListenr(data.data)
|
||||
break
|
||||
|
@ -29,18 +31,24 @@ export const sendSyncAction = (data: LX.Sync.SyncMainWindowActions) => {
|
|||
sendEvent(WIN_MAIN_RENDERER_EVENT_NAME.sync_action, data)
|
||||
}
|
||||
|
||||
export const sendStatus = (status: LX.Sync.Status) => {
|
||||
export const sendClientStatus = (status: LX.Sync.ClientStatus) => {
|
||||
sendSyncAction({
|
||||
action: 'status',
|
||||
action: 'client_status',
|
||||
data: status,
|
||||
})
|
||||
}
|
||||
export const sendSelectMode = (keyInfo: LX.Sync.KeyInfo, listener: (mode: LX.Sync.Mode) => void) => {
|
||||
export const sendServerStatus = (status: LX.Sync.ServerStatus) => {
|
||||
sendSyncAction({
|
||||
action: 'server_status',
|
||||
data: status,
|
||||
})
|
||||
}
|
||||
export const sendSelectMode = (deviceName: string, listener: (mode: LX.Sync.Mode) => void) => {
|
||||
selectModeListenr = listener
|
||||
sendSyncAction({ action: 'select_mode', data: keyInfo })
|
||||
return () => {
|
||||
selectModeListenr = null
|
||||
}
|
||||
sendSyncAction({ action: 'select_mode', data: deviceName })
|
||||
}
|
||||
export const removeSelectModeListener = () => {
|
||||
selectModeListenr = null
|
||||
}
|
||||
export const sendCloseSelectMode = () => {
|
||||
sendSyncAction({ action: 'close_select_mode' })
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Socket as _Socket, RemoteSocket as _RemoteSocket } from 'socket.io'
|
||||
import type WS from 'ws'
|
||||
|
||||
type DefaultEventsMap = Record<string, (...args: any[]) => void>
|
||||
|
||||
|
@ -6,24 +6,57 @@ type DefaultEventsMap = Record<string, (...args: any[]) => void>
|
|||
declare global {
|
||||
namespace LX {
|
||||
namespace Sync {
|
||||
class Socket extends _Socket {
|
||||
data: SocketData
|
||||
namespace Client {
|
||||
interface Socket extends WS.WebSocket {
|
||||
isReady: boolean
|
||||
data: {
|
||||
keyInfo: ClientKeyInfo
|
||||
urlInfo: UrlInfo
|
||||
}
|
||||
|
||||
onRemoteEvent: <T extends keyof LX.Sync.ActionSyncSendType>(
|
||||
eventName: T,
|
||||
handler: (data: LX.Sync.ActionSyncSendType[T]) => (void | Promise<void>)
|
||||
) => () => void
|
||||
|
||||
onClose: (handler: (err: Error) => (void | Promise<void>)) => () => void
|
||||
|
||||
sendData: <T extends keyof LX.Sync.ActionSyncType>(
|
||||
eventName: T,
|
||||
data?: LX.Sync.ActionSyncType[T],
|
||||
callback?: (err?: Error) => void
|
||||
) => void
|
||||
}
|
||||
|
||||
interface UrlInfo {
|
||||
wsProtocol: string
|
||||
httpProtocol: string
|
||||
hostPath: string
|
||||
href: string
|
||||
}
|
||||
}
|
||||
class RemoteSocket extends _RemoteSocket<DefaultEventsMap, any> {
|
||||
readonly data: SocketData
|
||||
namespace Server {
|
||||
interface Socket extends WS.WebSocket {
|
||||
isAlive?: boolean
|
||||
isMobile: boolean
|
||||
isReady: boolean
|
||||
keyInfo: LX.Sync.ServerKeyInfo
|
||||
|
||||
onRemoteEvent: <T extends keyof LX.Sync.ActionSyncType>(
|
||||
eventName: T,
|
||||
handler: (data: LX.Sync.ActionSyncType[T]) => void
|
||||
) => () => void
|
||||
|
||||
onClose: (handler: (err: Error) => (void | Promise<void>)) => () => void
|
||||
|
||||
sendData: <T extends keyof LX.Sync.ActionSyncSendType>(
|
||||
eventName: T,
|
||||
data?: LX.Sync.ActionSyncSendType[T],
|
||||
callback?: (err?: Error) => void
|
||||
) => void
|
||||
}
|
||||
type SocketServer = WS.Server<Socket>
|
||||
}
|
||||
interface Data {
|
||||
action: string
|
||||
data: any
|
||||
}
|
||||
interface SocketData {
|
||||
snapshotFilePath: string
|
||||
isCreatedSnapshot: boolean
|
||||
keyInfo: KeyInfo
|
||||
isReady: boolean
|
||||
}
|
||||
type Action = 'list:sync'
|
||||
type ListAction = 'getData' | 'finished'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,221 @@
|
|||
import needle, { type NeedleHttpVerbs, type NeedleOptions, type BodyData, type NeedleCallback, type NeedleResponse } from 'needle'
|
||||
// import progress from 'request-progress'
|
||||
import { httpOverHttp, httpsOverHttp } from 'tunnel'
|
||||
import { type ClientRequest } from 'node:http'
|
||||
// import fs from 'fs'
|
||||
|
||||
export const requestMsg = {
|
||||
fail: '请求异常😮,可以多试几次,若还是不行就换一首吧。。。',
|
||||
unachievable: '哦No😱...接口无法访问了!',
|
||||
timeout: '请求超时',
|
||||
// unachievable: '哦No😱...接口无法访问了!已帮你切换到临时接口,重试下看能不能播放吧~',
|
||||
notConnectNetwork: '无法连接到服务器',
|
||||
cancelRequest: '取消http请求',
|
||||
} as const
|
||||
|
||||
|
||||
const httpsRxp = /^https:/
|
||||
let envProxy: null | { host: string, port: string } = null
|
||||
|
||||
const getRequestAgent = (url: string) => {
|
||||
if (envProxy == null) {
|
||||
if (global.envParams.cmdParams['proxy-server'] && typeof global.envParams.cmdParams['proxy-server'] == 'string') {
|
||||
const [host, port = ''] = global.envParams.cmdParams['proxy-server'].split(':')
|
||||
envProxy = {
|
||||
host,
|
||||
port,
|
||||
}
|
||||
}
|
||||
}
|
||||
const proxy = {
|
||||
enable: global.lx.appSetting['network.proxy.enable'],
|
||||
host: global.lx.appSetting['network.proxy.host'],
|
||||
port: global.lx.appSetting['network.proxy.port'],
|
||||
envProxy,
|
||||
}
|
||||
|
||||
let options
|
||||
if (global.lx.appSetting['network.proxy.enable'] && proxy.host) {
|
||||
options = {
|
||||
proxy: {
|
||||
host: proxy.host,
|
||||
port: parseInt(proxy.port || '80'),
|
||||
},
|
||||
}
|
||||
} else if (proxy.envProxy) {
|
||||
options = {
|
||||
proxy: {
|
||||
host: proxy.envProxy.host,
|
||||
port: parseInt(proxy.envProxy.port || '80'),
|
||||
},
|
||||
}
|
||||
}
|
||||
return options ? (httpsRxp.test(url) ? httpsOverHttp : httpOverHttp)(options) : undefined
|
||||
}
|
||||
|
||||
export interface RequestOptions extends NeedleOptions {
|
||||
method?: NeedleHttpVerbs
|
||||
body?: BodyData
|
||||
form?: BodyData
|
||||
formData?: BodyData
|
||||
}
|
||||
export type RequestCallback = NeedleCallback
|
||||
type RequestResponse = NeedleResponse
|
||||
const request = (url: string, options: RequestOptions, callback: RequestCallback): ClientRequest => {
|
||||
let data: BodyData = null
|
||||
if (options.body) {
|
||||
data = options.body
|
||||
} else if (options.form) {
|
||||
data = options.form
|
||||
// data.content_type = 'application/x-www-form-urlencoded'
|
||||
options.json = false
|
||||
} else if (options.formData) {
|
||||
data = options.formData
|
||||
// data.content_type = 'multipart/form-data'
|
||||
options.json = false
|
||||
}
|
||||
options.response_timeout = options.timeout
|
||||
|
||||
return needle.request(options.method ?? 'get', url, data, options, (err, resp, body) => {
|
||||
if (!err) {
|
||||
body = resp.body = resp.raw.toString()
|
||||
try {
|
||||
resp.body = JSON.parse(resp.body)
|
||||
} catch (_) {}
|
||||
body = resp.body
|
||||
}
|
||||
callback(err, resp, body)
|
||||
// @ts-expect-error
|
||||
}).request
|
||||
}
|
||||
|
||||
|
||||
const defaultHeaders = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36',
|
||||
}
|
||||
// var proxyUrl = "http://" + user + ":" + password + "@" + host + ":" + port;
|
||||
// var proxiedRequest = request.defaults({'proxy': proxyUrl});
|
||||
|
||||
// interface RequestPromise extends Promise<RequestResponse> {
|
||||
// abort: () => void
|
||||
// }
|
||||
|
||||
/**
|
||||
* promise 形式的请求方法
|
||||
* @param {*} url
|
||||
* @param {*} options
|
||||
*/
|
||||
const buildHttpPromose = async(url: string, options: RequestOptions): Promise<RequestResponse> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
void fetchData(url, options.method, options, (err, resp, body) => {
|
||||
// options.isShowProgress && window.api.hideProgress()
|
||||
// debugRequest && console.log(`\n---response------${url}------------`)
|
||||
// debugRequest && console.log(body)
|
||||
// obj.requestObj = null
|
||||
// obj.cancelFn = null
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
resolve(resp)
|
||||
})
|
||||
// .then(request => {
|
||||
// // obj.requestObj = ro
|
||||
// // if (obj.isCancelled) obj.cancelHttp()
|
||||
// promise.abort = () => {
|
||||
// request.destroy(new Error('cancelled'))
|
||||
// }
|
||||
// })
|
||||
})
|
||||
// let obj = {
|
||||
// isCancelled: false,
|
||||
// cancelHttp: () => {
|
||||
// if (!obj.requestObj) return obj.isCancelled = true
|
||||
// cancelHttp(obj.requestObj)
|
||||
// obj.requestObj = null
|
||||
// obj.promise = obj.cancelHttp = null
|
||||
// obj.cancelFn(new Error(requestMsg.cancelRequest))
|
||||
// obj.cancelFn = null
|
||||
// },
|
||||
// }
|
||||
// obj.promise = new Promise((resolve, reject) => {
|
||||
// obj.cancelFn = reject
|
||||
// debugRequest && console.log(`\n---send request------${url}------------`)
|
||||
// fetchData(url, options.method, options, (err, resp, body) => {
|
||||
// // options.isShowProgress && window.api.hideProgress()
|
||||
// debugRequest && console.log(`\n---response------${url}------------`)
|
||||
// debugRequest && console.log(body)
|
||||
// obj.requestObj = null
|
||||
// obj.cancelFn = null
|
||||
// if (err) { reject(err); return }
|
||||
// resolve(resp)
|
||||
// }).then(ro => {
|
||||
// obj.requestObj = ro
|
||||
// if (obj.isCancelled) obj.cancelHttp()
|
||||
// })
|
||||
// })
|
||||
// return obj
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求超时自动重试
|
||||
* @param {*} url
|
||||
* @param {*} options
|
||||
*/
|
||||
export const httpFetch = async(url: string, options: RequestOptions = { method: 'get' }) => {
|
||||
return buildHttpPromose(url, options).catch(async(err: any) => {
|
||||
// console.log('出错', err)
|
||||
if (err.message === 'socket hang up') {
|
||||
// window.globalObj.apiSource = 'temp'
|
||||
throw new Error(requestMsg.unachievable)
|
||||
}
|
||||
switch (err.code) {
|
||||
case 'ETIMEDOUT':
|
||||
case 'ESOCKETTIMEDOUT':
|
||||
throw new Error(requestMsg.timeout)
|
||||
case 'ENOTFOUND':
|
||||
throw new Error(requestMsg.notConnectNetwork)
|
||||
default:
|
||||
throw err
|
||||
}
|
||||
})
|
||||
// requestObj.promise = requestObj.promise.catch(async err => {
|
||||
// // console.log('出错', err)
|
||||
// if (err.message === 'socket hang up') {
|
||||
// // window.globalObj.apiSource = 'temp'
|
||||
// return Promise.reject(new Error(requestMsg.unachievable))
|
||||
// }
|
||||
// switch (err.code) {
|
||||
// case 'ETIMEDOUT':
|
||||
// case 'ESOCKETTIMEDOUT':
|
||||
// return Promise.reject(new Error(requestMsg.timeout))
|
||||
// case 'ENOTFOUND':
|
||||
// return Promise.reject(new Error(requestMsg.notConnectNetwork))
|
||||
// default:
|
||||
// return Promise.reject(err)
|
||||
// }
|
||||
// })
|
||||
// return requestPromise
|
||||
}
|
||||
|
||||
const fetchData = async(url: string, method: RequestOptions['method'], {
|
||||
headers = {},
|
||||
format = 'json',
|
||||
timeout = 15000,
|
||||
...options
|
||||
}, callback: RequestCallback) => {
|
||||
// console.log(url, options)
|
||||
console.log('---start---', url)
|
||||
headers = Object.assign({}, headers)
|
||||
|
||||
return request(url, {
|
||||
...options,
|
||||
method,
|
||||
headers: Object.assign({}, defaultHeaders, headers),
|
||||
timeout,
|
||||
agent: getRequestAgent(url),
|
||||
json: format === 'json',
|
||||
}, (err, resp, body) => {
|
||||
callback(err, resp, body)
|
||||
})
|
||||
}
|
|
@ -11,6 +11,7 @@
|
|||
<layout-update-modal />
|
||||
<layout-pact-modal />
|
||||
<layout-sync-mode-modal />
|
||||
<layout-sync-auth-code-modal />
|
||||
<layout-play-detail />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
<template>
|
||||
<material-modal :show="sync.isShowAuthCodeModal" :bg-close="false" @close="handleClose" @after-enter="$refs.input.focus()">
|
||||
<main :class="$style.main">
|
||||
<h2>{{ $t('sync__auth_code_title') }}</h2>
|
||||
<base-input
|
||||
ref="input"
|
||||
v-model="authCode"
|
||||
:class="$style.input"
|
||||
:placeholder="$t('sync__auth_code_input_tip')"
|
||||
@submit="handleSubmit" @blur="verify"
|
||||
/>
|
||||
<div :class="$style.footer">
|
||||
<base-btn :class="$style.btn" @click="handleSubmit">{{ $t('btn_confirm') }}</base-btn>
|
||||
</div>
|
||||
</main>
|
||||
</material-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref } from '@common/utils/vueTools'
|
||||
import { sync } from '@renderer/store'
|
||||
import { appSetting } from '@renderer/store/setting'
|
||||
import { sendSyncAction } from '@renderer/utils/ipc'
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const authCode = ref('')
|
||||
const handleClose = () => {
|
||||
sync.isShowAuthCodeModal = false
|
||||
}
|
||||
const verify = () => {
|
||||
if (authCode.value.length > 100) authCode.value = authCode.value.substring(0, 100)
|
||||
return authCode.value
|
||||
}
|
||||
const handleSubmit = () => {
|
||||
let code = verify()
|
||||
if (code == '') return
|
||||
handleClose()
|
||||
sendSyncAction({
|
||||
action: 'enable_client',
|
||||
data: {
|
||||
enable: appSetting['sync.enable'],
|
||||
host: appSetting['sync.client.host'],
|
||||
authCode: code,
|
||||
},
|
||||
})
|
||||
}
|
||||
return {
|
||||
sync,
|
||||
authCode,
|
||||
handleClose,
|
||||
verify,
|
||||
handleSubmit,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
|
||||
.main {
|
||||
padding: 0 15px;
|
||||
max-width: 530px;
|
||||
min-width: 280px;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
min-height: 0;
|
||||
// max-height: 100%;
|
||||
// overflow: hidden;
|
||||
h2 {
|
||||
font-size: 13px;
|
||||
color: var(--color-font);
|
||||
line-height: 1.3;
|
||||
word-break: break-all;
|
||||
// text-align: center;
|
||||
padding: 15px 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
// width: 100%;
|
||||
// height: 26px;
|
||||
padding: 8px 8px;
|
||||
}
|
||||
.footer {
|
||||
margin: 20px 0 15px auto;
|
||||
}
|
||||
.btn {
|
||||
// box-sizing: border-box;
|
||||
// margin-left: 15px;
|
||||
// margin-bottom: 15px;
|
||||
// height: 36px;
|
||||
// line-height: 36px;
|
||||
// padding: 0 10px !important;
|
||||
min-width: 70px;
|
||||
// .mixin-ellipsis-1;
|
||||
|
||||
+.btn {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</style>
|
|
@ -23,7 +23,7 @@
|
|||
<dl :class="$style.btnGroup">
|
||||
<dt :class="$style.label">{{ $t('sync__other_label') }}</dt>
|
||||
<dd :class="$style.btns">
|
||||
<base-btn :class="$style.btn" @click="handleSelectMode('none')">{{ $t('sync__overwrite_btn_none') }}</base-btn>
|
||||
<!-- <base-btn :class="$style.btn" @click="handleSelectMode('none')">{{ $t('sync__overwrite_btn_none') }}</base-btn> -->
|
||||
<base-btn :class="$style.btn" @click="handleSelectMode('cancel')">{{ $t('sync__overwrite_btn_cancel') }}</base-btn>
|
||||
</dd>
|
||||
</dl>
|
||||
|
|
|
@ -32,25 +32,62 @@ export default () => {
|
|||
immediate: true,
|
||||
})
|
||||
|
||||
watch(() => appSetting['sync.mode'], (mode) => {
|
||||
sync.mode = mode
|
||||
})
|
||||
|
||||
watch(() => appSetting['sync.enable'], enable => {
|
||||
void sendSyncAction({
|
||||
action: 'enable',
|
||||
data: {
|
||||
enable,
|
||||
port: appSetting['sync.port'],
|
||||
},
|
||||
})
|
||||
switch (appSetting['sync.mode']) {
|
||||
case 'server':
|
||||
if (appSetting['sync.server.port']) {
|
||||
void sendSyncAction({
|
||||
action: 'enable_server',
|
||||
data: {
|
||||
enable: appSetting['sync.enable'],
|
||||
port: appSetting['sync.server.port'],
|
||||
},
|
||||
})
|
||||
}
|
||||
break
|
||||
case 'client':
|
||||
if (appSetting['sync.client.host']) {
|
||||
void sendSyncAction({
|
||||
action: 'enable_client',
|
||||
data: {
|
||||
enable: appSetting['sync.enable'],
|
||||
host: appSetting['sync.client.host'],
|
||||
},
|
||||
})
|
||||
}
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
sync.enable = enable
|
||||
})
|
||||
watch(() => appSetting['sync.port'], port => {
|
||||
void sendSyncAction({
|
||||
action: 'enable',
|
||||
data: {
|
||||
enable: appSetting['sync.enable'],
|
||||
port: appSetting['sync.port'],
|
||||
},
|
||||
})
|
||||
sync.port = port
|
||||
watch(() => appSetting['sync.server.port'], port => {
|
||||
if (appSetting['sync.mode'] == 'server') {
|
||||
void sendSyncAction({
|
||||
action: 'enable_server',
|
||||
data: {
|
||||
enable: appSetting['sync.enable'],
|
||||
port: appSetting['sync.server.port'],
|
||||
},
|
||||
})
|
||||
}
|
||||
sync.server.port = port
|
||||
})
|
||||
watch(() => appSetting['sync.client.host'], host => {
|
||||
if (appSetting['sync.mode'] == 'client') {
|
||||
void sendSyncAction({
|
||||
action: 'enable_client',
|
||||
data: {
|
||||
enable: appSetting['sync.enable'],
|
||||
host: appSetting['sync.client.host'],
|
||||
},
|
||||
})
|
||||
}
|
||||
sync.client.host = host
|
||||
})
|
||||
|
||||
watch(() => appSetting['network.proxy.enable'], enable => {
|
||||
|
|
|
@ -2,23 +2,33 @@ import { markRaw, onBeforeUnmount } from '@common/utils/vueTools'
|
|||
import { onSyncAction, sendSyncAction } from '@renderer/utils/ipc'
|
||||
import { sync } from '@renderer/store'
|
||||
import { appSetting } from '@renderer/store/setting'
|
||||
import { SYNC_CODE } from '@common/constants'
|
||||
|
||||
export default () => {
|
||||
const handleSyncList = (event: LX.Sync.SyncMainWindowActions) => {
|
||||
console.log(event)
|
||||
switch (event.action) {
|
||||
case 'select_mode':
|
||||
sync.deviceName = event.data.deviceName
|
||||
sync.deviceName = event.data
|
||||
sync.isShowSyncMode = true
|
||||
break
|
||||
case 'close_select_mode':
|
||||
sync.isShowSyncMode = false
|
||||
break
|
||||
case 'status':
|
||||
sync.status.status = event.data.status
|
||||
sync.status.message = event.data.message
|
||||
sync.status.address = markRaw(event.data.address)
|
||||
sync.status.code = event.data.code
|
||||
sync.status.devices = markRaw(event.data.devices)
|
||||
case 'server_status':
|
||||
sync.server.status.status = event.data.status
|
||||
sync.server.status.message = event.data.message
|
||||
sync.server.status.address = markRaw(event.data.address)
|
||||
sync.server.status.code = event.data.code
|
||||
sync.server.status.devices = markRaw(event.data.devices)
|
||||
break
|
||||
case 'client_status':
|
||||
sync.client.status.status = event.data.status
|
||||
sync.client.status.message = event.data.message
|
||||
sync.client.status.address = markRaw(event.data.address)
|
||||
if (event.data.message == SYNC_CODE.missingAuthCode || event.data.message == SYNC_CODE.authFailed) {
|
||||
if (!sync.isShowAuthCodeModal) sync.isShowAuthCodeModal = true
|
||||
} else if (sync.isShowAuthCodeModal) sync.isShowAuthCodeModal = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
@ -33,15 +43,36 @@ export default () => {
|
|||
|
||||
return async() => {
|
||||
sync.enable = appSetting['sync.enable']
|
||||
sync.port = appSetting['sync.port']
|
||||
if (appSetting['sync.enable'] && appSetting['sync.port']) {
|
||||
void sendSyncAction({
|
||||
action: 'enable',
|
||||
data: {
|
||||
enable: appSetting['sync.enable'],
|
||||
port: appSetting['sync.port'],
|
||||
},
|
||||
})
|
||||
sync.mode = appSetting['sync.mode']
|
||||
sync.server.port = appSetting['sync.server.port']
|
||||
sync.client.host = appSetting['sync.client.host']
|
||||
if (appSetting['sync.enable']) {
|
||||
switch (appSetting['sync.mode']) {
|
||||
case 'server':
|
||||
if (appSetting['sync.server.port']) {
|
||||
void sendSyncAction({
|
||||
action: 'enable_server',
|
||||
data: {
|
||||
enable: appSetting['sync.enable'],
|
||||
port: appSetting['sync.server.port'],
|
||||
},
|
||||
})
|
||||
}
|
||||
break
|
||||
case 'client':
|
||||
if (appSetting['sync.client.host']) {
|
||||
void sendSyncAction({
|
||||
action: 'enable_client',
|
||||
data: {
|
||||
enable: appSetting['sync.enable'],
|
||||
host: appSetting['sync.client.host'],
|
||||
},
|
||||
})
|
||||
}
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,27 +27,51 @@ export const proxy: {
|
|||
}
|
||||
export const sync: {
|
||||
enable: boolean
|
||||
port: string
|
||||
mode: LX.AppSetting['sync.mode']
|
||||
isShowSyncMode: boolean
|
||||
isShowAuthCodeModal: boolean
|
||||
deviceName: string
|
||||
status: {
|
||||
status: boolean
|
||||
message: string
|
||||
address: string[]
|
||||
code: string
|
||||
devices: LX.Sync.KeyInfo[]
|
||||
server: {
|
||||
port: string
|
||||
status: {
|
||||
status: boolean
|
||||
message: string
|
||||
address: string[]
|
||||
code: string
|
||||
devices: LX.Sync.ServerKeyInfo[]
|
||||
}
|
||||
}
|
||||
client: {
|
||||
host: string
|
||||
status: {
|
||||
status: boolean
|
||||
message: string
|
||||
address: string[]
|
||||
}
|
||||
}
|
||||
} = reactive({
|
||||
enable: false,
|
||||
port: '',
|
||||
mode: 'server',
|
||||
isShowSyncMode: false,
|
||||
isShowAuthCodeModal: false,
|
||||
deviceName: '',
|
||||
status: {
|
||||
status: false,
|
||||
message: '',
|
||||
address: [],
|
||||
code: '',
|
||||
devices: [],
|
||||
server: {
|
||||
port: '',
|
||||
status: {
|
||||
status: false,
|
||||
message: '',
|
||||
address: [],
|
||||
code: '',
|
||||
devices: [],
|
||||
},
|
||||
},
|
||||
client: {
|
||||
host: '',
|
||||
status: {
|
||||
status: false,
|
||||
message: '',
|
||||
address: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
@ -4,18 +4,37 @@ dt#sync
|
|||
button(class="help-btn" @click="openUrl('https://lyswhut.github.io/lx-music-doc/desktop/faq/sync')" :aria-label="$t('setting__sync_tip')")
|
||||
svg-icon(name="help-circle-outline")
|
||||
dd
|
||||
base-checkbox(id="setting_sync_enable" :modelValue="appSetting['sync.enable']" @update:modelValue="updateSetting({ 'sync.enable': $event })" :label="syncEnableTitle")
|
||||
div
|
||||
p.small {{$t('setting__sync_auth_code', { code: sync.status.code || '' })}}
|
||||
p.small {{$t('setting__sync_address', { address: sync.status.address.join(', ') || '' })}}
|
||||
p.small {{$t('setting__sync_device', { devices: syncDevices })}}
|
||||
p
|
||||
base-btn.btn(min :disabled="!sync.status.status" @click="refreshSyncCode") {{$t('setting__sync_refresh_code')}}
|
||||
base-checkbox(id="setting_sync_enable" :modelValue="appSetting['sync.enable']" @update:modelValue="updateSetting({ 'sync.enable': $event })" :label="$t('setting__sync_enable')")
|
||||
|
||||
dd
|
||||
h3#sync_port {{$t('setting__sync_port')}}
|
||||
h3#sync_mode {{$t('setting__sync_mode')}}
|
||||
div
|
||||
base-checkbox.gap-left(id="setting_sync_mode_server" :disabled="sync.enable" :modelValue="appSetting['sync.mode']" @update:modelValue="updateSetting({ 'sync.mode': $event })" need value="server" :label="$t('setting__sync_mode_server')")
|
||||
base-checkbox.gap-left(id="setting_sync_mode_client" :disabled="sync.enable" :modelValue="appSetting['sync.mode']" @update:modelValue="updateSetting({ 'sync.mode': $event })" need value="client" :label="$t('setting__sync_mode_client')")
|
||||
|
||||
|
||||
dd(v-if="sync.mode == 'client'")
|
||||
h3 {{$t('setting__sync_client_mode')}}
|
||||
div
|
||||
p.small {{$t('setting__sync_client_status', { status: clientStatus })}}
|
||||
p.small {{$t('setting__sync_client_address', { address: sync.client.status.address.join(', ') || '' })}}
|
||||
p
|
||||
base-input.gap-left(:modelValue="appSetting['sync.port']" type="number" @update:modelValue="setSyncPort" :placeholder="$t('setting__sync_port_tip')")
|
||||
p.small {{$t('setting__sync_client_host')}}
|
||||
div
|
||||
base-input.gap-left(:class="$style.hostInput" :modelValue="appSetting['sync.client.host']" :disabled="sync.enable" @update:modelValue="setSyncClientHost" :placeholder="$t('setting__sync_client_host_tip')")
|
||||
dd(v-else)
|
||||
h3 {{syncEnableServerTitle}}
|
||||
div
|
||||
p.small {{$t('setting__sync_server_auth_code', { code: sync.server.status.code || '' })}}
|
||||
p.small {{$t('setting__sync_server_address', { address: sync.server.status.address.join(', ') || '' })}}
|
||||
p.small {{$t('setting__sync_server_device', { devices: syncDevices })}}
|
||||
p
|
||||
base-btn.btn(min :disabled="!sync.server.status.status" @click="refreshSyncCode") {{$t('setting__sync_server_refresh_code')}}
|
||||
p
|
||||
p.small {{$t('setting__sync_server_port')}}
|
||||
div
|
||||
base-input.gap-left(:class="$style.portInput" :modelValue="appSetting['sync.server.port']" :disabled="sync.enable" type="number" @update:modelValue="setSyncServerPort" :placeholder="$t('setting__sync_server_port_tip')")
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -26,6 +45,7 @@ import { openUrl } from '@common/utils/electron'
|
|||
import { useI18n } from '@renderer/plugins/i18n'
|
||||
import { appSetting, updateSetting } from '@renderer/store/setting'
|
||||
import { debounce } from '@common/utils/common'
|
||||
import { SYNC_CODE } from '@common/constants'
|
||||
|
||||
export default {
|
||||
name: 'SettingSync',
|
||||
|
@ -33,19 +53,39 @@ export default {
|
|||
const t = useI18n()
|
||||
|
||||
|
||||
const syncEnableTitle = computed(() => {
|
||||
let title = t('setting__sync_enable')
|
||||
if (sync.status.message) {
|
||||
title += ` [${sync.status.message}]`
|
||||
const syncEnableServerTitle = computed(() => {
|
||||
let title = t('setting__sync_server_mode')
|
||||
if (sync.server.status.message) {
|
||||
title += ` [${sync.server.status.message}]`
|
||||
}
|
||||
// else if (this.sync.status.address.length) {
|
||||
// // title += ` [${this.sync.status.address.join(', ')}]`
|
||||
// else if (this.sync.server.status.address.length) {
|
||||
// // title += ` [${this.sync.server.status.address.join(', ')}]`
|
||||
// }
|
||||
return title
|
||||
})
|
||||
|
||||
const clientStatus = computed(() => {
|
||||
let status
|
||||
switch (sync.client.status.message) {
|
||||
case SYNC_CODE.msgBlockedIp:
|
||||
status = t('setting__sync_code_blocked_ip')
|
||||
break
|
||||
case SYNC_CODE.authFailed:
|
||||
status = t('setting__sync_code_fail')
|
||||
break
|
||||
default:
|
||||
status = sync.client.status.message
|
||||
? sync.client.status.message
|
||||
: sync.client.status.status
|
||||
? t('setting_sync_status_enabled')
|
||||
: t('sync_status_disabled')
|
||||
break
|
||||
}
|
||||
return status
|
||||
})
|
||||
const syncDevices = computed(() => {
|
||||
return sync.status.devices.length
|
||||
? sync.status.devices.map(d => `${d.deviceName} (${d.clientId.substring(0, 5)})`).join(', ')
|
||||
return sync.server.status.devices.length
|
||||
? sync.server.status.devices.map(d => `${d.deviceName} (${d.clientId.substring(0, 5)})`).join(', ')
|
||||
: ''
|
||||
})
|
||||
|
||||
|
@ -53,8 +93,12 @@ export default {
|
|||
sendSyncAction({ action: 'generate_code' })
|
||||
}
|
||||
|
||||
const setSyncPort = debounce(port => {
|
||||
updateSetting({ 'sync.port': port.trim() })
|
||||
const setSyncServerPort = debounce(port => {
|
||||
updateSetting({ 'sync.server.port': port.trim() })
|
||||
}, 500)
|
||||
|
||||
const setSyncClientHost = debounce(host => {
|
||||
updateSetting({ 'sync.client.host': host.trim() })
|
||||
}, 500)
|
||||
|
||||
|
||||
|
@ -62,10 +106,12 @@ export default {
|
|||
appSetting,
|
||||
updateSetting,
|
||||
sync,
|
||||
syncEnableTitle,
|
||||
setSyncPort,
|
||||
syncEnableServerTitle,
|
||||
setSyncServerPort,
|
||||
setSyncClientHost,
|
||||
syncDevices,
|
||||
refreshSyncCode,
|
||||
clientStatus,
|
||||
openUrl,
|
||||
}
|
||||
},
|
||||
|
@ -76,4 +122,12 @@ export default {
|
|||
.save-path {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.portInput[disabled], .hostInput[disabled] {
|
||||
opacity: .8 !important;
|
||||
}
|
||||
|
||||
.hostInput {
|
||||
min-width: 380px;
|
||||
}
|
||||
</style>
|
||||
|
|
Loading…
Reference in New Issue