Files
XCDesktop/api/modules/remote/service.ts
2026-03-08 01:34:54 +08:00

179 lines
5.7 KiB
TypeScript

import fs from 'fs/promises'
import path from 'path'
import { resolveNotebookPath } from '../../utils/pathSafety.js'
import type { RemoteConfig, RemoteDevice } from '../../../shared/modules/remote/types.js'
export interface RemoteServiceDependencies { }
const REMOTE_DIR = 'remote'
export interface DeviceData {
lastConnected?: string
}
export class RemoteService {
constructor(private deps: RemoteServiceDependencies = {}) { }
private getRemoteDir(): { relPath: string; fullPath: string } {
const { fullPath } = resolveNotebookPath(REMOTE_DIR)
return { relPath: REMOTE_DIR, fullPath }
}
private getDeviceDir(deviceName: string): { relPath: string; fullPath: string } {
const safeName = this.sanitizeFileName(deviceName)
const { fullPath } = resolveNotebookPath(path.join(REMOTE_DIR, safeName))
return { relPath: path.join(REMOTE_DIR, safeName), fullPath }
}
private sanitizeFileName(name: string): string {
return name.replace(/[<>:"/\\|?*]/g, '_').trim() || 'unnamed'
}
private getDeviceConfigPath(deviceName: string): string {
const { fullPath } = this.getDeviceDir(deviceName)
return path.join(fullPath, 'config.json')
}
private getDeviceScreenshotPath(deviceName: string): string {
const { fullPath } = this.getDeviceDir(deviceName)
return path.join(fullPath, 'screenshot.png')
}
private getDeviceDataPath(deviceName: string): string {
const { fullPath } = this.getDeviceDir(deviceName)
return path.join(fullPath, 'data.json')
}
private async ensureDir(dirPath: string): Promise<void> {
await fs.mkdir(dirPath, { recursive: true })
}
private async getDeviceNames(): Promise<string[]> {
const { fullPath } = this.getRemoteDir()
try {
const entries = await fs.readdir(fullPath, { withFileTypes: true })
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name)
return dirs
} catch {
return []
}
}
async getConfig(): Promise<RemoteConfig> {
const deviceNames = await this.getDeviceNames()
const devices: RemoteDevice[] = await Promise.all(
deviceNames.map(async (name) => {
try {
const configPath = this.getDeviceConfigPath(name)
const content = await fs.readFile(configPath, 'utf-8')
const deviceConfig = JSON.parse(content)
return {
id: deviceConfig.id || name,
deviceName: name,
serverHost: deviceConfig.serverHost || '',
desktopPort: deviceConfig.desktopPort || 3000,
gitPort: deviceConfig.gitPort || 3001,
}
} catch {
return {
id: name,
deviceName: name,
serverHost: '',
desktopPort: 3000,
gitPort: 3001,
}
}
})
)
return { devices }
}
async saveConfig(config: RemoteConfig): Promise<void> {
const { fullPath: remoteDirFullPath } = this.getRemoteDir()
await this.ensureDir(remoteDirFullPath)
const existingDevices = await this.getDeviceNames()
const newDeviceNames = config.devices.map(d => this.sanitizeFileName(d.deviceName))
for (const oldDevice of existingDevices) {
if (!newDeviceNames.includes(oldDevice)) {
try {
const oldDir = path.join(remoteDirFullPath, oldDevice)
await fs.rm(oldDir, { recursive: true, force: true })
} catch { }
}
}
for (const device of config.devices) {
const deviceDir = this.getDeviceDir(device.deviceName)
await this.ensureDir(deviceDir.fullPath)
const deviceConfigPath = this.getDeviceConfigPath(device.deviceName)
const deviceConfig = {
id: device.id,
serverHost: device.serverHost,
desktopPort: device.desktopPort,
gitPort: device.gitPort,
}
await fs.writeFile(deviceConfigPath, JSON.stringify(deviceConfig, null, 2), 'utf-8')
}
}
async getScreenshot(deviceName?: string): Promise<Buffer | null> {
if (!deviceName) {
return null
}
const screenshotPath = this.getDeviceScreenshotPath(deviceName)
try {
return await fs.readFile(screenshotPath)
} catch {
return null
}
}
async saveScreenshot(dataUrl: string, deviceName?: string): Promise<void> {
console.log('[RemoteService] saveScreenshot:', { deviceName, dataUrlLength: dataUrl?.length })
if (!deviceName || deviceName.trim() === '') {
console.warn('[RemoteService] saveScreenshot skipped: no deviceName')
return
}
const deviceDir = this.getDeviceDir(deviceName)
await this.ensureDir(deviceDir.fullPath)
const base64Data = dataUrl.replace(/^data:image\/png;base64,/, '')
const buffer = Buffer.from(base64Data, 'base64')
const screenshotPath = this.getDeviceScreenshotPath(deviceName)
await fs.writeFile(screenshotPath, buffer)
}
async getData(deviceName?: string): Promise<DeviceData | null> {
if (!deviceName || deviceName.trim() === '') {
return null
}
const dataPath = this.getDeviceDataPath(deviceName)
try {
const content = await fs.readFile(dataPath, 'utf-8')
return JSON.parse(content)
} catch {
return null
}
}
async saveData(data: DeviceData, deviceName?: string): Promise<void> {
if (!deviceName || deviceName.trim() === '') {
console.warn('[RemoteService] saveData skipped: no deviceName')
return
}
const deviceDir = this.getDeviceDir(deviceName)
await this.ensureDir(deviceDir.fullPath)
const dataPath = this.getDeviceDataPath(deviceName)
await fs.writeFile(dataPath, JSON.stringify(data, null, 2), 'utf-8')
}
}
export const createRemoteService = (deps?: RemoteServiceDependencies): RemoteService => {
return new RemoteService(deps)
}