重构同步服务端功能部分代码,使其更易扩展新功能

pull/1583/head
lyswhut 2023-08-25 19:13:50 +08:00
parent 70815e276e
commit 0d76c9ccb4
35 changed files with 1089 additions and 473 deletions

View File

@ -31,3 +31,4 @@
### 其他 ### 其他
- 更新 electron 到 v22.3.21 - 更新 electron 到 v22.3.21
- 重构同步服务端功能部分代码,使其更易扩展新功能

View File

@ -140,5 +140,18 @@ declare namespace LX {
tempList: LX.Music.MusicInfo[] 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>
} }
} }

View File

@ -24,7 +24,7 @@ declare namespace LX {
| SyncAction<'client_status', ClientStatus> | SyncAction<'client_status', ClientStatus>
| SyncAction<'server_status', ServerStatus> | SyncAction<'server_status', ServerStatus>
type SyncServiceActions = SyncAction<'select_mode', Mode> type SyncServiceActions = SyncAction<'select_mode', ListSyncMode>
| SyncAction<'get_server_status'> | SyncAction<'get_server_status'>
| SyncAction<'get_client_status'> | SyncAction<'get_client_status'>
| SyncAction<'generate_code'> | SyncAction<'generate_code'>
@ -90,14 +90,17 @@ declare namespace LX {
clientId: string clientId: string
key: string key: string
deviceName: string deviceName: string
lastSyncDate?: number lastConnectDate?: number
snapshotKey: string
isMobile: boolean isMobile: boolean
} }
interface ListInfo {
lastSyncDate?: number
snapshotKey: string
}
type ListData = Omit<LX.List.ListDataFull, 'tempList'> type ListData = Omit<LX.List.ListDataFull, 'tempList'>
type Mode = 'merge_local_remote' type ListSyncMode = 'merge_local_remote'
| 'merge_remote_local' | 'merge_remote_local'
| 'overwrite_local_remote' | 'overwrite_local_remote'
| 'overwrite_remote_local' | 'overwrite_remote_local'

View File

