Initial commit
This commit is contained in:
25
api/modules/remote/index.ts
Normal file
25
api/modules/remote/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { Router } from 'express'
|
||||
import type { ServiceContainer } from '../../infra/container.js'
|
||||
import { createApiModule } from '../../infra/createModule.js'
|
||||
import { REMOTE_MODULE } from '../../../shared/modules/remote/index.js'
|
||||
import { RemoteService } from './service.js'
|
||||
import { createRemoteRoutes } from './routes.js'
|
||||
|
||||
export * from './service.js'
|
||||
export * from './routes.js'
|
||||
|
||||
export const createRemoteModule = () => {
|
||||
return createApiModule(REMOTE_MODULE, {
|
||||
routes: (container: ServiceContainer): Router => {
|
||||
const remoteService = container.getSync<RemoteService>('remoteService')
|
||||
return createRemoteRoutes({ remoteService })
|
||||
},
|
||||
lifecycle: {
|
||||
onLoad: (container: ServiceContainer): void => {
|
||||
container.register('remoteService', () => new RemoteService())
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export default createRemoteModule
|
||||
80
api/modules/remote/routes.ts
Normal file
80
api/modules/remote/routes.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import express, { type Request, type Response, type Router } from 'express'
|
||||
import { asyncHandler } from '../../utils/asyncHandler.js'
|
||||
import { successResponse } from '../../utils/response.js'
|
||||
import { RemoteService, type DeviceData } from './service.js'
|
||||
|
||||
export interface RemoteRoutesDependencies {
|
||||
remoteService: RemoteService
|
||||
}
|
||||
|
||||
export const createRemoteRoutes = (deps: RemoteRoutesDependencies): Router => {
|
||||
const router = express.Router()
|
||||
const { remoteService } = deps
|
||||
|
||||
router.get(
|
||||
'/config',
|
||||
asyncHandler(async (req: Request, res: Response) => {
|
||||
const config = await remoteService.getConfig()
|
||||
successResponse(res, config)
|
||||
}),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/config',
|
||||
asyncHandler(async (req: Request, res: Response) => {
|
||||
const config = req.body
|
||||
await remoteService.saveConfig(config)
|
||||
successResponse(res, null)
|
||||
}),
|
||||
)
|
||||
|
||||
router.get(
|
||||
'/screenshot',
|
||||
asyncHandler(async (req: Request, res: Response) => {
|
||||
const deviceName = req.query.device as string | undefined
|
||||
const buffer = await remoteService.getScreenshot(deviceName)
|
||||
if (!buffer) {
|
||||
return successResponse(res, '')
|
||||
}
|
||||
const base64 = `data:image/png;base64,${buffer.toString('base64')}`
|
||||
successResponse(res, base64)
|
||||
}),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/screenshot',
|
||||
asyncHandler(async (req: Request, res: Response) => {
|
||||
const { dataUrl, deviceName } = req.body
|
||||
console.log('[Remote] saveScreenshot called:', { deviceName, hasDataUrl: !!dataUrl })
|
||||
await remoteService.saveScreenshot(dataUrl, deviceName)
|
||||
successResponse(res, null)
|
||||
}),
|
||||
)
|
||||
|
||||
router.get(
|
||||
'/data',
|
||||
asyncHandler(async (req: Request, res: Response) => {
|
||||
const deviceName = req.query.device as string | undefined
|
||||
const data = await remoteService.getData(deviceName)
|
||||
successResponse(res, data)
|
||||
}),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/data',
|
||||
asyncHandler(async (req: Request, res: Response) => {
|
||||
const { deviceName, lastConnected } = req.body
|
||||
const data: DeviceData = {}
|
||||
if (lastConnected !== undefined) {
|
||||
data.lastConnected = lastConnected
|
||||
}
|
||||
await remoteService.saveData(data, deviceName)
|
||||
successResponse(res, null)
|
||||
}),
|
||||
)
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
const remoteService = new RemoteService()
|
||||
export default createRemoteRoutes({ remoteService })
|
||||
178
api/modules/remote/service.ts
Normal file
178
api/modules/remote/service.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user