diff --git a/publish/changeLog.md b/publish/changeLog.md index 3548e27e..bf5ce147 100644 --- a/publish/changeLog.md +++ b/publish/changeLog.md @@ -31,3 +31,4 @@ ### 其他 - 更新 electron 到 v22.3.21 +- 重构同步服务端功能部分代码,使其更易扩展新功能 diff --git a/src/common/types/list.d.ts b/src/common/types/list.d.ts index f8ed4a9b..085c9eb3 100644 --- a/src/common/types/list.d.ts +++ b/src/common/types/list.d.ts @@ -140,5 +140,18 @@ declare namespace LX { tempList: LX.Music.MusicInfo[] } + type ActionList = SyncAction<'list_data_overwrite', LX.List.ListActionDataOverwrite> + | SyncAction<'list_create', LX.List.ListActionAdd> + | SyncAction<'list_remove', LX.List.ListActionRemove> + | SyncAction<'list_update', LX.List.ListActionUpdate> + | SyncAction<'list_update_position', LX.List.ListActionUpdatePosition> + | SyncAction<'list_music_add', LX.List.ListActionMusicAdd> + | SyncAction<'list_music_move', LX.List.ListActionMusicMove> + | SyncAction<'list_music_remove', LX.List.ListActionMusicRemove> + | SyncAction<'list_music_update', LX.List.ListActionMusicUpdate> + | SyncAction<'list_music_update_position', LX.List.ListActionMusicUpdatePosition> + | SyncAction<'list_music_overwrite', LX.List.ListActionMusicOverwrite> + | SyncAction<'list_music_clear', LX.List.ListActionMusicClear> + } } diff --git a/src/common/types/sync.d.ts b/src/common/types/sync.d.ts index db53623e..7856b50c 100644 --- a/src/common/types/sync.d.ts +++ b/src/common/types/sync.d.ts @@ -24,7 +24,7 @@ declare namespace LX { | SyncAction<'client_status', ClientStatus> | SyncAction<'server_status', ServerStatus> - type SyncServiceActions = SyncAction<'select_mode', Mode> + type SyncServiceActions = SyncAction<'select_mode', ListSyncMode> | SyncAction<'get_server_status'> | SyncAction<'get_client_status'> | SyncAction<'generate_code'> @@ -90,14 +90,17 @@ declare namespace LX { clientId: string key: string deviceName: string - lastSyncDate?: number - snapshotKey: string + lastConnectDate?: number isMobile: boolean } + interface ListInfo { + lastSyncDate?: number + snapshotKey: string + } type ListData = Omit - type Mode = 'merge_local_remote' + type ListSyncMode = 'merge_local_remote' | 'merge_remote_local' | 'overwrite_local_remote' | 'overwrite_remote_local' diff --git a/src/common/types/sync_common.d.ts b/src/common/types/sync_common.d.ts index ad82a458..8c27c822 100644 --- a/src/common/types/sync_common.d.ts +++ b/src/common/types/sync_common.d.ts @@ -1,26 +1,13 @@ declare namespace LX { namespace Sync { - type ActionList = SyncAction<'list_data_overwrite', LX.List.ListActionDataOverwrite> - | SyncAction<'list_create', LX.List.ListActionAdd> - | SyncAction<'list_remove', LX.List.ListActionRemove> - | SyncAction<'list_update', LX.List.ListActionUpdate> - | SyncAction<'list_update_position', LX.List.ListActionUpdatePosition> - | SyncAction<'list_music_add', LX.List.ListActionMusicAdd> - | SyncAction<'list_music_move', LX.List.ListActionMusicMove> - | SyncAction<'list_music_remove', LX.List.ListActionMusicRemove> - | SyncAction<'list_music_update', LX.List.ListActionMusicUpdate> - | SyncAction<'list_music_update_position', LX.List.ListActionMusicUpdatePosition> - | SyncAction<'list_music_overwrite', LX.List.ListActionMusicOverwrite> - | SyncAction<'list_music_clear', LX.List.ListActionMusicClear> - type ServerActions = WarpPromiseRecord<{ - onListSyncAction: (action: LX.Sync.ActionList) => void + onListSyncAction: (action: LX.List.ActionList) => void }> type ClientActions = WarpPromiseRecord<{ - onListSyncAction: (action: LX.Sync.ActionList) => void + onListSyncAction: (action: LX.List.ActionList) => void list_sync_get_md5: () => string - list_sync_get_sync_mode: () => Mode + list_sync_get_sync_mode: () => ListSyncMode list_sync_get_list_data: () => ListData list_sync_set_list_data: (data: ListData) => void list_sync_finished: () => void diff --git a/src/main/modules/sync/client/auth.ts b/src/main/modules/sync/client/auth.ts index c17b448c..6f9afcab 100644 --- a/src/main/modules/sync/client/auth.ts +++ b/src/main/modules/sync/client/auth.ts @@ -1,5 +1,5 @@ import { request, generateRsaKey } from './utils' -import { getSyncAuthKey, setSyncAuthKey } from '../data' +import { getSyncAuthKey, setSyncAuthKey } from './data' import { SYNC_CODE } from '@common/constants' import log from '../log' import { aesDecrypt, aesEncrypt, getComputerName, rsaDecrypt } from '../utils' diff --git a/src/main/modules/sync/client/data.ts b/src/main/modules/sync/client/data.ts new file mode 100644 index 00000000..63916e23 --- /dev/null +++ b/src/main/modules/sync/client/data.ts @@ -0,0 +1,76 @@ +import fs from 'node:fs' +import path from 'node:path' +import { File } from '../constants' + + +let syncAuthKeys: Record + + +const saveSyncAuthKeys = async() => { + const syncAuthKeysFilePath = path.join(global.lxDataPath, File.clientDataPath, File.syncAuthKeysJSON) + return fs.promises.writeFile(syncAuthKeysFilePath, JSON.stringify(syncAuthKeys), 'utf8') +} + +const exists = async(path: string) => fs.promises.stat(path).then(() => true).catch(() => false) +export const initClientInfo = async() => { + if (syncAuthKeys != null) return + const syncAuthKeysFilePath = path.join(global.lxDataPath, File.clientDataPath, File.syncAuthKeysJSON) + if (await fs.promises.stat(syncAuthKeysFilePath).then(() => true).catch(() => false)) { + syncAuthKeys = JSON.parse((await fs.promises.readFile(syncAuthKeysFilePath)).toString()) + } else { + syncAuthKeys = {} + const syncDataPath = path.join(global.lxDataPath, File.clientDataPath) + if (!await exists(syncDataPath)) { + await fs.promises.mkdir(syncDataPath, { recursive: true }) + } + void saveSyncAuthKeys() + } +} + +export const getSyncAuthKey = async(serverId: string) => { + await initClientInfo() + return syncAuthKeys[serverId] ?? null +} +export const setSyncAuthKey = async(serverId: string, info: LX.Sync.ClientKeyInfo) => { + await initClientInfo() + syncAuthKeys[serverId] = info + void saveSyncAuthKeys() +} + +// 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) +// } diff --git a/src/main/modules/sync/client/index.ts b/src/main/modules/sync/client/index.ts index bca80ee4..6ff494a1 100644 --- a/src/main/modules/sync/client/index.ts +++ b/src/main/modules/sync/client/index.ts @@ -4,6 +4,7 @@ import { connect as socketConnect, disconnect as socketDisconnect, sendSyncStatu import { SYNC_CODE } from '@common/constants' import log from '../log' import { parseUrl } from './utils' +import migrateData from '../migrate' let connectId = 0 @@ -29,6 +30,8 @@ const connectServer = async(host: string, authCode?: string) => { message: SYNC_CODE.connecting, }) const id = connectId + await migrateData(global.lxDataPath) + return handleConnect(host, authCode).catch(async err => { if (id != connectId) return sendSyncStatus({ diff --git a/src/main/modules/sync/client/modules/list/handler.ts b/src/main/modules/sync/client/modules/list/handler.ts index 72db2ac3..f0e52b3f 100644 --- a/src/main/modules/sync/client/modules/list/handler.ts +++ b/src/main/modules/sync/client/modules/list/handler.ts @@ -21,7 +21,7 @@ export const list_sync_get_md5 = async(socket: LX.Sync.Client.Socket) => { return toMD5(JSON.stringify(await getLocalListData())) } -const getSyncMode = async(socket: LX.Sync.Client.Socket): Promise => new Promise((resolve, reject) => { +const getSyncMode = async(socket: LX.Sync.Client.Socket): Promise => new Promise((resolve, reject) => { const handleDisconnect = (err: Error) => { sendCloseSelectMode() removeSelectModeListener() diff --git a/src/main/modules/sync/constants.ts b/src/main/modules/sync/constants.ts new file mode 100644 index 00000000..3cbee460 --- /dev/null +++ b/src/main/modules/sync/constants.ts @@ -0,0 +1,66 @@ +export const ENV_PARAMS = [ + 'PORT', + 'BIND_IP', + 'CONFIG_PATH', + 'LOG_PATH', + 'DATA_PATH', + 'PROXY_HEADER', + 'MAX_SNAPSHOT_NUM', + 'LIST_ADD_MUSIC_LOCATION_TYPE', + 'LX_USER_', +] as const + + +export const LIST_IDS = { + DEFAULT: 'default', + LOVE: 'love', + TEMP: 'temp', + DOWNLOAD: 'download', + PLAY_LATER: null, +} as const + +export const SYNC_CODE = { + helloMsg: 'Hello~::^-^::~v4~', + idPrefix: 'OjppZDo6', + authMsg: 'lx-music auth::', + msgAuthFailed: 'Auth failed', + msgBlockedIp: 'Blocked IP', + msgConnect: 'lx-music connect', + + + authFailed: 'Auth failed', + missingAuthCode: 'Missing auth code', + getServiceIdFailed: 'Get service id failed', + connectServiceFailed: 'Connect service failed', + connecting: 'Connecting...', + unknownServiceAddress: 'Unknown service address', +} as const + +export const SYNC_CLOSE_CODE = { + normal: 1000, + failed: 4100, +} as const + +export const TRANS_MODE: Readonly> = { + merge_local_remote: 'merge_remote_local', + merge_remote_local: 'merge_local_remote', + overwrite_local_remote: 'overwrite_remote_local', + overwrite_remote_local: 'overwrite_local_remote', + overwrite_local_remote_full: 'overwrite_remote_local_full', + overwrite_remote_local_full: 'overwrite_local_remote_full', + cancel: 'cancel', +} as const + +export const File = { + serverDataPath: 'sync/server', + clientDataPath: 'sync/client', + + serverInfoJSON: 'serverInfo.json', + userDir: 'users', + userDevicesJSON: 'devices.json', + listDir: 'list', + listSnapshotDir: 'snapshot', + listSnapshotInfoJSON: 'snapshotInfo.json', + + syncAuthKeysJSON: 'syncAuthKey.json', +} as const diff --git a/src/main/modules/sync/data.ts b/src/main/modules/sync/data.ts deleted file mode 100644 index efc0c120..00000000 --- a/src/main/modules/sync/data.ts +++ /dev/null @@ -1,185 +0,0 @@ -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 | 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 = (store.get('syncAuthKey') as Record | 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 - 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('clients', 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') - const store = getStore(STORE_NAMES.SYNC) - store.set('serverId', devicesInfo.serverId) - } - const devices = store.get('clients') as DevicesInfo['clients'] | undefined - if (devices) devicesInfo.clients = devices - deviceKeys = Object.values(devicesInfo.clients).map(device => device.snapshotKey).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 - if (!devicesInfo.serverId) initDeviceInfo() - 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) => { - console.log('getSnapshot', name) - 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) => { - console.log('saveSnapshot', name) - 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) => { - console.log('removeSnapshot', name) - const filePath = path.join(global.lxDataPath, `snapshot_${name}`) - return fs.promises.unlink(filePath).catch((err) => { - log.error(err) - }) -} diff --git a/src/main/modules/sync/migrate.ts b/src/main/modules/sync/migrate.ts new file mode 100644 index 00000000..a5fa87d2 --- /dev/null +++ b/src/main/modules/sync/migrate.ts @@ -0,0 +1,77 @@ +import { File } from './constants' +import fs from 'node:fs' +import path from 'node:path' + +interface ServerKeyInfo { + clientId: string + key: string + deviceName: string + lastSyncDate?: number + snapshotKey?: string + lastConnectDate?: number + isMobile: boolean +} + + +const exists = async(path: string) => fs.promises.stat(path).then(() => true).catch(() => false) + +// 迁移 v2 sync 数据 +export default async(dataPath: string) => { + const syncDataPath = path.join(dataPath, 'sync') + // console.log(syncDataPath) + if (await exists(syncDataPath)) return + const oldInfoPath = path.join(dataPath, 'sync.json') + // console.log(oldInfoPath) + if (!await exists(oldInfoPath)) return + const serverSyncDataPath = path.join(dataPath, File.serverDataPath) + const clientSyncDataPath = path.join(dataPath, File.clientDataPath) + + await fs.promises.mkdir(serverSyncDataPath, { recursive: true }) + await fs.promises.mkdir(clientSyncDataPath, { recursive: true }) + const info = JSON.parse((await fs.promises.readFile(oldInfoPath)).toString()) + + + const serverInfoPath = path.join(serverSyncDataPath, File.serverInfoJSON) + const devicesInfoPath = path.join(serverSyncDataPath, File.userDevicesJSON) + const listDir = path.join(serverSyncDataPath, File.listDir) + await fs.promises.mkdir(listDir) + + + const snapshotInfo = info.snapshotInfo + delete info.snapshotInfo + snapshotInfo.clients = {} + for (const device of Object.values(info.clients)) { + snapshotInfo.clients[device.clientId] = { + snapshotKey: device.snapshotKey, + lastSyncDate: device.lastSyncDate, + } + device.lastConnectDate = device.lastSyncDate + delete device.lastSyncDate + delete device.snapshotKey + } + const devicesInfo = { + userName: 'default', + clients: info.clients, + } + await fs.promises.writeFile(serverInfoPath, JSON.stringify({ serverId: info.serverId, version: 2 })) + await fs.promises.writeFile(devicesInfoPath, JSON.stringify(devicesInfo)) + await fs.promises.writeFile(path.join(listDir, File.listSnapshotInfoJSON), JSON.stringify(snapshotInfo)) + + const snapshotPath = path.join(listDir, File.listSnapshotDir) + await fs.promises.mkdir(snapshotPath) + const snapshots = (await fs.promises.readdir(dataPath)).filter(name => name.startsWith('snapshot_')) + if (snapshots.length) { + for (const file of snapshots) { + await fs.promises.copyFile(path.join(dataPath, file), path.join(snapshotPath, file)) + } + } + + + await fs.promises.writeFile(path.join(clientSyncDataPath, File.syncAuthKeysJSON), JSON.stringify(info.syncAuthKey)) + + for (const file of snapshots) { + await fs.promises.unlink(path.join(dataPath, file)) + } + await fs.promises.unlink(oldInfoPath) +} + diff --git a/src/main/modules/sync/server/index.ts b/src/main/modules/sync/server/index.ts index 269daa83..a1d46046 100644 --- a/src/main/modules/sync/server/index.ts +++ b/src/main/modules/sync/server/index.ts @@ -1,4 +1,3 @@ -import * as modules from './modules' import { startServer, stopServer, @@ -12,5 +11,4 @@ export { stopServer, getStatus, generateCode, - modules, } diff --git a/src/main/modules/sync/server/modules/index.ts b/src/main/modules/sync/server/modules/index.ts index e484eb63..6d952960 100644 --- a/src/main/modules/sync/server/modules/index.ts +++ b/src/main/modules/sync/server/modules/index.ts @@ -1,10 +1,12 @@ -import * as list from './list' -// export * as theme from './theme' - - -export const callObj = Object.assign({}, list.handler) +import { sync } from './list' +export const callObj = Object.assign({}, + sync.handler, +) export const modules = { - list, + list: sync, } + + +export { ListManage } from './list' diff --git a/src/main/modules/sync/server/modules/list/handler.ts b/src/main/modules/sync/server/modules/list/handler.ts deleted file mode 100644 index 6376b9b1..00000000 --- a/src/main/modules/sync/server/modules/list/handler.ts +++ /dev/null @@ -1,86 +0,0 @@ -// import { throttle } from '@common/utils/common' -// import { sendSyncActionList } from '@main/modules/winMain' -// import { SYNC_CLOSE_CODE } from '@common/constants' -import { updateDeviceSnapshotKey } from '@main/modules/sync/data' -import { handleRemoteListAction } from '../../../utils' -import { createSnapshot } 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 }) -// const clients = Array.from(wss.clients).filter(socket => !excludeIds.includes(socket.keyInfo.clientId) && socket.isReady) -// if (!clients.length) return -// const enData = await encryptMsg(null, dataStr) -// for (const socket of clients) { -// if (excludeIds.includes(socket.keyInfo.clientId) || !socket.isReady) continue -// socket.send(enData, (err) => { -// if (err) { -// socket.close(SYNC_CLOSE_CODE.failed) -// return -// } -// updateDeviceSnapshotKey(socket.keyInfo, key) -// }) -// } -// } - -// const sendListAction = async(action: LX.Sync.ActionList) => { -// console.log('sendListAction', action.action) -// // io.sockets -// await broadcast(await getCurrentListInfoKey(), action) -// } - -// export const registerListHandler = (_wss: LX.Sync.Server.SocketServer, socket: LX.Sync.Server.Socket) => { -// if (!wss) { -// wss = _wss -// removeListener = registerListActionEvent(sendListAction) -// } - -// 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 -// } -// } - -export const onListSyncAction = async(socket: LX.Sync.Server.Socket, action: LX.Sync.ActionList) => { - await handleRemoteListAction(action).then(updated => { - if (!updated) return - console.log(updated) - void createSnapshot().then(key => { - if (!key) return - updateDeviceSnapshotKey(socket.keyInfo, key) - const currentId = socket.keyInfo.clientId - socket.broadcast((client) => { - if (client.keyInfo.clientId == currentId || !client.isReady) return - void client.remoteSyncList.onListSyncAction(action) - }) - }) - }) -} diff --git a/src/main/modules/sync/server/modules/list/index.ts b/src/main/modules/sync/server/modules/list/index.ts index 59c43c8e..68ee8f0a 100644 --- a/src/main/modules/sync/server/modules/list/index.ts +++ b/src/main/modules/sync/server/modules/list/index.ts @@ -1,4 +1,3 @@ -export * as handler from './handler' -export { default as sync } from './sync' +export * as sync from './sync' +export { ListManage } from './manage' -export * from './localEvent' diff --git a/src/main/modules/sync/server/modules/list/manage.ts b/src/main/modules/sync/server/modules/list/manage.ts new file mode 100644 index 00000000..c036db78 --- /dev/null +++ b/src/main/modules/sync/server/modules/list/manage.ts @@ -0,0 +1,51 @@ +import { type UserDataManage } from '../../user' +import { SnapshotDataManage } from './snapshotDataManage' +import { toMD5 } from '../../utils' +import { getLocalListData } from '@main/modules/sync/utils' + +export class ListManage { + snapshotDataManage: SnapshotDataManage + + constructor(userDataManage: UserDataManage) { + this.snapshotDataManage = new SnapshotDataManage(userDataManage) + } + + createSnapshot = async() => { + const listData = JSON.stringify(await this.getListData()) + const md5 = toMD5(listData) + const snapshotInfo = await this.snapshotDataManage.getSnapshotInfo() + console.log(md5, snapshotInfo.latest) + if (snapshotInfo.latest == md5) return md5 + if (snapshotInfo.list.includes(md5)) { + snapshotInfo.list.splice(snapshotInfo.list.indexOf(md5), 1) + } else await this.snapshotDataManage.saveSnapshot(md5, listData) + if (snapshotInfo.latest) snapshotInfo.list.unshift(snapshotInfo.latest) + snapshotInfo.latest = md5 + snapshotInfo.time = Date.now() + this.snapshotDataManage.saveSnapshotInfo(snapshotInfo) + return md5 + } + + getCurrentListInfoKey = async() => { + const snapshotInfo = await this.snapshotDataManage.getSnapshotInfo() + if (snapshotInfo.latest) { + return snapshotInfo.latest + } + snapshotInfo.latest = toMD5(JSON.stringify(await this.getListData())) + this.snapshotDataManage.saveSnapshotInfo(snapshotInfo) + return snapshotInfo.latest + } + + getDeviceCurrentSnapshotKey = async(clientId: string) => { + return this.snapshotDataManage.getDeviceCurrentSnapshotKey(clientId) + } + + updateDeviceSnapshotKey = async(clientId: string, key: string) => { + await this.snapshotDataManage.updateDeviceSnapshotKey(clientId, key) + } + + getListData = async(): Promise => { + return getLocalListData() + } +} + diff --git a/src/main/modules/sync/server/modules/list/snapshotDataManage.ts b/src/main/modules/sync/server/modules/list/snapshotDataManage.ts new file mode 100644 index 00000000..978f66df --- /dev/null +++ b/src/main/modules/sync/server/modules/list/snapshotDataManage.ts @@ -0,0 +1,134 @@ +import { throttle } from '@common/utils/common' +import fs from 'node:fs' +import path from 'node:path' +import syncLog from '../../../log' +import { getUserConfig, type UserDataManage } from '../../user/data' +import { File } from '../../../constants' +import { checkAndCreateDirSync } from '../../utils' + + +interface SnapshotInfo { + latest: string | null + time: number + list: string[] + clients: Record +} +export class SnapshotDataManage { + userDataManage: UserDataManage + listDir: string + snapshotDir: string + snapshotInfoFilePath: string + snapshotInfo: SnapshotInfo + clientSnapshotKeys: string[] + private readonly saveSnapshotInfoThrottle: () => void + + isIncluedsDevice = (key: string) => { + return this.clientSnapshotKeys.includes(key) + } + + clearOldSnapshot = async() => { + if (!this.snapshotInfo) return + const snapshotList = this.snapshotInfo.list.filter(key => !this.isIncluedsDevice(key)) + // console.log(snapshotList.length, lx.config.maxSnapshotNum) + const userMaxSnapshotNum = getUserConfig(this.userDataManage.userName).maxSnapshotNum + let requiredSave = snapshotList.length > userMaxSnapshotNum + while (snapshotList.length > userMaxSnapshotNum) { + const name = snapshotList.pop() + if (name) { + await this.removeSnapshot(name) + this.snapshotInfo.list.splice(this.snapshotInfo.list.indexOf(name), 1) + } else break + } + if (requiredSave) this.saveSnapshotInfo(this.snapshotInfo) + } + + updateDeviceSnapshotKey = async(clientId: string, key: string) => { + // console.log('updateDeviceSnapshotKey', key) + let client = this.snapshotInfo.clients[clientId] + if (!client) client = this.snapshotInfo.clients[clientId] = { snapshotKey: '', lastSyncDate: 0 } + if (client.snapshotKey) this.clientSnapshotKeys.splice(this.clientSnapshotKeys.indexOf(client.snapshotKey), 1) + client.snapshotKey = key + client.lastSyncDate = Date.now() + this.clientSnapshotKeys.push(key) + this.saveSnapshotInfoThrottle() + } + + getDeviceCurrentSnapshotKey = async(clientId: string) => { + // console.log('updateDeviceSnapshotKey', key) + const client = this.snapshotInfo.clients[clientId] + return client?.snapshotKey + } + + getSnapshotInfo = async(): Promise => { + return this.snapshotInfo + } + + saveSnapshotInfo = (info: SnapshotInfo) => { + this.snapshotInfo = info + this.saveSnapshotInfoThrottle() + } + + getSnapshot = async(name: string) => { + const filePath = path.join(this.snapshotDir, `snapshot_${name}`) + let listData: LX.Sync.ListData + try { + listData = JSON.parse((await fs.promises.readFile(filePath)).toString('utf-8')) + } catch (err) { + syncLog.warn(err) + return null + } + return listData + } + + saveSnapshot = async(name: string, data: string) => { + syncLog.info('saveSnapshot', this.userDataManage.userName, name) + const filePath = path.join(this.snapshotDir, `snapshot_${name}`) + try { + await fs.promises.writeFile(filePath, data) + } catch (err) { + syncLog.error(err) + throw err + } + } + + removeSnapshot = async(name: string) => { + syncLog.info('removeSnapshot', this.userDataManage.userName, name) + const filePath = path.join(this.snapshotDir, `snapshot_${name}`) + try { + await fs.promises.unlink(filePath) + } catch (err) { + syncLog.error(err) + } + } + + + constructor(userDataManage: UserDataManage) { + this.userDataManage = userDataManage + + this.listDir = path.join(userDataManage.userDir, File.listDir) + checkAndCreateDirSync(this.listDir) + + this.snapshotDir = path.join(this.listDir, File.listSnapshotDir) + checkAndCreateDirSync(this.snapshotDir) + + this.snapshotInfoFilePath = path.join(this.listDir, File.listSnapshotInfoJSON) + this.snapshotInfo = fs.existsSync(this.snapshotInfoFilePath) + ? JSON.parse(fs.readFileSync(this.snapshotInfoFilePath).toString()) + : { latest: null, time: 0, list: [], clients: {} } + + this.saveSnapshotInfoThrottle = throttle(() => { + fs.writeFile(this.snapshotInfoFilePath, JSON.stringify(this.snapshotInfo), 'utf8', (err) => { + if (err) console.error(err) + void this.clearOldSnapshot() + }) + }) + + this.clientSnapshotKeys = Object.values(this.snapshotInfo.clients).map(device => device.snapshotKey).filter(k => k) + } +} +// type UserDataManages = Map + +// export const createUserDataManage = (user: LX.UserConfig) => { +// const manage = Object.create(userDataManage) as typeof userDataManage +// manage.userDir = user.dataPath +// } diff --git a/src/main/modules/sync/server/modules/list/sync/handler.ts b/src/main/modules/sync/server/modules/list/sync/handler.ts new file mode 100644 index 00000000..8785622d --- /dev/null +++ b/src/main/modules/sync/server/modules/list/sync/handler.ts @@ -0,0 +1,163 @@ +// import { throttle } from '@common/utils/common' +// import { sendSyncActionList } from '@main/modules/winMain' +// import { SYNC_CLOSE_CODE } from '@/constants' +import { SYNC_CLOSE_CODE } from '@main/modules/sync/constants' +import { getUserSpace } from '@main/modules/sync/server/user' +import { handleRemoteListAction } from '@main/modules/sync/utils' +// import { encryptMsg } from '@/utils/tools' + +// let wss: LX.SocketServer | null +// let removeListener: (() => void) | null + +// type listAction = 'list:action' + +// const registerListActionEvent = () => { +// const list_data_overwrite = async(listData: MakeOptional, isRemote: boolean = false) => { +// if (isRemote) return +// await sendListAction({ action: 'list_data_overwrite', data: listData }) +// } +// const list_create = async(position: number, listInfos: LX.List.UserListInfo[], isRemote: boolean = false) => { +// if (isRemote) return +// await sendListAction({ action: 'list_create', data: { position, listInfos } }) +// } +// const list_remove = async(ids: string[], isRemote: boolean = false) => { +// if (isRemote) return +// await sendListAction({ action: 'list_remove', data: ids }) +// } +// const list_update = async(lists: LX.List.UserListInfo[], isRemote: boolean = false) => { +// if (isRemote) return +// await sendListAction({ action: 'list_update', data: lists }) +// } +// const list_update_position = async(position: number, ids: string[], isRemote: boolean = false) => { +// if (isRemote) return +// await sendListAction({ action: 'list_update_position', data: { position, ids } }) +// } +// const list_music_overwrite = async(listId: string, musicInfos: LX.Music.MusicInfo[], isRemote: boolean = false) => { +// if (isRemote) return +// await sendListAction({ action: 'list_music_overwrite', data: { listId, musicInfos } }) +// } +// const list_music_add = async(id: string, musicInfos: LX.Music.MusicInfo[], addMusicLocationType: LX.AddMusicLocationType, isRemote: boolean = false) => { +// if (isRemote) return +// await sendListAction({ action: 'list_music_add', data: { id, musicInfos, addMusicLocationType } }) +// } +// const list_music_move = async(fromId: string, toId: string, musicInfos: LX.Music.MusicInfo[], addMusicLocationType: LX.AddMusicLocationType, isRemote: boolean = false) => { +// if (isRemote) return +// await sendListAction({ action: 'list_music_move', data: { fromId, toId, musicInfos, addMusicLocationType } }) +// } +// const list_music_remove = async(listId: string, ids: string[], isRemote: boolean = false) => { +// if (isRemote) return +// await sendListAction({ action: 'list_music_remove', data: { listId, ids } }) +// } +// const list_music_update = async(musicInfos: LX.List.ListActionMusicUpdate, isRemote: boolean = false) => { +// if (isRemote) return +// await sendListAction({ action: 'list_music_update', data: musicInfos }) +// } +// const list_music_clear = async(ids: string[], isRemote: boolean = false) => { +// if (isRemote) return +// await sendListAction({ action: 'list_music_clear', data: ids }) +// } +// const list_music_update_position = async(listId: string, position: number, ids: string[], isRemote: boolean = false) => { +// if (isRemote) return +// await sendListAction({ action: 'list_music_update_position', data: { listId, position, ids } }) +// } +// global.event_list.on('list_data_overwrite', list_data_overwrite) +// global.event_list.on('list_create', list_create) +// global.event_list.on('list_remove', list_remove) +// global.event_list.on('list_update', list_update) +// global.event_list.on('list_update_position', list_update_position) +// global.event_list.on('list_music_overwrite', list_music_overwrite) +// global.event_list.on('list_music_add', list_music_add) +// global.event_list.on('list_music_move', list_music_move) +// global.event_list.on('list_music_remove', list_music_remove) +// global.event_list.on('list_music_update', list_music_update) +// global.event_list.on('list_music_clear', list_music_clear) +// global.event_list.on('list_music_update_position', list_music_update_position) +// return () => { +// global.event_list.off('list_data_overwrite', list_data_overwrite) +// global.event_list.off('list_create', list_create) +// global.event_list.off('list_remove', list_remove) +// global.event_list.off('list_update', list_update) +// global.event_list.off('list_update_position', list_update_position) +// global.event_list.off('list_music_overwrite', list_music_overwrite) +// global.event_list.off('list_music_add', list_music_add) +// global.event_list.off('list_music_move', list_music_move) +// global.event_list.off('list_music_remove', list_music_remove) +// global.event_list.off('list_music_update', list_music_update) +// global.event_list.off('list_music_clear', list_music_clear) +// global.event_list.off('list_music_update_position', list_music_update_position) +// } +// } + +// const addMusic = (orderId, callback) => { +// // ... +// } + +// const broadcast = async(socket: LX.Socket, key: string, data: any, excludeIds: string[] = []) => { +// if (!wss) return +// const dataStr = JSON.stringify({ action: 'list:sync:action', data }) +// const userSpace = getUserSpace(socket.userInfo.name) +// for (const client of wss.clients) { +// if (excludeIds.includes(client.keyInfo.clientId) || !client.isReady || client.userInfo.name != socket.userInfo.name) continue +// client.send(encryptMsg(client.keyInfo, dataStr), (err) => { +// if (err) { +// client.close(SYNC_CLOSE_CODE.failed) +// return +// } +// userSpace.dataManage.updateDeviceSnapshotKey(client.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.SocketServer, socket: LX.Socket) => { +// if (!wss) { +// wss = _wss +// // removeListener = registerListActionEvent() +// } + +// const userSpace = getUserSpace(socket.userInfo.name) +// socket.onRemoteEvent('list:sync:action', (action) => { +// if (!socket.isReady) return +// // console.log(msg) +// void handleListAction(socket.userInfo.name, action).then(key => { +// if (!key) return +// console.log(key) +// userSpace.dataManage.updateDeviceSnapshotKey(socket.keyInfo, key) +// void broadcast(socket, 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 +// // } +// } + +export const onListSyncAction = async(socket: LX.Sync.Server.Socket, action: LX.Sync.ActionList) => { + const userSpace = getUserSpace(socket.userInfo.name) + await handleRemoteListAction(action).then(async updated => { + if (!updated) { + socket.close(SYNC_CLOSE_CODE.failed) + return + } + const key = await userSpace.listManage.createSnapshot() + userSpace.listManage.updateDeviceSnapshotKey(socket.keyInfo.clientId, key) + const currentUserName = socket.userInfo.name + const currentId = socket.keyInfo.clientId + socket.broadcast((client) => { + if (client.keyInfo.clientId == currentId || !client.isReady || client.userInfo.name != currentUserName) return + void client.remoteSyncList.onListSyncAction(action) + }) + }) +} diff --git a/src/main/modules/sync/server/modules/list/sync/index.ts b/src/main/modules/sync/server/modules/list/sync/index.ts new file mode 100644 index 00000000..2470e4a9 --- /dev/null +++ b/src/main/modules/sync/server/modules/list/sync/index.ts @@ -0,0 +1,3 @@ +export * as handler from './handler' +export { default as sync } from './sync' +export * from './localEvent' diff --git a/src/main/modules/sync/server/modules/list/localEvent.ts b/src/main/modules/sync/server/modules/list/sync/localEvent.ts similarity index 74% rename from src/main/modules/sync/server/modules/list/localEvent.ts rename to src/main/modules/sync/server/modules/list/sync/localEvent.ts index 4a14cb3e..54464986 100644 --- a/src/main/modules/sync/server/modules/list/localEvent.ts +++ b/src/main/modules/sync/server/modules/list/sync/localEvent.ts @@ -1,6 +1,5 @@ -import { updateDeviceSnapshotKey } from '@main/modules/sync/data' -import { registerListActionEvent } from '../../../utils' -import { getCurrentListInfoKey } from '../../utils' +import { registerListActionEvent } from '../../../../utils' +import { getUserSpace } from '../../../user' // let socket: LX.Sync.Server.Socket | null let unregisterLocalListAction: (() => void) | null @@ -8,11 +7,12 @@ let unregisterLocalListAction: (() => void) | null const sendListAction = async(wss: LX.Sync.Server.SocketServer, action: LX.Sync.ActionList) => { // console.log('sendListAction', action.action) - const key = await getCurrentListInfoKey() + const userSpace = getUserSpace() + const key = await userSpace.listManage.createSnapshot() for (const client of wss.clients) { if (!client.isReady) return void client.remoteSyncList.onListSyncAction(action).then(() => { - updateDeviceSnapshotKey(client.keyInfo, key) + void userSpace.listManage.updateDeviceSnapshotKey(client.keyInfo.clientId, key) }) } } diff --git a/src/main/modules/sync/server/modules/list/sync.ts b/src/main/modules/sync/server/modules/list/sync/sync.ts similarity index 79% rename from src/main/modules/sync/server/modules/list/sync.ts rename to src/main/modules/sync/server/modules/list/sync/sync.ts index 4fe8da53..23ce4352 100644 --- a/src/main/modules/sync/server/modules/list/sync.ts +++ b/src/main/modules/sync/server/modules/list/sync/sync.ts @@ -1,14 +1,7 @@ -import { SYNC_CLOSE_CODE } from '@common/constants' +import { SYNC_CLOSE_CODE } from '../../../../constants' +import { getUserSpace, getUserConfig } from '../../../user' import { getLocalListData, setLocalListData } from '@main/modules/sync/utils' -import { removeSelectModeListener, sendCloseSelectMode, sendSelectMode } from '@main/modules/winMain' -import { createSnapshot, getCurrentListInfoKey } from '../../utils' -import { getSnapshot, updateDeviceSnapshotKey } from '@main/modules/sync/data' - - -const handleSetLocalListData = async(listData: LX.Sync.ListData) => { - await setLocalListData(listData) - return createSnapshot() -} +// import { LIST_IDS } from '@common/constants' // type ListInfoType = LX.List.UserListInfoFull | LX.List.MyDefaultListInfoFull | LX.List.MyLoveListInfoFull @@ -33,35 +26,32 @@ const getRemoteListMD5 = async(socket: LX.Sync.Server.Socket): Promise = return socket.remoteSyncList.list_sync_get_md5() } -const getSyncMode = async(socket: LX.Sync.Server.Socket): Promise => new Promise((resolve, reject) => { - const handleDisconnect = (err: Error) => { - sendCloseSelectMode() - removeSelectModeListener() - reject(err) - } - let removeEventClose = socket.onClose(handleDisconnect) - sendSelectMode(socket.keyInfo.deviceName, (mode) => { - if (mode == null) { - reject(new Error('cancel')) - return - } - resolve(mode) - removeSelectModeListener() - removeEventClose() - }) -}) +// const getLocalListData = async(socket: LX.Sync.Server.Socket): Promise => { +// return getUserSpace(socket.userInfo.name).listManage.getListData() +// } +const getSyncMode = async(socket: LX.Sync.Server.Socket): Promise => { + return socket.remoteSyncList.list_sync_get_sync_mode() +} const finishedSync = async(socket: LX.Sync.Server.Socket) => { await socket.remoteSyncList.list_sync_finished() } + +const setLocalList = async(socket: LX.Sync.Server.Socket, listData: LX.Sync.ListData) => { + await setLocalListData(listData) + const userSpace = getUserSpace(socket.userInfo.name) + return userSpace.listManage.createSnapshot() +} + const overwriteRemoteListData = async(socket: LX.Sync.Server.Socket, listData: LX.Sync.ListData, key: string, excludeIds: string[] = []) => { const action = { action: 'list_data_overwrite', data: listData } as const const tasks: Array> = [] socket.broadcast((client) => { - if (excludeIds.includes(client.keyInfo.clientId) || !client.isReady) return - tasks.push(client.remoteSyncList.onListSyncAction(action).then(() => { - updateDeviceSnapshotKey(socket.keyInfo, key) + if (excludeIds.includes(client.keyInfo.clientId) || client.userInfo.name != socket.userInfo.name || !client.isReady) return + tasks.push(client.remoteSyncList.onListSyncAction(action).then(async() => { + const userSpace = getUserSpace(socket.userInfo.name) + return userSpace.listManage.updateDeviceSnapshotKey(socket.keyInfo.clientId, key) }).catch(err => { console.log(err.message) })) @@ -71,7 +61,8 @@ const overwriteRemoteListData = async(socket: LX.Sync.Server.Socket, listData: L } const setRemotelList = async(socket: LX.Sync.Server.Socket, listData: LX.Sync.ListData, key: string): Promise => { await socket.remoteSyncList.list_sync_set_list_data(listData) - updateDeviceSnapshotKey(socket.keyInfo, key) + const userSpace = getUserSpace(socket.userInfo.name) + await userSpace.listManage.updateDeviceSnapshotKey(socket.keyInfo.clientId, key) } type UserDataObj = Map @@ -120,8 +111,8 @@ const handleMergeList = ( } return ids.map(id => map.get(id)) as LX.Music.MusicInfo[] } -const mergeList = (sourceListData: LX.Sync.ListData, targetListData: LX.Sync.ListData): LX.Sync.ListData => { - const addMusicLocationType = global.lx.appSetting['list.addMusicLocationType'] +const mergeList = (socket: LX.Sync.Server.Socket, sourceListData: LX.Sync.ListData, targetListData: LX.Sync.ListData): LX.Sync.ListData => { + const addMusicLocationType = getUserConfig(socket.userInfo.name)['list.addMusicLocationType'] const newListData: LX.Sync.ListData = { defaultList: [], loveList: [], @@ -181,7 +172,7 @@ const overwriteList = (sourceListData: LX.Sync.ListData, targetListData: LX.Sync } const handleMergeListData = async(socket: LX.Sync.Server.Socket): Promise<[LX.Sync.ListData, boolean, boolean]> => { - const mode: LX.Sync.Mode = await getSyncMode(socket) + const mode: LX.Sync.ListSyncMode = await getSyncMode(socket) if (mode == 'cancel') { socket.close(SYNC_CLOSE_CODE.normal) @@ -194,10 +185,10 @@ const handleMergeListData = async(socket: LX.Sync.Server.Socket): Promise<[LX.Sy let requiredUpdateRemoteListData = true switch (mode) { case 'merge_local_remote': - listData = mergeList(localListData, remoteListData) + listData = mergeList(socket, localListData, remoteListData) break case 'merge_remote_local': - listData = mergeList(remoteListData, localListData) + listData = mergeList(socket, remoteListData, localListData) break case 'overwrite_local_remote': listData = overwriteList(localListData, remoteListData) @@ -225,31 +216,35 @@ const handleMergeListData = async(socket: LX.Sync.Server.Socket): Promise<[LX.Sy const handleSyncList = async(socket: LX.Sync.Server.Socket) => { const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalListData()]) console.log('handleSyncList', 'remoteListData, localListData') + console.log('localListData', localListData.defaultList.length || localListData.loveList.length || localListData.userList.length) + console.log('remoteListData', remoteListData.defaultList.length || remoteListData.loveList.length || remoteListData.userList.length) + const userSpace = getUserSpace(socket.userInfo.name) + const clientId = socket.keyInfo.clientId if (localListData.defaultList.length || localListData.loveList.length || localListData.userList.length) { if (remoteListData.defaultList.length || remoteListData.loveList.length || remoteListData.userList.length) { const [mergedList, requiredUpdateLocalListData, requiredUpdateRemoteListData] = await handleMergeListData(socket) - console.log('handleMergeListData', 'mergedList') + console.log('handleMergeListData', 'mergedList', requiredUpdateLocalListData, requiredUpdateRemoteListData) let key if (requiredUpdateLocalListData) { - key = await handleSetLocalListData(mergedList) - await overwriteRemoteListData(socket, mergedList, key, [socket.keyInfo.clientId]) - if (!requiredUpdateRemoteListData) updateDeviceSnapshotKey(socket.keyInfo, key) + key = await setLocalList(socket, mergedList) + await overwriteRemoteListData(socket, mergedList, key, [clientId]) + if (!requiredUpdateRemoteListData) await userSpace.listManage.updateDeviceSnapshotKey(clientId, key) } if (requiredUpdateRemoteListData) { - if (!key) key = await getCurrentListInfoKey() + if (!key) key = await userSpace.listManage.getCurrentListInfoKey() await setRemotelList(socket, mergedList, key) } } else { - await setRemotelList(socket, localListData, await getCurrentListInfoKey()) + await setRemotelList(socket, localListData, await userSpace.listManage.getCurrentListInfoKey()) } } else { let key: string if (remoteListData.defaultList.length || remoteListData.loveList.length || remoteListData.userList.length) { - key = await handleSetLocalListData(remoteListData) - await overwriteRemoteListData(socket, remoteListData, key, [socket.keyInfo.clientId]) + key = await setLocalList(socket, remoteListData) + await overwriteRemoteListData(socket, remoteListData, key, [clientId]) } - key ??= await getCurrentListInfoKey() - updateDeviceSnapshotKey(socket.keyInfo, key) + key ??= await userSpace.listManage.getCurrentListInfoKey() + await userSpace.listManage.updateDeviceSnapshotKey(clientId, key) } } @@ -297,15 +292,17 @@ const mergeListDataFromSnapshot = ( } const checkListLatest = async(socket: LX.Sync.Server.Socket) => { const remoteListMD5 = await getRemoteListMD5(socket) - const currentListInfoKey = await getCurrentListInfoKey() + const userSpace = getUserSpace(socket.userInfo.name) + const userCurrentListInfoKey = await userSpace.listManage.getDeviceCurrentSnapshotKey(socket.keyInfo.clientId) + const currentListInfoKey = await userSpace.listManage.getCurrentListInfoKey() const latest = remoteListMD5 == currentListInfoKey - if (latest && socket.keyInfo.snapshotKey != currentListInfoKey) updateDeviceSnapshotKey(socket.keyInfo, currentListInfoKey) + if (latest && userCurrentListInfoKey != currentListInfoKey) await userSpace.listManage.updateDeviceSnapshotKey(socket.keyInfo.clientId, 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 addMusicLocationType = getUserConfig(socket.userInfo.name)['list.addMusicLocationType'] const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalListData()]) const newListData: LX.Sync.ListData = { defaultList: [], @@ -361,17 +358,21 @@ const handleMergeListDataFromSnapshot = async(socket: LX.Sync.Server.Socket, sna }) newListData.userList = newUserList - const key = await handleSetLocalListData(newListData) - await setRemotelList(socket, newListData, key) + const key = await setLocalList(socket, newListData) + const err = await setRemotelList(socket, newListData, key).catch(err => err) await overwriteRemoteListData(socket, newListData, key, [socket.keyInfo.clientId]) + if (err) throw err } - 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) + // console.log(socket.keyInfo) + const user = getUserSpace(socket.userInfo.name) + const userCurrentListInfoKey = await user.listManage.getDeviceCurrentSnapshotKey(socket.keyInfo.clientId) + if (userCurrentListInfoKey) { + const listData = await user.listManage.snapshotDataManage.getSnapshot(userCurrentListInfoKey) if (listData) { + console.log('handleMergeListDataFromSnapshot') await handleMergeListDataFromSnapshot(socket, listData) return } @@ -379,7 +380,6 @@ const syncList = async(socket: LX.Sync.Server.Socket) => { await handleSyncList(socket) } - // export default async(_wss: LX.Sync.Server.SocketServer, socket: LX.Sync.Server.Socket) => { // if (!wss) { // wss = _wss @@ -402,7 +402,6 @@ const syncList = async(socket: LX.Sync.Server.Socket) => { // syncingId = socket.keyInfo.clientId // await syncList(socket).then(async() => { -// // if (newListData) registerUpdateSnapshotTask(socket, { ...newListData }) // return finishedSync(socket) // }).finally(() => { // syncingId = null diff --git a/src/main/modules/sync/server/auth.ts b/src/main/modules/sync/server/server/auth.ts similarity index 84% rename from src/main/modules/sync/server/auth.ts rename to src/main/modules/sync/server/server/auth.ts index 7a21fd73..64274b67 100644 --- a/src/main/modules/sync/server/auth.ts +++ b/src/main/modules/sync/server/server/auth.ts @@ -1,21 +1,26 @@ import type http from 'http' import { SYNC_CODE } from '@common/constants' +import { + aesEncrypt, + aesDecrypt, + rsaEncrypt, + getIP, +} from '../utils/tools' import querystring from 'node:querystring' -import { getIP } from './utils' -import { createClientKeyInfo, getClientKeyInfo, saveClientKeyInfo } from '../data' -import { aesDecrypt, aesEncrypt, getComputerName, rsaEncrypt } from '../utils' -import { toMD5 } from '@common/utils/nodejs' +import { getUserSpace, createClientKeyInfo } from '../user' +import { toMD5 } from '../utils' +import { getComputerName } from '../../utils' const requestIps = new Map() - const getAvailableIP = (req: http.IncomingMessage) => { let ip = getIP(req) return ip && (requestIps.get(ip) ?? 0) < 10 ? ip : null } const verifyByKey = (encryptMsg: string, userId: string) => { - const keyInfo = getClientKeyInfo(userId) + const userSpace = getUserSpace() + const keyInfo = userSpace.dataManage.getClientKeyInfo(userId) if (!keyInfo) return null let text try { @@ -28,7 +33,7 @@ const verifyByKey = (encryptMsg: string, userId: string) => { const deviceName = text.replace(SYNC_CODE.authMsg, '') || 'Unknown' if (deviceName != keyInfo.deviceName) { keyInfo.deviceName = deviceName - saveClientKeyInfo(keyInfo) + userSpace.dataManage.saveClientKeyInfo(keyInfo) } return aesEncrypt(SYNC_CODE.helloMsg, keyInfo.key) } @@ -43,7 +48,7 @@ const verifyByCode = (encryptMsg: string, password: string) => { let text try { text = aesDecrypt(encryptMsg, key) - } catch (err) { + } catch { return null } // console.log(text) @@ -53,6 +58,8 @@ const verifyByCode = (encryptMsg: string, password: string) => { const deviceName = data[2] || 'Unknown' const isMobile = data[3] == 'lx_music_mobile' const keyInfo = createClientKeyInfo(deviceName, isMobile) + const userSpace = getUserSpace() + userSpace.dataManage.saveClientKeyInfo(keyInfo) return rsaEncrypt(Buffer.from(JSON.stringify({ clientId: keyInfo.clientId, key: keyInfo.key, @@ -66,7 +73,6 @@ export const authCode = async(req: http.IncomingMessage, res: http.ServerRespons let code = 401 let msg: string = SYNC_CODE.msgAuthFailed - // console.log(req.headers) let ip = getAvailableIP(req) if (ip) { if (typeof req.headers.m == 'string' && req.headers.m) { @@ -89,13 +95,15 @@ export const authCode = async(req: http.IncomingMessage, res: http.ServerRespons code = 403 msg = SYNC_CODE.msgBlockedIp } + // console.log(req.headers) res.writeHead(code) res.end(msg) } const verifyConnection = (encryptMsg: string, userId: string) => { - const keyInfo = getClientKeyInfo(userId) + const userSpace = getUserSpace() + const keyInfo = userSpace.dataManage.getClientKeyInfo(userId) if (!keyInfo) return false let text try { diff --git a/src/main/modules/sync/server/server/index.ts b/src/main/modules/sync/server/server/index.ts new file mode 100644 index 00000000..a1d46046 --- /dev/null +++ b/src/main/modules/sync/server/server/index.ts @@ -0,0 +1,14 @@ +import { + startServer, + stopServer, + getStatus, + generateCode, +} from './server' + + +export { + startServer, + stopServer, + getStatus, + generateCode, +} diff --git a/src/main/modules/sync/server/server.ts b/src/main/modules/sync/server/server/server.ts similarity index 89% rename from src/main/modules/sync/server/server.ts rename to src/main/modules/sync/server/server/server.ts index ea00def3..264a32c9 100644 --- a/src/main/modules/sync/server/server.ts +++ b/src/main/modules/sync/server/server/server.ts @@ -1,15 +1,16 @@ import http, { type IncomingMessage } from 'node:http' import url from 'node:url' import { WebSocketServer } from 'ws' -import { modules, callObj } from './modules' +import { modules, callObj } from '../modules' import { authCode, authConnect } from './auth' -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' +import { getAddress } from '../../utils' +import { SYNC_CLOSE_CODE, SYNC_CODE } from '../../constants' +import { getUserSpace, releaseUserSpace, getServerId, initServerInfo } from '../user' import { createMsg2call } from 'message2call' +import log from '../../log' +import { sendServerStatus } from '@main/modules/winMain' +import { decryptMsg, encryptMsg, generateCode as handleGenerateCode } from '../utils/tools' +import migrateData from '../../migrate' let status: LX.Sync.ServerStatus = { @@ -19,6 +20,7 @@ let status: LX.Sync.ServerStatus = { code: '', devices: [], } + let stopingServer = false const codeTools: { @@ -51,6 +53,7 @@ const syncData = async(socket: LX.Sync.Server.Socket) => { } } + const registerLocalSyncEvent = async(wss: LX.Sync.Server.SocketServer) => { for (const module of Object.values(modules)) { module.registerEvent(wss) @@ -63,10 +66,11 @@ const unregisterLocalSyncEvent = () => { } } + const checkDuplicateClient = (newSocket: LX.Sync.Server.Socket) => { for (const client of [...wss!.clients]) { if (client === newSocket || client.keyInfo.clientId != newSocket.keyInfo.clientId) continue - console.log('duplicate client', client.keyInfo.deviceName) + log.info('duplicate client', client.userInfo.name, client.keyInfo.deviceName) client.isReady = false client.close(SYNC_CLOSE_CODE.normal) } @@ -76,15 +80,18 @@ const handleConnection = async(socket: LX.Sync.Server.Socket, request: IncomingM const queryData = url.parse(request.url as string, true).query as Record // // if (typeof socket.handshake.query.i != 'string') return socket.disconnect(true) - const keyInfo = getClientKeyInfo(queryData.i) + const userSpace = getUserSpace() + const keyInfo = userSpace.dataManage.getClientKeyInfo(queryData.i) if (!keyInfo) { socket.close(SYNC_CLOSE_CODE.failed) return } - keyInfo.lastSyncDate = Date.now() - saveClientKeyInfo(keyInfo) + keyInfo.lastConnectDate = Date.now() + userSpace.dataManage.saveClientKeyInfo(keyInfo) // // socket.lx_keyInfo = keyInfo socket.keyInfo = keyInfo + socket.userInfo = { name: 'default' } + checkDuplicateClient(socket) try { @@ -95,13 +102,12 @@ const handleConnection = async(socket: LX.Sync.Server.Socket, request: IncomingM return } status.devices.push(keyInfo) - socket.onClose(() => { - // console.log('disconnect', reason) - status.devices.splice(status.devices.findIndex(k => k.clientId == keyInfo?.clientId), 1) - sendServerStatus(status) - }) // handleConnection(io, socket) sendServerStatus(status) + socket.onClose(() => { + status.devices.splice(status.devices.findIndex(k => k.clientId == keyInfo.clientId), 1) + sendServerStatus(status) + }) // console.log('connection', keyInfo.deviceName) log.info('connection', keyInfo.deviceName) @@ -111,8 +117,8 @@ const handleConnection = async(socket: LX.Sync.Server.Socket, request: IncomingM } const handleUnconnection = () => { - console.log('unconnection') - // console.log(socket.handshake.query) + // console.log('unconnection') + releaseUserSpace() } const authConnection = (req: http.IncomingMessage, callback: (err: string | null | undefined, success: boolean) => void) => { @@ -229,7 +235,7 @@ const handleStartServer = async(port = 9527, ip = '0.0.0.0') => await new Promis closeEvents = [] msg2call.onDestroy() if (socket.isReady) { - log.info('deconnection', socket.keyInfo.deviceName) + log.info('deconnection', socket.userInfo.name, socket.keyInfo.deviceName) // events = {} if (!status.devices.length) handleUnconnection() } else { @@ -247,14 +253,16 @@ const handleStartServer = async(port = 9527, ip = '0.0.0.0') => await new Promis if (!wss) return for (const client of wss.clients) handler(client) } + void handleConnection(socket, request) }) httpServer.on('upgrade', function upgrade(request, socket, head) { - socket.on('error', onSocketError) + socket.addListener('error', onSocketError) // This function is not defined on purpose. Implement it with your own logic. authConnection(request, err => { if (err) { + console.log(err) socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n') socket.destroy() return @@ -270,6 +278,7 @@ const handleStartServer = async(port = 9527, ip = '0.0.0.0') => await new Promis const interval = setInterval(() => { wss?.clients.forEach(socket => { if (socket.isAlive == false) { + log.info('alive check false:', socket.userInfo.name, socket.keyInfo.deviceName) socket.terminate() return } @@ -321,7 +330,6 @@ const handleStopServer = async() => new Promise((resolve, reject) => { }) export const stopServer = async() => { - console.log('stop') codeTools.stop() if (!status.status) { status.status = false @@ -345,16 +353,20 @@ export const stopServer = async() => { console.log(err) status.message = err.message }).finally(() => { - stopingServer = false sendServerStatus(status) + stopingServer = false }) } export const startServer = async(port: number) => { - console.log('status.status', status.status) + // if (status.status) await handleStopServer() + console.log('status.status', status.status, stopingServer) if (stopingServer) return if (status.status) await handleStopServer() + await migrateData(global.lxDataPath) + await initServerInfo() + log.info('starting sync server') await handleStartServer(port).then(() => { console.log('sync server started') diff --git a/src/main/modules/sync/server/user/data.ts b/src/main/modules/sync/server/user/data.ts new file mode 100644 index 00000000..c8b66559 --- /dev/null +++ b/src/main/modules/sync/server/user/data.ts @@ -0,0 +1,142 @@ +import fs from 'node:fs' +import path from 'node:path' +import { randomBytes } from 'node:crypto' +import { throttle } from '@common/utils/common' +import { filterFileName, toMD5 } from '../utils' +import { File } from '../../constants' + + +interface ServerInfo { + serverId: string + version: number +} +interface DevicesInfo { + userName: string + clients: Record +} +const saveServerInfoThrottle = throttle(() => { + fs.writeFile(path.join(global.lxDataPath, File.serverDataPath, File.serverInfoJSON), JSON.stringify(serverInfo), (err) => { + if (err) console.error(err) + }) +}) +let serverInfo: ServerInfo +const exists = async(path: string) => fs.promises.stat(path).then(() => true).catch(() => false) +export const initServerInfo = async() => { + if (serverInfo != null) return + const serverInfoFilePath = path.join(global.lxDataPath, File.serverDataPath, File.serverInfoJSON) + if (await exists(serverInfoFilePath)) { + serverInfo = JSON.parse((await fs.promises.readFile(serverInfoFilePath)).toString()) + } else { + serverInfo = { + serverId: randomBytes(4 * 4).toString('base64'), + version: 2, + } + const syncDataPath = path.join(global.lxDataPath, File.serverDataPath) + if (!await exists(syncDataPath)) { + await fs.promises.mkdir(syncDataPath, { recursive: true }) + } + saveServerInfoThrottle() + } +} +export const getServerId = () => { + return serverInfo.serverId +} +export const getVersion = async() => { + await initServerInfo() + return serverInfo.version ?? 1 +} +export const setVersion = async(version: number) => { + await initServerInfo() + serverInfo.version = version + saveServerInfoThrottle() +} + +export const getUserDirname = (userName: string) => `${filterFileName(userName)}_${toMD5(userName).substring(0, 6)}` + +export const getUserConfig = (userName: string) => { + return { + maxSnapshotNum: global.lx.appSetting['sync.server.maxSsnapshotNum'], + 'list.addMusicLocationType': global.lx.appSetting['list.addMusicLocationType'], + } +} + + +// 读取所有用户目录下的devicesInfo信息,建立clientId与用户的对应关系,用于非首次连接 +// let deviceUserMap: Map = new Map() +// const init +// for (const deviceInfo of fs.readdirSync(syncDataPath).map(dirname => { +// const devicesFilePath = path.join(syncDataPath, dirname, File.userDevicesJSON) +// if (fs.existsSync(devicesFilePath)) { +// const devicesInfo = JSON.parse(fs.readFileSync(devicesFilePath).toString()) as DevicesInfo +// if (getUserDirname(devicesInfo.userName) == dirname) return { userName: devicesInfo.userName, devices: devicesInfo.clients } +// } +// return { userName: '', devices: {} } +// })) { +// for (const device of Object.values(deviceInfo.devices)) { +// if (deviceInfo.userName) deviceUserMap.set(device.clientId, deviceInfo.userName) +// } +// } +// export const getUserName = (clientId: string): string | null => { +// if (!clientId) return null +// return deviceUserMap.get(clientId) ?? null +// } +// export const setUserName = (clientId: string, dir: string) => { +// deviceUserMap.set(clientId, dir) +// } +// export const deleteUserName = (clientId: string) => { +// deviceUserMap.delete(clientId) +// } + +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, + lastConnectDate: 0, + } + return keyInfo +} + +export class UserDataManage { + userName: string + userDir: string + devicesFilePath: string + devicesInfo: DevicesInfo + private readonly saveDevicesInfoThrottle: () => void + + saveClientKeyInfo = (keyInfo: LX.Sync.ServerKeyInfo) => { + if (this.devicesInfo.clients[keyInfo.clientId] == null && Object.keys(this.devicesInfo.clients).length > 101) throw new Error('max keys') + this.devicesInfo.clients[keyInfo.clientId] = keyInfo + this.saveDevicesInfoThrottle() + } + + getClientKeyInfo = (clientId?: string): LX.Sync.ServerKeyInfo | null => { + if (!clientId) return null + return this.devicesInfo.clients[clientId] ?? null + } + + isIncluedsClient = (clientId: string) => { + return Object.values(this.devicesInfo.clients).some(client => client.clientId == clientId) + } + + constructor(userName: string) { + this.userName = userName + const syncDataPath = path.join(global.lxDataPath, File.serverDataPath) + this.userDir = syncDataPath + this.devicesFilePath = path.join(this.userDir, File.userDevicesJSON) + this.devicesInfo = fs.existsSync(this.devicesFilePath) ? JSON.parse(fs.readFileSync(this.devicesFilePath).toString()) : { userName, clients: {} } + + this.saveDevicesInfoThrottle = throttle(() => { + fs.writeFile(this.devicesFilePath, JSON.stringify(this.devicesInfo), 'utf8', (err) => { + if (err) console.error(err) + }) + }) + } +} +// type UserDataManages = Map + +// export const createUserDataManage = (user: LX.UserConfig) => { +// const manage = Object.create(userDataManage) as typeof userDataManage +// manage.userDir = user.dataPath +// } diff --git a/src/main/modules/sync/server/user/index.ts b/src/main/modules/sync/server/user/index.ts new file mode 100644 index 00000000..9e881de6 --- /dev/null +++ b/src/main/modules/sync/server/user/index.ts @@ -0,0 +1,50 @@ +import { UserDataManage } from './data' +import { + ListManage, +} from '../modules' + +export interface UserSpace { + dataManage: UserDataManage + listManage: ListManage +} +const users = new Map() + +const delayTime = 10 * 1000 +const delayReleaseTimeouts = new Map() +const clearDelayReleaseTimeout = (userName: string) => { + if (!delayReleaseTimeouts.has(userName)) return + + clearTimeout(delayReleaseTimeouts.get(userName)) + delayReleaseTimeouts.delete(userName) +} +const seartDelayReleaseTimeout = (userName: string) => { + clearDelayReleaseTimeout(userName) + delayReleaseTimeouts.set(userName, setTimeout(() => { + users.delete(userName) + }, delayTime)) +} + +export const getUserSpace = (userName = 'default') => { + clearDelayReleaseTimeout(userName) + + let user = users.get(userName) + if (!user) { + console.log('new user data manage:', userName) + const dataManage = new UserDataManage(userName) + users.set(userName, user = { + dataManage, + listManage: new ListManage(dataManage), + }) + } + return user +} + +export const releaseUserSpace = (userName = 'default', force = false) => { + if (force) { + clearDelayReleaseTimeout(userName) + users.delete(userName) + } else seartDelayReleaseTimeout(userName) +} + + +export * from './data' diff --git a/src/main/modules/sync/server/utils.ts b/src/main/modules/sync/server/utils.ts deleted file mode 100644 index 66e9885e..00000000 --- a/src/main/modules/sync/server/utils.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { toMD5 } from '@common/utils/nodejs' -import type http from 'node:http' -import { - getSnapshotInfo, - saveSnapshot, - saveSnapshotInfo, - type SnapshotInfo, -} from '../data' -import { decodeData, encodeData, getLocalListData } from '../utils' - -export const generateCode = (): string => { - return Math.random().toString().substring(2, 8) -} - -export const getIP = (request: http.IncomingMessage) => { - return request.socket.remoteAddress -} - -export const encryptMsg = async(keyInfo: LX.Sync.ServerKeyInfo | null, msg: string): Promise => { - return encodeData(msg) - // console.log('enmsg raw: ', msg.length, 'en: ', len.length) - // return len - // if (!keyInfo) return '' - // return aesEncrypt(msg, keyInfo.key, keyInfo.iv) -} - -export const decryptMsg = async(keyInfo: LX.Sync.ServerKeyInfo | null, enMsg: string): Promise => { - return decodeData(enMsg) - // console.log('decmsg raw: ', len.length, 'en: ', enMsg.length) - // return len - // if (!keyInfo) return '' - // let msg = '' - // try { - // msg = aesDecrypt(enMsg, keyInfo.key, keyInfo.iv) - // } catch (err) { - // console.log(err) - // } - // return msg -} - -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() - return createSnapshot() -} diff --git a/src/main/modules/sync/server/utils/index.ts b/src/main/modules/sync/server/utils/index.ts new file mode 100644 index 00000000..341f1d62 --- /dev/null +++ b/src/main/modules/sync/server/utils/index.ts @@ -0,0 +1,31 @@ +import fs from 'node:fs' +import crypto from 'node:crypto' + + +export const createDirSync = (path: string) => { + if (!fs.existsSync(path)) { + try { + fs.mkdirSync(path, { recursive: true }) + } catch (e: any) { + if (e.code !== 'EEXIST') { + console.error('Could not set up log directory, error was: ', e) + process.exit(1) + } + } + } +} + +const fileNameRxp = /[\\/:*?#"<>|]/g +export const filterFileName = (name: string): string => name.replace(fileNameRxp, '') + +/** + * 创建 MD5 hash + * @param {*} str + */ +export const toMD5 = (str: string) => crypto.createHash('md5').update(str).digest('hex') + +export const checkAndCreateDirSync = (path: string) => { + if (!fs.existsSync(path)) { + fs.mkdirSync(path, { recursive: true }) + } +} diff --git a/src/main/modules/sync/server/utils/tools.ts b/src/main/modules/sync/server/utils/tools.ts new file mode 100644 index 00000000..3a18faf8 --- /dev/null +++ b/src/main/modules/sync/server/utils/tools.ts @@ -0,0 +1,105 @@ +import { networkInterfaces } from 'node:os' +import { createCipheriv, createDecipheriv, publicEncrypt, privateDecrypt, constants } from 'node:crypto' +// import { join } from 'node:path' +import zlib from 'node:zlib' +import type http from 'node:http' +// import getStore from '@/utils/store' +// import syncLog from '../../log' +// import { getUserName } from '../user/data' +// import { saveClientKeyInfo } from './data' + +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 +} + +export const generateCode = (): string => { + return Math.random().toString().substring(2, 8) +} + +export const getIP = (request: http.IncomingMessage) => { + return request.socket.remoteAddress +} + + +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 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) +} + + +const gzip = async(data: string) => new Promise((resolve, reject) => { + zlib.gzip(data, (err, buf) => { + if (err) { + reject(err) + return + } + resolve(buf.toString('base64')) + }) +}) +const unGzip = async(data: string) => new Promise((resolve, reject) => { + zlib.gunzip(Buffer.from(data, 'base64'), (err, buf) => { + if (err) { + reject(err) + return + } + resolve(buf.toString()) + }) +}) + +export const encryptMsg = async(keyInfo: LX.Sync.ServerKeyInfo | null, msg: string): Promise => { + return msg.length > 1024 + ? 'cg_' + await gzip(msg) + : msg + // if (!keyInfo) return '' + // return aesEncrypt(msg, keyInfo.key, keyInfo.iv) +} + +export const decryptMsg = async(keyInfo: LX.Sync.ServerKeyInfo | null, enMsg: string): Promise => { + return enMsg.substring(0, 3) == 'cg_' + ? await unGzip(enMsg.replace('cg_', '')) + : enMsg + // console.log('decmsg raw: ', len.length, 'en: ', enMsg.length) + + // if (!keyInfo) return '' + // let msg = '' + // try { + // msg = aesDecrypt(enMsg, keyInfo.key, keyInfo.iv) + // } catch (err) { + // console.log(err) + // } + // return msg +} + +// export const getSnapshotFilePath = (keyInfo: LX.Sync.KeyInfo): string => { +// return join(global.lx.snapshotPath, `snapshot_${keyInfo.snapshotKey}.json`) +// } + +// export const sendStatus = (status: LX.Sync.ServerStatus) => { +// syncLog.info('status', status.devices.map(d => `${getUserName(d.clientId) ?? ''} ${d.deviceName}`)) +// } + diff --git a/src/main/modules/sync/utils.ts b/src/main/modules/sync/utils.ts index 31006f26..b926832b 100644 --- a/src/main/modules/sync/utils.ts +++ b/src/main/modules/sync/utils.ts @@ -193,40 +193,40 @@ export const handleRemoteListAction = async({ action, data }: LX.Sync.ActionList switch (action) { case 'list_data_overwrite': - void global.lx.event_list.list_data_overwrite(data, true) + await 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) + await global.lx.event_list.list_create(data.position, data.listInfos, true) break case 'list_remove': - void global.lx.event_list.list_remove(data, true) + await global.lx.event_list.list_remove(data, true) break case 'list_update': - void global.lx.event_list.list_update(data, true) + await 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) + await 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) + await 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) + await 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) + await 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) + await 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) + await 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) + await 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) + await global.lx.event_list.list_music_clear(data, true) break default: return false diff --git a/src/main/modules/winMain/rendererEvent/sync.ts b/src/main/modules/winMain/rendererEvent/sync.ts index 5da172b6..d4bf68a3 100644 --- a/src/main/modules/winMain/rendererEvent/sync.ts +++ b/src/main/modules/winMain/rendererEvent/sync.ts @@ -3,7 +3,7 @@ import { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames' import { startServer, stopServer, getServerStatus, generateCode, connectServer, disconnectServer, getClientStatus } from '@main/modules/sync' import { sendEvent } from '../main' -let selectModeListenr: ((mode: LX.Sync.Mode | null) => void) | null = null +let selectModeListenr: ((mode: LX.Sync.ListSyncMode | null) => void) | null = null export default () => { mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.sync_action, async({ params: data }) => { @@ -46,7 +46,7 @@ export const sendServerStatus = (status: LX.Sync.ServerStatus) => { data: status, }) } -export const sendSelectMode = (deviceName: string, listener: (mode: LX.Sync.Mode | null) => void) => { +export const sendSelectMode = (deviceName: string, listener: (mode: LX.Sync.ListSyncMode | null) => void) => { selectModeListenr = listener sendSyncAction({ action: 'select_mode', data: deviceName }) } diff --git a/src/main/types/sync.d.ts b/src/main/types/sync.d.ts index 21440a99..6a9cd6d2 100644 --- a/src/main/types/sync.d.ts +++ b/src/main/types/sync.d.ts @@ -29,6 +29,7 @@ declare global { interface Socket extends WS.WebSocket { isAlive?: boolean isReady: boolean + userInfo: { name: 'default' } keyInfo: ServerKeyInfo onClose: (handler: (err: Error) => (void | Promise)) => () => void broadcast: (handler: (client: Socket) => void) => void diff --git a/src/renderer/components/layout/SyncAuthCodeModal.vue b/src/renderer/components/layout/SyncAuthCodeModal.vue index dd67dd35..822cdbe4 100644 --- a/src/renderer/components/layout/SyncAuthCodeModal.vue +++ b/src/renderer/components/layout/SyncAuthCodeModal.vue @@ -44,6 +44,8 @@ export default { host: appSetting['sync.client.host'], authCode: code, }, + }).catch(err => { + console.log(err) }) } return { diff --git a/src/renderer/core/useApp/useSettingSync.ts b/src/renderer/core/useApp/useSettingSync.ts index 17897e65..eec74b54 100644 --- a/src/renderer/core/useApp/useSettingSync.ts +++ b/src/renderer/core/useApp/useSettingSync.ts @@ -46,6 +46,8 @@ export default () => { enable: appSetting['sync.enable'], port: appSetting['sync.server.port'], }, + }).catch(err => { + console.log(err) }) } break @@ -57,6 +59,8 @@ export default () => { enable: appSetting['sync.enable'], host: appSetting['sync.client.host'], }, + }).catch(err => { + console.log(err) }) } break diff --git a/src/renderer/core/useApp/useSync.ts b/src/renderer/core/useApp/useSync.ts index 7fe8d8ed..5d58b5fe 100644 --- a/src/renderer/core/useApp/useSync.ts +++ b/src/renderer/core/useApp/useSync.ts @@ -56,6 +56,8 @@ export default () => { enable: appSetting['sync.enable'], port: appSetting['sync.server.port'], }, + }).catch(err => { + console.log(err) }) } break @@ -67,6 +69,8 @@ export default () => { enable: appSetting['sync.enable'], host: appSetting['sync.client.host'], }, + }).catch(err => { + console.log(err) }) } break