@ -1,26 +1,13 @@
declare namespace LX { declare namespace LX {
namespace Sync { 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<{ type ServerActions = WarpPromiseRecord<{
onListSyncAction: (action: LX.Sync.ActionList) => void onListSyncAction: (action: LX.List.ActionList) => void
}> }>
type ClientActions = WarpPromiseRecord<{ type ClientActions = WarpPromiseRecord<{
onListSyncAction: (action: LX.Sync.ActionList) => void onListSyncAction: (action: LX.List.ActionList) => void
list_sync_get_md5: () => string 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_get_list_data: () => ListData
list_sync_set_list_data: (data: ListData) => void list_sync_set_list_data: (data: ListData) => void
list_sync_finished: () => void list_sync_finished: () => void

View File

@ -1,5 +1,5 @@
import { request, generateRsaKey } from './utils' import { request, generateRsaKey } from './utils'
import { getSyncAuthKey, setSyncAuthKey } from '../data' import { getSyncAuthKey, setSyncAuthKey } from './data'
import { SYNC_CODE } from '@common/constants' import { SYNC_CODE } from '@common/constants'
import log from '../log' import log from '../log'
import { aesDecrypt, aesEncrypt, getComputerName, rsaDecrypt } from '../utils' import { aesDecrypt, aesEncrypt, getComputerName, rsaDecrypt } from '../utils'

View File

@ -0,0 +1,76 @@
import fs from 'node:fs'
import path from 'node:path'
import { File } from '../constants'
let syncAuthKeys: Record<string, LX.Sync.ClientKeyInfo>
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)
// }

View File

@ -4,6 +4,7 @@ import { connect as socketConnect, disconnect as socketDisconnect, sendSyncStatu
import { SYNC_CODE } from '@common/constants' import { SYNC_CODE } from '@common/constants'
import log from '../log' import log from '../log'
import { parseUrl } from './utils' import { parseUrl } from './utils'
import migrateData from '../migrate'
let connectId = 0 let connectId = 0
@ -29,6 +30,8 @@ const connectServer = async(host: string, authCode?: string) => {
message: SYNC_CODE.connecting, message: SYNC_CODE.connecting,
}) })
const id = connectId const id = connectId
await migrateData(global.lxDataPath)
return handleConnect(host, authCode).catch(async err => { return handleConnect(host, authCode).catch(async err => {
if (id != connectId) return if (id != connectId) return
sendSyncStatus({ sendSyncStatus({

View File

@ -21,7 +21,7 @@ export const list_sync_get_md5 = async(socket: LX.Sync.Client.Socket) => {
return toMD5(JSON.stringify(await getLocalListData())) return toMD5(JSON.stringify(await getLocalListData()))
} }
const getSyncMode = async(socket: LX.Sync.Client.Socket): Promise<LX.Sync.Mode> => new Promise((resolve, reject) => { const getSyncMode = async(socket: LX.Sync.Client.Socket): Promise<LX.Sync.ListSyncMode> => new Promise((resolve, reject) => {
const handleDisconnect = (err: Error) => { const handleDisconnect = (err: Error) => {
sendCloseSelectMode() sendCloseSelectMode()
removeSelectModeListener() removeSelectModeListener()

View File

@ -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<Record<LX.Sync.ListSyncMode, LX.Sync.ListSyncMode>> = {
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

View File

@ -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<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('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)
})
}

View File

@ -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<ServerKeyInfo>(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)
}

View File

@ -1,4 +1,3 @@
import * as modules from './modules'
import { import {
startServer, startServer,
stopServer, stopServer,
@ -12,5 +11,4 @@ export {
stopServer, stopServer,
getStatus, getStatus,
generateCode, generateCode,
modules,
} }

View File

@ -1,10 +1,12 @@
import * as list from './list' import { sync } from './list'
// export * as theme from './theme'
export const callObj = Object.assign({}, list.handler)
export const callObj = Object.assign({},
sync.handler,
)
export const modules = { export const modules = {
list, list: sync,
} }
export { ListManage } from './list'

View File

@ -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)
})
})
})
}

View File

@ -1,4 +1,3 @@
export * as handler from './handler' export * as sync from './sync'
export { default as sync } from './sync' export { ListManage } from './manage'
export * from './localEvent'

View File

@ -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<LX.Sync.ListData> => {
return getLocalListData()
}
}

View File

@ -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<string, LX.Sync.ListInfo>
}
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<SnapshotInfo> => {
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<string, UserDataManage>
// export const createUserDataManage = (user: LX.UserConfig) => {
// const manage = Object.create(userDataManage) as typeof userDataManage
// manage.userDir = user.dataPath
// }

View File

@ -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<LX.List.ListDataFull, 'tempList'>, 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)
})
})
}

View File

@ -0,0 +1,3 @@
export * as handler from './handler'
export { default as sync } from './sync'
export * from './localEvent'

View File

@ -1,6 +1,5 @@
import { updateDeviceSnapshotKey } from '@main/modules/sync/data' import { registerListActionEvent } from '../../../../utils'
import { registerListActionEvent } from '../../../utils' import { getUserSpace } from '../../../user'
import { getCurrentListInfoKey } from '../../utils'
// let socket: LX.Sync.Server.Socket | null // let socket: LX.Sync.Server.Socket | null
let unregisterLocalListAction: (() => void) | 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) => { const sendListAction = async(wss: LX.Sync.Server.SocketServer, action: LX.Sync.ActionList) => {
// console.log('sendListAction', action.action) // console.log('sendListAction', action.action)
const key = await getCurrentListInfoKey() const userSpace = getUserSpace()
const key = await userSpace.listManage.createSnapshot()
for (const client of wss.clients) { for (const client of wss.clients) {
if (!client.isReady) return if (!client.isReady) return
void client.remoteSyncList.onListSyncAction(action).then(() => { void client.remoteSyncList.onListSyncAction(action).then(() => {
updateDeviceSnapshotKey(client.keyInfo, key) void userSpace.listManage.updateDeviceSnapshotKey(client.keyInfo.clientId, key)
}) })
} }
} }

View File

