179 lines
5.7 KiB
TypeScript
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)
|
|
}
|