@ -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 { getLocalListData, setLocalListData } from '@main/modules/sync/utils'
import { removeSelectModeListener, sendCloseSelectMode, sendSelectMode } from '@main/modules/winMain' // import { LIST_IDS } from '@common/constants'
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()
}
// type ListInfoType = LX.List.UserListInfoFull | LX.List.MyDefaultListInfoFull | LX.List.MyLoveListInfoFull // type ListInfoType = LX.List.UserListInfoFull | LX.List.MyDefaultListInfoFull | LX.List.MyLoveListInfoFull
@ -33,35 +26,32 @@ const getRemoteListMD5 = async(socket: LX.Sync.Server.Socket): Promise<string> =
return socket.remoteSyncList.list_sync_get_md5() return socket.remoteSyncList.list_sync_get_md5()
} }
const getSyncMode = async(socket: LX.Sync.Server.Socket): Promise<LX.Sync.Mode> => new Promise((resolve, reject) => { // const getLocalListData = async(socket: LX.Sync.Server.Socket): Promise<LX.Sync.ListData> => {
const handleDisconnect = (err: Error) => { // return getUserSpace(socket.userInfo.name).listManage.getListData()
sendCloseSelectMode() // }
removeSelectModeListener() const getSyncMode = async(socket: LX.Sync.Server.Socket): Promise<LX.Sync.ListSyncMode> => {
reject(err) return socket.remoteSyncList.list_sync_get_sync_mode()
} }
let removeEventClose = socket.onClose(handleDisconnect)
sendSelectMode(socket.keyInfo.deviceName, (mode) => {
if (mode == null) {
reject(new Error('cancel'))
return
}
resolve(mode)
removeSelectModeListener()
removeEventClose()
})
})
const finishedSync = async(socket: LX.Sync.Server.Socket) => { const finishedSync = async(socket: LX.Sync.Server.Socket) => {
await socket.remoteSyncList.list_sync_finished() 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 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 action = { action: 'list_data_overwrite', data: listData } as const
const tasks: Array<Promise<void>> = [] const tasks: Array<Promise<void>> = []
socket.broadcast((client) => { socket.broadcast((client) => {
if (excludeIds.includes(client.keyInfo.clientId) || !client.isReady) return if (excludeIds.includes(client.keyInfo.clientId) || client.userInfo.name != socket.userInfo.name || !client.isReady) return
tasks.push(client.remoteSyncList.onListSyncAction(action).then(() => { tasks.push(client.remoteSyncList.onListSyncAction(action).then(async() => {
updateDeviceSnapshotKey(socket.keyInfo, key) const userSpace = getUserSpace(socket.userInfo.name)
return userSpace.listManage.updateDeviceSnapshotKey(socket.keyInfo.clientId, key)
}).catch(err => { }).catch(err => {
console.log(err.message) 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<void> => { const setRemotelList = async(socket: LX.Sync.Server.Socket, listData: LX.Sync.ListData, key: string): Promise<void> => {
await socket.remoteSyncList.list_sync_set_list_data(listData) 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<string, LX.List.UserListInfoFull> type UserDataObj = Map<string, LX.List.UserListInfoFull>
@ -120,8 +111,8 @@ const handleMergeList = (
} }
return ids.map(id => map.get(id)) as LX.Music.MusicInfo[] 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 mergeList = (socket: LX.Sync.Server.Socket, sourceListData: LX.Sync.ListData, targetListData: LX.Sync.ListData): LX.Sync.ListData => {
const addMusicLocationType = global.lx.appSetting['list.addMusicLocationType'] const addMusicLocationType = getUserConfig(socket.userInfo.name)['list.addMusicLocationType']
const newListData: LX.Sync.ListData = { const newListData: LX.Sync.ListData = {
defaultList: [], defaultList: [],
loveList: [], 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 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') { if (mode == 'cancel') {
socket.close(SYNC_CLOSE_CODE.normal) socket.close(SYNC_CLOSE_CODE.normal)
@ -194,10 +185,10 @@ const handleMergeListData = async(socket: LX.Sync.Server.Socket): Promise<[LX.Sy
let requiredUpdateRemoteListData = true let requiredUpdateRemoteListData = true
switch (mode) { switch (mode) {
case 'merge_local_remote': case 'merge_local_remote':
listData = mergeList(localListData, remoteListData) listData = mergeList(socket, localListData, remoteListData)
break break
case 'merge_remote_local': case 'merge_remote_local':
listData = mergeList(remoteListData, localListData) listData = mergeList(socket, remoteListData, localListData)
break break
case 'overwrite_local_remote': case 'overwrite_local_remote':
listData = overwriteList(localListData, remoteListData) 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 handleSyncList = async(socket: LX.Sync.Server.Socket) => {
const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalListData()]) const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalListData()])
console.log('handleSyncList', 'remoteListData, localListData') 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 (localListData.defaultList.length || localListData.loveList.length || localListData.userList.length) {
if (remoteListData.defaultList.length || remoteListData.loveList.length || remoteListData.userList.length) { if (remoteListData.defaultList.length || remoteListData.loveList.length || remoteListData.userList.length) {
const [mergedList, requiredUpdateLocalListData, requiredUpdateRemoteListData] = await handleMergeListData(socket) const [mergedList, requiredUpdateLocalListData, requiredUpdateRemoteListData] = await handleMergeListData(socket)
console.log('handleMergeListData', 'mergedList') console.log('handleMergeListData', 'mergedList', requiredUpdateLocalListData, requiredUpdateRemoteListData)
let key let key
if (requiredUpdateLocalListData) { if (requiredUpdateLocalListData) {
key = await handleSetLocalListData(mergedList) key = await setLocalList(socket, mergedList)
await overwriteRemoteListData(socket, mergedList, key, [socket.keyInfo.clientId]) await overwriteRemoteListData(socket, mergedList, key, [clientId])
if (!requiredUpdateRemoteListData) updateDeviceSnapshotKey(socket.keyInfo, key) if (!requiredUpdateRemoteListData) await userSpace.listManage.updateDeviceSnapshotKey(clientId, key)
} }
if (requiredUpdateRemoteListData) { if (requiredUpdateRemoteListData) {
if (!key) key = await getCurrentListInfoKey() if (!key) key = await userSpace.listManage.getCurrentListInfoKey()
await setRemotelList(socket, mergedList, key) await setRemotelList(socket, mergedList, key)
} }
} else { } else {
await setRemotelList(socket, localListData, await getCurrentListInfoKey()) await setRemotelList(socket, localListData, await userSpace.listManage.getCurrentListInfoKey())
} }
} else { } else {
let key: string let key: string
if (remoteListData.defaultList.length || remoteListData.loveList.length || remoteListData.userList.length) { if (remoteListData.defaultList.length || remoteListData.loveList.length || remoteListData.userList.length) {
key = await handleSetLocalListData(remoteListData) key = await setLocalList(socket, remoteListData)
await overwriteRemoteListData(socket, remoteListData, key, [socket.keyInfo.clientId]) await overwriteRemoteListData(socket, remoteListData, key, [clientId])
} }
key ??= await getCurrentListInfoKey() key ??= await userSpace.listManage.getCurrentListInfoKey()
updateDeviceSnapshotKey(socket.keyInfo, key) await userSpace.listManage.updateDeviceSnapshotKey(clientId, key)
} }
} }
@ -297,15 +292,17 @@ const mergeListDataFromSnapshot = (
} }
const checkListLatest = async(socket: LX.Sync.Server.Socket) => { const checkListLatest = async(socket: LX.Sync.Server.Socket) => {
const remoteListMD5 = await getRemoteListMD5(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 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 return latest
} }
const handleMergeListDataFromSnapshot = async(socket: LX.Sync.Server.Socket, snapshot: LX.Sync.ListData) => { const handleMergeListDataFromSnapshot = async(socket: LX.Sync.Server.Socket, snapshot: LX.Sync.ListData) => {
if (await checkListLatest(socket)) return 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 [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalListData()])
const newListData: LX.Sync.ListData = { const newListData: LX.Sync.ListData = {
defaultList: [], defaultList: [],
@ -361,17 +358,21 @@ const handleMergeListDataFromSnapshot = async(socket: LX.Sync.Server.Socket, sna
}) })
newListData.userList = newUserList newListData.userList = newUserList
const key = await handleSetLocalListData(newListData) const key = await setLocalList(socket, newListData)
await setRemotelList(socket, newListData, key) const err = await setRemotelList(socket, newListData, key).catch(err => err)
await overwriteRemoteListData(socket, newListData, key, [socket.keyInfo.clientId]) await overwriteRemoteListData(socket, newListData, key, [socket.keyInfo.clientId])
if (err) throw err
} }
const syncList = async(socket: LX.Sync.Server.Socket) => { const syncList = async(socket: LX.Sync.Server.Socket) => {
// socket.data.snapshotFilePath = getSnapshotFilePath(socket.keyInfo) // socket.data.snapshotFilePath = getSnapshotFilePath(socket.keyInfo)
if (socket.keyInfo.snapshotKey) { // console.log(socket.keyInfo)
const listData = await getSnapshot(socket.keyInfo.snapshotKey) 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) { if (listData) {
console.log('handleMergeListDataFromSnapshot')
await handleMergeListDataFromSnapshot(socket, listData) await handleMergeListDataFromSnapshot(socket, listData)
return return
} }
@ -379,7 +380,6 @@ const syncList = async(socket: LX.Sync.Server.Socket) => {
await handleSyncList(socket) await handleSyncList(socket)
} }
// export default async(_wss: LX.Sync.Server.SocketServer, socket: LX.Sync.Server.Socket) => { // export default async(_wss: LX.Sync.Server.SocketServer, socket: LX.Sync.Server.Socket) => {
// if (!wss) { // if (!wss) {
// wss = _wss // wss = _wss
@ -402,7 +402,6 @@ const syncList = async(socket: LX.Sync.Server.Socket) => {
// syncingId = socket.keyInfo.clientId // syncingId = socket.keyInfo.clientId
// await syncList(socket).then(async() => { // await syncList(socket).then(async() => {
// // if (newListData) registerUpdateSnapshotTask(socket, { ...newListData })
// return finishedSync(socket) // return finishedSync(socket)
// }).finally(() => { // }).finally(() => {
// syncingId = null // syncingId = null

View File

@ -1,21 +1,26 @@
import type http from 'http' import type http from 'http'
import { SYNC_CODE } from '@common/constants' import { SYNC_CODE } from '@common/constants'
import {
aesEncrypt,
aesDecrypt,
rsaEncrypt,
getIP,
} from '../utils/tools'
import querystring from 'node:querystring' import querystring from 'node:querystring'
import { getIP } from './utils' import { getUserSpace, createClientKeyInfo } from '../user'
import { createClientKeyInfo, getClientKeyInfo, saveClientKeyInfo } from '../data' import { toMD5 } from '../utils'
import { aesDecrypt, aesEncrypt, getComputerName, rsaEncrypt } from '../utils' import { getComputerName } from '../../utils'
import { toMD5 } from '@common/utils/nodejs'
const requestIps = new Map<string, number>() const requestIps = new Map<string, number>()
const getAvailableIP = (req: http.IncomingMessage) => { const getAvailableIP = (req: http.IncomingMessage) => {
let ip = getIP(req) let ip = getIP(req)
return ip && (requestIps.get(ip) ?? 0) < 10 ? ip : null return ip && (requestIps.get(ip) ?? 0) < 10 ? ip : null
} }
const verifyByKey = (encryptMsg: string, userId: string) => { const verifyByKey = (encryptMsg: string, userId: string) => {
const keyInfo = getClientKeyInfo(userId) const userSpace = getUserSpace()
const keyInfo = userSpace.dataManage.getClientKeyInfo(userId)
if (!keyInfo) return null if (!keyInfo) return null
let text let text
try { try {
@ -28,7 +33,7 @@ const verifyByKey = (encryptMsg: string, userId: string) => {
const deviceName = text.replace(SYNC_CODE.authMsg, '') || 'Unknown' const deviceName = text.replace(SYNC_CODE.authMsg, '') || 'Unknown'
if (deviceName != keyInfo.deviceName) { if (deviceName != keyInfo.deviceName) {
keyInfo.deviceName = deviceName keyInfo.deviceName = deviceName
saveClientKeyInfo(keyInfo) userSpace.dataManage.saveClientKeyInfo(keyInfo)
} }
return aesEncrypt(SYNC_CODE.helloMsg, keyInfo.key) return aesEncrypt(SYNC_CODE.helloMsg, keyInfo.key)
} }
@ -43,7 +48,7 @@ const verifyByCode = (encryptMsg: string, password: string) => {
let text let text
try { try {
text = aesDecrypt(encryptMsg, key) text = aesDecrypt(encryptMsg, key)
} catch (err) { } catch {
return null return null
} }
// console.log(text) // console.log(text)
@ -53,6 +58,8 @@ const verifyByCode = (encryptMsg: string, password: string) => {
const deviceName = data[2] || 'Unknown' const deviceName = data[2] || 'Unknown'
const isMobile = data[3] == 'lx_music_mobile' const isMobile = data[3] == 'lx_music_mobile'
const keyInfo = createClientKeyInfo(deviceName, isMobile) const keyInfo = createClientKeyInfo(deviceName, isMobile)
const userSpace = getUserSpace()
userSpace.dataManage.saveClientKeyInfo(keyInfo)
return rsaEncrypt(Buffer.from(JSON.stringify({ return rsaEncrypt(Buffer.from(JSON.stringify({
clientId: keyInfo.clientId, clientId: keyInfo.clientId,
key: keyInfo.key, key: keyInfo.key,
@ -66,7 +73,6 @@ export const authCode = async(req: http.IncomingMessage, res: http.ServerRespons
let code = 401 let code = 401
let msg: string = SYNC_CODE.msgAuthFailed let msg: string = SYNC_CODE.msgAuthFailed
// console.log(req.headers)
let ip = getAvailableIP(req) let ip = getAvailableIP(req)
if (ip) { if (ip) {
if (typeof req.headers.m == 'string' && req.headers.m) { 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 code = 403
msg = SYNC_CODE.msgBlockedIp msg = SYNC_CODE.msgBlockedIp
} }
// console.log(req.headers)
res.writeHead(code) res.writeHead(code)
res.end(msg) res.end(msg)
} }
const verifyConnection = (encryptMsg: string, userId: string) => { const verifyConnection = (encryptMsg: string, userId: string) => {
const keyInfo = getClientKeyInfo(userId) const userSpace = getUserSpace()
const keyInfo = userSpace.dataManage.getClientKeyInfo(userId)
if (!keyInfo) return false if (!keyInfo) return false
let text let text
try { try {

View File

@ -0,0 +1,14 @@
import {
startServer,
stopServer,
getStatus,
generateCode,
} from './server'
export {
startServer,
stopServer,
getStatus,
generateCode,
}

View File

@ -1,15 +1,16 @@
import http, { type IncomingMessage } from 'node:http' import http, { type IncomingMessage } from 'node:http'
import url from 'node:url' import url from 'node:url'
import { WebSocketServer } from 'ws' import { WebSocketServer } from 'ws'
import { modules, callObj } from './modules' import { modules, callObj } from '../modules'
import { authCode, authConnect } from './auth' import { authCode, authConnect } from './auth'
import log from '../log' import { getAddress } from '../../utils'
import { SYNC_CLOSE_CODE, SYNC_CODE } from '@common/constants' import { SYNC_CLOSE_CODE, SYNC_CODE } from '../../constants'
import { decryptMsg, encryptMsg, generateCode as handleGenerateCode } from './utils' import { getUserSpace, releaseUserSpace, getServerId, initServerInfo } from '../user'
import { getAddress } from '../utils'
import { sendServerStatus } from '@main/modules/winMain/index'
import { getClientKeyInfo, getServerId, saveClientKeyInfo } from '../data'
import { createMsg2call } from 'message2call' 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 = { let status: LX.Sync.ServerStatus = {
@ -19,6 +20,7 @@ let status: LX.Sync.ServerStatus = {
code: '', code: '',
devices: [], devices: [],
} }
let stopingServer = false let stopingServer = false
const codeTools: { const codeTools: {
@ -51,6 +53,7 @@ const syncData = async(socket: LX.Sync.Server.Socket) => {
} }
} }
const registerLocalSyncEvent = async(wss: LX.Sync.Server.SocketServer) => { const registerLocalSyncEvent = async(wss: LX.Sync.Server.SocketServer) => {
for (const module of Object.values(modules)) { for (const module of Object.values(modules)) {
module.registerEvent(wss) module.registerEvent(wss)
@ -63,10 +66,11 @@ const unregisterLocalSyncEvent = () => {
} }
} }
const checkDuplicateClient = (newSocket: LX.Sync.Server.Socket) => { const checkDuplicateClient = (newSocket: LX.Sync.Server.Socket) => {
for (const client of [...wss!.clients]) { for (const client of [...wss!.clients]) {
if (client === newSocket || client.keyInfo.clientId != newSocket.keyInfo.clientId) continue 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.isReady = false
client.close(SYNC_CLOSE_CODE.normal) 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<string, string> const queryData = url.parse(request.url as string, true).query as Record<string, string>
// // if (typeof socket.handshake.query.i != 'string') return socket.disconnect(true) // // 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) { if (!keyInfo) {
socket.close(SYNC_CLOSE_CODE.failed) socket.close(SYNC_CLOSE_CODE.failed)
return return
} }
keyInfo.lastSyncDate = Date.now() keyInfo.lastConnectDate = Date.now()
saveClientKeyInfo(keyInfo) userSpace.dataManage.saveClientKeyInfo(keyInfo)
// // socket.lx_keyInfo = keyInfo // // socket.lx_keyInfo = keyInfo
socket.keyInfo = keyInfo socket.keyInfo = keyInfo
socket.userInfo = { name: 'default' }
checkDuplicateClient(socket) checkDuplicateClient(socket)
try { try {
@ -95,13 +102,12 @@ const handleConnection = async(socket: LX.Sync.Server.Socket, request: IncomingM
return return
} }
status.devices.push(keyInfo) 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) // handleConnection(io, socket)
sendServerStatus(status) sendServerStatus(status)
socket.onClose(() => {
status.devices.splice(status.devices.findIndex(k => k.clientId == keyInfo.clientId), 1)
sendServerStatus(status)
})
// console.log('connection', keyInfo.deviceName) // console.log('connection', keyInfo.deviceName)
log.info('connection', keyInfo.deviceName) log.info('connection', keyInfo.deviceName)
@ -111,8 +117,8 @@ const handleConnection = async(socket: LX.Sync.Server.Socket, request: IncomingM
} }
const handleUnconnection = () => { const handleUnconnection = () => {
console.log('unconnection') // console.log('unconnection')
// console.log(socket.handshake.query) releaseUserSpace()
} }
const authConnection = (req: http.IncomingMessage, callback: (err: string | null | undefined, success: boolean) => void) => { 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 = [] closeEvents = []
msg2call.onDestroy() msg2call.onDestroy()
if (socket.isReady) { if (socket.isReady) {
log.info('deconnection', socket.keyInfo.deviceName) log.info('deconnection', socket.userInfo.name, socket.keyInfo.deviceName)
// events = {} // events = {}
if (!status.devices.length) handleUnconnection() if (!status.devices.length) handleUnconnection()
} else { } else {
@ -247,14 +253,16 @@ const handleStartServer = async(port = 9527, ip = '0.0.0.0') => await new Promis
if (!wss) return if (!wss) return
for (const client of wss.clients) handler(client) for (const client of wss.clients) handler(client)
} }
void handleConnection(socket, request) void handleConnection(socket, request)
}) })
httpServer.on('upgrade', function upgrade(request, socket, head) { 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. // This function is not defined on purpose. Implement it with your own logic.
authConnection(request, err => { authConnection(request, err => {
if (err) { if (err) {
console.log(err)
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n') socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n')
socket.destroy() socket.destroy()
return return
@ -270,6 +278,7 @@ const handleStartServer = async(port = 9527, ip = '0.0.0.0') => await new Promis
const interval = setInterval(() => { const interval = setInterval(() => {
wss?.clients.forEach(socket => { wss?.clients.forEach(socket => {
if (socket.isAlive == false) { if (socket.isAlive == false) {
log.info('alive check false:', socket.userInfo.name, socket.keyInfo.deviceName)
socket.terminate() socket.terminate()
return return
} }
@ -321,7 +330,6 @@ const handleStopServer = async() => new Promise<void>((resolve, reject) => {
}) })
export const stopServer = async() => { export const stopServer = async() => {
console.log('stop')
codeTools.stop() codeTools.stop()
if (!status.status) { if (!status.status) {
status.status = false status.status = false
@ -345,16 +353,20 @@ export const stopServer = async() => {
console.log(err) console.log(err)
status.message = err.message status.message = err.message
}).finally(() => { }).finally(() => {
stopingServer = false
sendServerStatus(status) sendServerStatus(status)
stopingServer = false
}) })
} }
export const startServer = async(port: number) => { 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 (stopingServer) return
if (status.status) await handleStopServer() if (status.status) await handleStopServer()
await migrateData(global.lxDataPath)
await initServerInfo()
log.info('starting sync server') log.info('starting sync server')
await handleStartServer(port).then(() => { await handleStartServer(port).then(() => {
console.log('sync server started') console.log('sync server started')

View File

@ -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<string, LX.Sync.ServerKeyInfo>
}
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<string, string> = new Map<string, string>()
// 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<string, UserDataManage>
// export const createUserDataManage = (user: LX.UserConfig) => {
// const manage = Object.create(userDataManage) as typeof userDataManage
// manage.userDir = user.dataPath
// }

View File

@ -0,0 +1,50 @@
import { UserDataManage } from './data'
import {
ListManage,
} from '../modules'
export interface UserSpace {
dataManage: UserDataManage
listManage: ListManage
}
const users = new Map<string, UserSpace>()
const delayTime = 10 * 1000
const delayReleaseTimeouts = new Map<string, NodeJS.Timeout>()
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'

View File

@ -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<string> => {
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<string> => {
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()
}

View File

@ -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 })
}
}

View File

@ -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<string>((resolve, reject) => {
zlib.gzip(data, (err, buf) => {
if (err) {
reject(err)
return
}
resolve(buf.toString('base64'))
})
})
const unGzip = async(data: string) => new Promise<string>((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<string> => {
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<string> => {
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}`))
// }

View File

@ -193,40 +193,40 @@ export const handleRemoteListAction = async({ action, data }: LX.Sync.ActionList
switch (action) { switch (action) {
case 'list_data_overwrite': 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 break
case 'list_create': 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 break
case 'list_remove': case 'list_remove':
void global.lx.event_list.list_remove(data, true) await global.lx.event_list.list_remove(data, true)
break break
case 'list_update': case 'list_update':
void global.lx.event_list.list_update(data, true) await global.lx.event_list.list_update(data, true)
break break
case 'list_update_position': 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 break
case 'list_music_add': 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 break
case 'list_music_move': 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 break
case 'list_music_remove': 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 break
case 'list_music_update': 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 break
case 'list_music_update_position': 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 break
case 'list_music_overwrite': 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 break
case 'list_music_clear': 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 break
default: default:
return false return false

View File

@ -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 { startServer, stopServer, getServerStatus, generateCode, connectServer, disconnectServer, getClientStatus } from '@main/modules/sync'
import { sendEvent } from '../main' 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 () => { export default () => {
mainHandle<LX.Sync.SyncServiceActions, any>(WIN_MAIN_RENDERER_EVENT_NAME.sync_action, async({ params: data }) => { mainHandle<LX.Sync.SyncServiceActions, any>(WIN_MAIN_RENDERER_EVENT_NAME.sync_action, async({ params: data }) => {
@ -46,7 +46,7 @@ export const sendServerStatus = (status: LX.Sync.ServerStatus) => {
data: status, 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 selectModeListenr = listener
sendSyncAction({ action: 'select_mode', data: deviceName }) sendSyncAction({ action: 'select_mode', data: deviceName })
} }

View File

@ -29,6 +29,7 @@ declare global {
interface Socket extends WS.WebSocket { interface Socket extends WS.WebSocket {
isAlive?: boolean isAlive?: boolean
isReady: boolean isReady: boolean
userInfo: { name: 'default' }
keyInfo: ServerKeyInfo keyInfo: ServerKeyInfo
onClose: (handler: (err: Error) => (void | Promise<void>)) => () => void onClose: (handler: (err: Error) => (void | Promise<void>)) => () => void
broadcast: (handler: (client: Socket) => void) => void broadcast: (handler: (client: Socket) => void) => void

View File

@ -44,6 +44,8 @@ export default {
host: appSetting['sync.client.host'], host: appSetting['sync.client.host'],
authCode: code, authCode: code,
}, },
}).catch(err => {
console.log(err)
}) })
} }
return { return {

View File

@ -46,6 +46,8 @@ export default () => {
enable: appSetting['sync.enable'], enable: appSetting['sync.enable'],
port: appSetting['sync.server.port'], port: appSetting['sync.server.port'],
}, },
}).catch(err => {
console.log(err)
}) })
} }
break break
@ -57,6 +59,8 @@ export default () => {
enable: appSetting['sync.enable'], enable: appSetting['sync.enable'],
host: appSetting['sync.client.host'], host: appSetting['sync.client.host'],
}, },
}).catch(err => {
console.log(err)
}) })
} }
break break

View File

@ -56,6 +56,8 @@ export default () => {
enable: appSetting['sync.enable'], enable: appSetting['sync.enable'],
port: appSetting['sync.server.port'], port: appSetting['sync.server.port'],
}, },
}).catch(err => {
console.log(err)
}) })
} }
break break
@ -67,6 +69,8 @@ export default () => {
enable: appSetting['sync.enable'], enable: appSetting['sync.enable'],
host: appSetting['sync.client.host'], host: appSetting['sync.client.host'],
}, },
}).catch(err => {
console.log(err)
}) })
} }
break break