chore: add comments
This commit is contained in:
parent
54706314ea
commit
7722c533a4
@ -1,15 +1,19 @@
|
|||||||
import { SSHConfig, SSHSocketClient } from "./SSHSocketClient"
|
import { SSHConfig, SSHSocketClient } from "./SSHSocketClient"
|
||||||
|
|
||||||
|
// Interface for the response structure from Dokku commands
|
||||||
export interface DokkuResponse {
|
export interface DokkuResponse {
|
||||||
ok: boolean
|
ok: boolean
|
||||||
output: string
|
output: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DokkuClient class extends SSHSocketClient to interact with Dokku via SSH
|
||||||
export class DokkuClient extends SSHSocketClient {
|
export class DokkuClient extends SSHSocketClient {
|
||||||
constructor(config: SSHConfig) {
|
constructor(config: SSHConfig) {
|
||||||
|
// Initialize with Dokku daemon socket path
|
||||||
super(config, "/var/run/dokku-daemon/dokku-daemon.sock")
|
super(config, "/var/run/dokku-daemon/dokku-daemon.sock")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send a command to Dokku and parse the response
|
||||||
async sendCommand(command: string): Promise<DokkuResponse> {
|
async sendCommand(command: string): Promise<DokkuResponse> {
|
||||||
try {
|
try {
|
||||||
const response = await this.sendData(command)
|
const response = await this.sendData(command)
|
||||||
@ -18,15 +22,18 @@ export class DokkuClient extends SSHSocketClient {
|
|||||||
throw new Error("Received data is not a string")
|
throw new Error("Received data is not a string")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse the JSON response from Dokku
|
||||||
return JSON.parse(response)
|
return JSON.parse(response)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
throw new Error(`Failed to send command: ${error.message}`)
|
throw new Error(`Failed to send command: ${error.message}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// List all deployed Dokku apps
|
||||||
async listApps(): Promise<string[]> {
|
async listApps(): Promise<string[]> {
|
||||||
const response = await this.sendCommand("apps:list")
|
const response = await this.sendCommand("apps:list")
|
||||||
return response.output.split("\n").slice(1) // Split by newline and ignore the first line (header)
|
// Split the output by newline and remove the header
|
||||||
|
return response.output.split("\n").slice(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,11 +12,13 @@ import {
|
|||||||
import { MAX_BODY_SIZE } from "./ratelimit"
|
import { MAX_BODY_SIZE } from "./ratelimit"
|
||||||
import { TFile, TFileData, TFolder } from "./types"
|
import { TFile, TFileData, TFolder } from "./types"
|
||||||
|
|
||||||
|
// Define the structure for sandbox files
|
||||||
export type SandboxFiles = {
|
export type SandboxFiles = {
|
||||||
files: (TFolder | TFile)[]
|
files: (TFolder | TFile)[]
|
||||||
fileData: TFileData[]
|
fileData: TFileData[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FileManager class to handle file operations in a sandbox
|
||||||
export class FileManager {
|
export class FileManager {
|
||||||
private sandboxId: string
|
private sandboxId: string
|
||||||
private sandbox: Sandbox
|
private sandbox: Sandbox
|
||||||
@ -25,6 +27,7 @@ export class FileManager {
|
|||||||
private dirName = "/home/user"
|
private dirName = "/home/user"
|
||||||
private refreshFileList: (files: SandboxFiles) => void
|
private refreshFileList: (files: SandboxFiles) => void
|
||||||
|
|
||||||
|
// Constructor to initialize the FileManager
|
||||||
constructor(
|
constructor(
|
||||||
sandboxId: string,
|
sandboxId: string,
|
||||||
sandbox: Sandbox,
|
sandbox: Sandbox,
|
||||||
@ -36,6 +39,7 @@ export class FileManager {
|
|||||||
this.refreshFileList = refreshFileList
|
this.refreshFileList = refreshFileList
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize the FileManager
|
||||||
async initialize() {
|
async initialize() {
|
||||||
this.sandboxFiles = await getSandboxFiles(this.sandboxId)
|
this.sandboxFiles = await getSandboxFiles(this.sandboxId)
|
||||||
const projectDirectory = path.posix.join(
|
const projectDirectory = path.posix.join(
|
||||||
@ -94,6 +98,7 @@ export class FileManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Watch a directory for changes
|
||||||
async watchDirectory(directory: string): Promise<WatchHandle | undefined> {
|
async watchDirectory(directory: string): Promise<WatchHandle | undefined> {
|
||||||
try {
|
try {
|
||||||
const handle = await this.sandbox.files.watch(
|
const handle = await this.sandbox.files.watch(
|
||||||
@ -130,7 +135,7 @@ export class FileManager {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// A new file or directory was created.
|
// Handle file/directory creation event
|
||||||
if (event.type === "create") {
|
if (event.type === "create") {
|
||||||
const folder = findFolderById(
|
const folder = findFolderById(
|
||||||
this.sandboxFiles.files,
|
this.sandboxFiles.files,
|
||||||
@ -174,7 +179,7 @@ export class FileManager {
|
|||||||
console.log(`Create ${sandboxFilePath}`)
|
console.log(`Create ${sandboxFilePath}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// A file or directory was removed or renamed.
|
// Handle file/directory removal or rename event
|
||||||
else if (event.type === "remove" || event.type == "rename") {
|
else if (event.type === "remove" || event.type == "rename") {
|
||||||
const folder = findFolderById(
|
const folder = findFolderById(
|
||||||
this.sandboxFiles.files,
|
this.sandboxFiles.files,
|
||||||
@ -206,7 +211,7 @@ export class FileManager {
|
|||||||
console.log(`Removed: ${sandboxFilePath}`)
|
console.log(`Removed: ${sandboxFilePath}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The contents of a file were changed.
|
// Handle file write event
|
||||||
else if (event.type === "write") {
|
else if (event.type === "write") {
|
||||||
const folder = findFolderById(
|
const folder = findFolderById(
|
||||||
this.sandboxFiles.files,
|
this.sandboxFiles.files,
|
||||||
@ -259,6 +264,7 @@ export class FileManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Watch subdirectories recursively
|
||||||
async watchSubdirectories(directory: string) {
|
async watchSubdirectories(directory: string) {
|
||||||
const dirContent = await this.sandbox.files.list(directory)
|
const dirContent = await this.sandbox.files.list(directory)
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
@ -271,15 +277,18 @@ export class FileManager {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get file content
|
||||||
async getFile(fileId: string): Promise<string | undefined> {
|
async getFile(fileId: string): Promise<string | undefined> {
|
||||||
const file = this.sandboxFiles.fileData.find((f) => f.id === fileId)
|
const file = this.sandboxFiles.fileData.find((f) => f.id === fileId)
|
||||||
return file?.data
|
return file?.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get folder content
|
||||||
async getFolder(folderId: string): Promise<string[]> {
|
async getFolder(folderId: string): Promise<string[]> {
|
||||||
return getFolder(folderId)
|
return getFolder(folderId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save file content
|
||||||
async saveFile(fileId: string, body: string): Promise<void> {
|
async saveFile(fileId: string, body: string): Promise<void> {
|
||||||
if (!fileId) return // handles saving when no file is open
|
if (!fileId) return // handles saving when no file is open
|
||||||
|
|
||||||
@ -295,6 +304,7 @@ export class FileManager {
|
|||||||
this.fixPermissions()
|
this.fixPermissions()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Move a file to a different folder
|
||||||
async moveFile(
|
async moveFile(
|
||||||
fileId: string,
|
fileId: string,
|
||||||
folderId: string
|
folderId: string
|
||||||
@ -318,6 +328,7 @@ export class FileManager {
|
|||||||
return newFiles.files
|
return newFiles.files
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Move a file within the container
|
||||||
private async moveFileInContainer(oldPath: string, newPath: string) {
|
private async moveFileInContainer(oldPath: string, newPath: string) {
|
||||||
try {
|
try {
|
||||||
const fileContents = await this.sandbox.files.read(
|
const fileContents = await this.sandbox.files.read(
|
||||||
@ -333,6 +344,7 @@ export class FileManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a new file
|
||||||
async createFile(name: string): Promise<boolean> {
|
async createFile(name: string): Promise<boolean> {
|
||||||
const size: number = await getProjectSize(this.sandboxId)
|
const size: number = await getProjectSize(this.sandboxId)
|
||||||
if (size > 200 * 1024 * 1024) {
|
if (size > 200 * 1024 * 1024) {
|
||||||
@ -360,11 +372,13 @@ export class FileManager {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a new folder
|
||||||
async createFolder(name: string): Promise<void> {
|
async createFolder(name: string): Promise<void> {
|
||||||
const id = `projects/${this.sandboxId}/${name}`
|
const id = `projects/${this.sandboxId}/${name}`
|
||||||
await this.sandbox.files.makeDir(path.posix.join(this.dirName, id))
|
await this.sandbox.files.makeDir(path.posix.join(this.dirName, id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rename a file
|
||||||
async renameFile(fileId: string, newName: string): Promise<void> {
|
async renameFile(fileId: string, newName: string): Promise<void> {
|
||||||
const fileData = this.sandboxFiles.fileData.find((f) => f.id === fileId)
|
const fileData = this.sandboxFiles.fileData.find((f) => f.id === fileId)
|
||||||
const file = this.sandboxFiles.files.find((f) => f.id === fileId)
|
const file = this.sandboxFiles.files.find((f) => f.id === fileId)
|
||||||
@ -381,6 +395,7 @@ export class FileManager {
|
|||||||
file.id = newFileId
|
file.id = newFileId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete a file
|
||||||
async deleteFile(fileId: string): Promise<(TFolder | TFile)[]> {
|
async deleteFile(fileId: string): Promise<(TFolder | TFile)[]> {
|
||||||
const file = this.sandboxFiles.fileData.find((f) => f.id === fileId)
|
const file = this.sandboxFiles.fileData.find((f) => f.id === fileId)
|
||||||
if (!file) return this.sandboxFiles.files
|
if (!file) return this.sandboxFiles.files
|
||||||
@ -396,6 +411,7 @@ export class FileManager {
|
|||||||
return newFiles.files
|
return newFiles.files
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete a folder
|
||||||
async deleteFolder(folderId: string): Promise<(TFolder | TFile)[]> {
|
async deleteFolder(folderId: string): Promise<(TFolder | TFile)[]> {
|
||||||
const files = await getFolder(folderId)
|
const files = await getFolder(folderId)
|
||||||
|
|
||||||
@ -413,6 +429,7 @@ export class FileManager {
|
|||||||
return newFiles.files
|
return newFiles.files
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close all file watchers
|
||||||
async closeWatchers() {
|
async closeWatchers() {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
this.fileWatchers.map(async (handle: WatchHandle) => {
|
this.fileWatchers.map(async (handle: WatchHandle) => {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Client } from "ssh2"
|
import { Client } from "ssh2"
|
||||||
|
|
||||||
|
// Interface defining the configuration for SSH connection
|
||||||
export interface SSHConfig {
|
export interface SSHConfig {
|
||||||
host: string
|
host: string
|
||||||
port?: number
|
port?: number
|
||||||
@ -7,25 +8,29 @@ export interface SSHConfig {
|
|||||||
privateKey: Buffer
|
privateKey: Buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Class to handle SSH connections and communicate with a Unix socket
|
||||||
export class SSHSocketClient {
|
export class SSHSocketClient {
|
||||||
private conn: Client
|
private conn: Client
|
||||||
private config: SSHConfig
|
private config: SSHConfig
|
||||||
private socketPath: string
|
private socketPath: string
|
||||||
private isConnected: boolean = false
|
private isConnected: boolean = false
|
||||||
|
|
||||||
|
// Constructor initializes the SSH client and sets up configuration
|
||||||
constructor(config: SSHConfig, socketPath: string) {
|
constructor(config: SSHConfig, socketPath: string) {
|
||||||
this.conn = new Client()
|
this.conn = new Client()
|
||||||
this.config = { ...config, port: 22 }
|
this.config = { ...config, port: 22 } // Default port to 22 if not provided
|
||||||
this.socketPath = socketPath
|
this.socketPath = socketPath
|
||||||
|
|
||||||
this.setupTerminationHandlers()
|
this.setupTerminationHandlers()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set up handlers for graceful termination
|
||||||
private setupTerminationHandlers() {
|
private setupTerminationHandlers() {
|
||||||
process.on("SIGINT", this.closeConnection.bind(this))
|
process.on("SIGINT", this.closeConnection.bind(this))
|
||||||
process.on("SIGTERM", this.closeConnection.bind(this))
|
process.on("SIGTERM", this.closeConnection.bind(this))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Method to close the SSH connection
|
||||||
private closeConnection() {
|
private closeConnection() {
|
||||||
console.log("Closing SSH connection...")
|
console.log("Closing SSH connection...")
|
||||||
this.conn.end()
|
this.conn.end()
|
||||||
@ -33,6 +38,7 @@ export class SSHSocketClient {
|
|||||||
process.exit(0)
|
process.exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Method to establish the SSH connection
|
||||||
connect(): Promise<void> {
|
connect(): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.conn
|
this.conn
|
||||||
@ -54,6 +60,7 @@ export class SSHSocketClient {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Method to send data through the SSH connection to the Unix socket
|
||||||
sendData(data: string): Promise<string> {
|
sendData(data: string): Promise<string> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (!this.isConnected) {
|
if (!this.isConnected) {
|
||||||
@ -61,6 +68,7 @@ export class SSHSocketClient {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use netcat to send data to the Unix socket
|
||||||
this.conn.exec(
|
this.conn.exec(
|
||||||
`echo "${data}" | nc -U ${this.socketPath}`,
|
`echo "${data}" | nc -U ${this.socketPath}`,
|
||||||
(err, stream) => {
|
(err, stream) => {
|
||||||
|
@ -20,12 +20,14 @@ import { TerminalManager } from "./TerminalManager"
|
|||||||
import { User } from "./types"
|
import { User } from "./types"
|
||||||
import { LockManager } from "./utils"
|
import { LockManager } from "./utils"
|
||||||
|
|
||||||
|
// Handle uncaught exceptions
|
||||||
process.on("uncaughtException", (error) => {
|
process.on("uncaughtException", (error) => {
|
||||||
console.error("Uncaught Exception:", error)
|
console.error("Uncaught Exception:", error)
|
||||||
// Do not exit the process
|
// Do not exit the process
|
||||||
// You can add additional logging or recovery logic here
|
// You can add additional logging or recovery logic here
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Handle unhandled promise rejections
|
||||||
process.on("unhandledRejection", (reason, promise) => {
|
process.on("unhandledRejection", (reason, promise) => {
|
||||||
console.error("Unhandled Rejection at:", promise, "reason:", reason)
|
console.error("Unhandled Rejection at:", promise, "reason:", reason)
|
||||||
// Do not exit the process
|
// Do not exit the process
|
||||||
@ -35,8 +37,10 @@ process.on("unhandledRejection", (reason, promise) => {
|
|||||||
// The amount of time in ms that a container will stay alive without a hearbeat.
|
// The amount of time in ms that a container will stay alive without a hearbeat.
|
||||||
const CONTAINER_TIMEOUT = 120_000
|
const CONTAINER_TIMEOUT = 120_000
|
||||||
|
|
||||||
|
// Load environment variables
|
||||||
dotenv.config()
|
dotenv.config()
|
||||||
|
|
||||||
|
// Initialize Express app and create HTTP server
|
||||||
const app: Express = express()
|
const app: Express = express()
|
||||||
const port = process.env.PORT || 4000
|
const port = process.env.PORT || 4000
|
||||||
app.use(cors())
|
app.use(cors())
|
||||||
@ -47,10 +51,12 @@ const io = new Server(httpServer, {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Check if the sandbox owner is connected
|
||||||
function isOwnerConnected(sandboxId: string): boolean {
|
function isOwnerConnected(sandboxId: string): boolean {
|
||||||
return (connections[sandboxId] ?? 0) > 0
|
return (connections[sandboxId] ?? 0) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract port number from a string
|
||||||
function extractPortNumber(inputString: string): number | null {
|
function extractPortNumber(inputString: string): number | null {
|
||||||
const cleanedString = inputString.replace(/\x1B\[[0-9;]*m/g, "")
|
const cleanedString = inputString.replace(/\x1B\[[0-9;]*m/g, "")
|
||||||
const regex = /http:\/\/localhost:(\d+)/
|
const regex = /http:\/\/localhost:(\d+)/
|
||||||
@ -58,12 +64,15 @@ function extractPortNumber(inputString: string): number | null {
|
|||||||
return match ? parseInt(match[1]) : null
|
return match ? parseInt(match[1]) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize containers and managers
|
||||||
const containers: Record<string, Sandbox> = {}
|
const containers: Record<string, Sandbox> = {}
|
||||||
const connections: Record<string, number> = {}
|
const connections: Record<string, number> = {}
|
||||||
const fileManagers: Record<string, FileManager> = {}
|
const fileManagers: Record<string, FileManager> = {}
|
||||||
const terminalManagers: Record<string, TerminalManager> = {}
|
const terminalManagers: Record<string, TerminalManager> = {}
|
||||||
|
|
||||||
|
// Middleware for socket authentication
|
||||||
io.use(async (socket, next) => {
|
io.use(async (socket, next) => {
|
||||||
|
// Define the schema for handshake query validation
|
||||||
const handshakeSchema = z.object({
|
const handshakeSchema = z.object({
|
||||||
userId: z.string(),
|
userId: z.string(),
|
||||||
sandboxId: z.string(),
|
sandboxId: z.string(),
|
||||||
@ -74,12 +83,14 @@ io.use(async (socket, next) => {
|
|||||||
const q = socket.handshake.query
|
const q = socket.handshake.query
|
||||||
const parseQuery = handshakeSchema.safeParse(q)
|
const parseQuery = handshakeSchema.safeParse(q)
|
||||||
|
|
||||||
|
// Check if the query is valid according to the schema
|
||||||
if (!parseQuery.success) {
|
if (!parseQuery.success) {
|
||||||
next(new Error("Invalid request."))
|
next(new Error("Invalid request."))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const { sandboxId, userId } = parseQuery.data
|
const { sandboxId, userId } = parseQuery.data
|
||||||
|
// Fetch user data from the database
|
||||||
const dbUser = await fetch(
|
const dbUser = await fetch(
|
||||||
`${process.env.DATABASE_WORKER_URL}/api/user?id=${userId}`,
|
`${process.env.DATABASE_WORKER_URL}/api/user?id=${userId}`,
|
||||||
{
|
{
|
||||||
@ -90,32 +101,39 @@ io.use(async (socket, next) => {
|
|||||||
)
|
)
|
||||||
const dbUserJSON = (await dbUser.json()) as User
|
const dbUserJSON = (await dbUser.json()) as User
|
||||||
|
|
||||||
|
// Check if user data was retrieved successfully
|
||||||
if (!dbUserJSON) {
|
if (!dbUserJSON) {
|
||||||
next(new Error("DB error."))
|
next(new Error("DB error."))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the user owns the sandbox or has shared access
|
||||||
const sandbox = dbUserJSON.sandbox.find((s) => s.id === sandboxId)
|
const sandbox = dbUserJSON.sandbox.find((s) => s.id === sandboxId)
|
||||||
const sharedSandboxes = dbUserJSON.usersToSandboxes.find(
|
const sharedSandboxes = dbUserJSON.usersToSandboxes.find(
|
||||||
(uts) => uts.sandboxId === sandboxId
|
(uts) => uts.sandboxId === sandboxId
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// If user doesn't own or have shared access to the sandbox, deny access
|
||||||
if (!sandbox && !sharedSandboxes) {
|
if (!sandbox && !sharedSandboxes) {
|
||||||
next(new Error("Invalid credentials."))
|
next(new Error("Invalid credentials."))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set socket data with user information
|
||||||
socket.data = {
|
socket.data = {
|
||||||
userId,
|
userId,
|
||||||
sandboxId: sandboxId,
|
sandboxId: sandboxId,
|
||||||
isOwner: sandbox !== undefined,
|
isOwner: sandbox !== undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Allow the connection
|
||||||
next()
|
next()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Initialize lock manager
|
||||||
const lockManager = new LockManager()
|
const lockManager = new LockManager()
|
||||||
|
|
||||||
|
// Check for required environment variables
|
||||||
if (!process.env.DOKKU_HOST)
|
if (!process.env.DOKKU_HOST)
|
||||||
console.error("Environment variable DOKKU_HOST is not defined")
|
console.error("Environment variable DOKKU_HOST is not defined")
|
||||||
if (!process.env.DOKKU_USERNAME)
|
if (!process.env.DOKKU_USERNAME)
|
||||||
@ -123,6 +141,7 @@ if (!process.env.DOKKU_USERNAME)
|
|||||||
if (!process.env.DOKKU_KEY)
|
if (!process.env.DOKKU_KEY)
|
||||||
console.error("Environment variable DOKKU_KEY is not defined")
|
console.error("Environment variable DOKKU_KEY is not defined")
|
||||||
|
|
||||||
|
// Initialize Dokku client
|
||||||
const client =
|
const client =
|
||||||
process.env.DOKKU_HOST && process.env.DOKKU_KEY && process.env.DOKKU_USERNAME
|
process.env.DOKKU_HOST && process.env.DOKKU_KEY && process.env.DOKKU_USERNAME
|
||||||
? new DokkuClient({
|
? new DokkuClient({
|
||||||
@ -133,6 +152,7 @@ const client =
|
|||||||
: null
|
: null
|
||||||
client?.connect()
|
client?.connect()
|
||||||
|
|
||||||
|
// Initialize Git client used to deploy Dokku apps
|
||||||
const git =
|
const git =
|
||||||
process.env.DOKKU_HOST && process.env.DOKKU_KEY
|
process.env.DOKKU_HOST && process.env.DOKKU_KEY
|
||||||
? new SecureGitClient(
|
? new SecureGitClient(
|
||||||
@ -141,6 +161,7 @@ const git =
|
|||||||
)
|
)
|
||||||
: null
|
: null
|
||||||
|
|
||||||
|
// Handle socket connections
|
||||||
io.on("connection", async (socket) => {
|
io.on("connection", async (socket) => {
|
||||||
try {
|
try {
|
||||||
const data = socket.data as {
|
const data = socket.data as {
|
||||||
@ -149,6 +170,7 @@ io.on("connection", async (socket) => {
|
|||||||
isOwner: boolean
|
isOwner: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle connection based on user type (owner or not)
|
||||||
if (data.isOwner) {
|
if (data.isOwner) {
|
||||||
connections[data.sandboxId] = (connections[data.sandboxId] ?? 0) + 1
|
connections[data.sandboxId] = (connections[data.sandboxId] ?? 0) + 1
|
||||||
} else {
|
} else {
|
||||||
@ -158,6 +180,7 @@ io.on("connection", async (socket) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create or retrieve container
|
||||||
const createdContainer = await lockManager.acquireLock(
|
const createdContainer = await lockManager.acquireLock(
|
||||||
data.sandboxId,
|
data.sandboxId,
|
||||||
async () => {
|
async () => {
|
||||||
@ -180,10 +203,12 @@ io.on("connection", async (socket) => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Function to send loaded event
|
||||||
const sendLoadedEvent = (files: SandboxFiles) => {
|
const sendLoadedEvent = (files: SandboxFiles) => {
|
||||||
socket.emit("loaded", files.files)
|
socket.emit("loaded", files.files)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize file and terminal managers if container was created
|
||||||
if (createdContainer) {
|
if (createdContainer) {
|
||||||
fileManagers[data.sandboxId] = new FileManager(
|
fileManagers[data.sandboxId] = new FileManager(
|
||||||
data.sandboxId,
|
data.sandboxId,
|
||||||
@ -203,6 +228,7 @@ io.on("connection", async (socket) => {
|
|||||||
// Load file list from the file manager into the editor
|
// Load file list from the file manager into the editor
|
||||||
sendLoadedEvent(fileManager.sandboxFiles)
|
sendLoadedEvent(fileManager.sandboxFiles)
|
||||||
|
|
||||||
|
// Handle various socket events (heartbeat, file operations, terminal operations, etc.)
|
||||||
socket.on("heartbeat", async () => {
|
socket.on("heartbeat", async () => {
|
||||||
try {
|
try {
|
||||||
// This keeps the container alive for another CONTAINER_TIMEOUT seconds.
|
// This keeps the container alive for another CONTAINER_TIMEOUT seconds.
|
||||||
@ -214,6 +240,7 @@ io.on("connection", async (socket) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Handle request to get file content
|
||||||
socket.on("getFile", async (fileId: string, callback) => {
|
socket.on("getFile", async (fileId: string, callback) => {
|
||||||
try {
|
try {
|
||||||
const fileContent = await fileManager.getFile(fileId)
|
const fileContent = await fileManager.getFile(fileId)
|
||||||
@ -224,6 +251,7 @@ io.on("connection", async (socket) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Handle request to get folder contents
|
||||||
socket.on("getFolder", async (folderId: string, callback) => {
|
socket.on("getFolder", async (folderId: string, callback) => {
|
||||||
try {
|
try {
|
||||||
const files = await fileManager.getFolder(folderId)
|
const files = await fileManager.getFolder(folderId)
|
||||||
@ -234,6 +262,7 @@ io.on("connection", async (socket) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Handle request to save file
|
||||||
socket.on("saveFile", async (fileId: string, body: string) => {
|
socket.on("saveFile", async (fileId: string, body: string) => {
|
||||||
try {
|
try {
|
||||||
await saveFileRL.consume(data.userId, 1)
|
await saveFileRL.consume(data.userId, 1)
|
||||||
@ -244,6 +273,7 @@ io.on("connection", async (socket) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Handle request to move file
|
||||||
socket.on(
|
socket.on(
|
||||||
"moveFile",
|
"moveFile",
|
||||||
async (fileId: string, folderId: string, callback) => {
|
async (fileId: string, folderId: string, callback) => {
|
||||||
@ -263,6 +293,7 @@ io.on("connection", async (socket) => {
|
|||||||
message?: string
|
message?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle request to list apps
|
||||||
socket.on(
|
socket.on(
|
||||||
"list",
|
"list",
|
||||||
async (callback: (response: CallbackResponse) => void) => {
|
async (callback: (response: CallbackResponse) => void) => {
|
||||||
@ -283,6 +314,7 @@ io.on("connection", async (socket) => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Handle request to deploy project
|
||||||
socket.on(
|
socket.on(
|
||||||
"deploy",
|
"deploy",
|
||||||
async (callback: (response: CallbackResponse) => void) => {
|
async (callback: (response: CallbackResponse) => void) => {
|
||||||
@ -313,6 +345,7 @@ io.on("connection", async (socket) => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Handle request to create a new file
|
||||||
socket.on("createFile", async (name: string, callback) => {
|
socket.on("createFile", async (name: string, callback) => {
|
||||||
try {
|
try {
|
||||||
await createFileRL.consume(data.userId, 1)
|
await createFileRL.consume(data.userId, 1)
|
||||||
@ -324,6 +357,7 @@ io.on("connection", async (socket) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Handle request to create a new folder
|
||||||
socket.on("createFolder", async (name: string, callback) => {
|
socket.on("createFolder", async (name: string, callback) => {
|
||||||
try {
|
try {
|
||||||
await createFolderRL.consume(data.userId, 1)
|
await createFolderRL.consume(data.userId, 1)
|
||||||
@ -335,6 +369,7 @@ io.on("connection", async (socket) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Handle request to rename a file
|
||||||
socket.on("renameFile", async (fileId: string, newName: string) => {
|
socket.on("renameFile", async (fileId: string, newName: string) => {
|
||||||
try {
|
try {
|
||||||
await renameFileRL.consume(data.userId, 1)
|
await renameFileRL.consume(data.userId, 1)
|
||||||
@ -345,6 +380,7 @@ io.on("connection", async (socket) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Handle request to delete a file
|
||||||
socket.on("deleteFile", async (fileId: string, callback) => {
|
socket.on("deleteFile", async (fileId: string, callback) => {
|
||||||
try {
|
try {
|
||||||
await deleteFileRL.consume(data.userId, 1)
|
await deleteFileRL.consume(data.userId, 1)
|
||||||
@ -356,6 +392,7 @@ io.on("connection", async (socket) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Handle request to delete a folder
|
||||||
socket.on("deleteFolder", async (folderId: string, callback) => {
|
socket.on("deleteFolder", async (folderId: string, callback) => {
|
||||||
try {
|
try {
|
||||||
const newFiles = await fileManager.deleteFolder(folderId)
|
const newFiles = await fileManager.deleteFolder(folderId)
|
||||||
@ -366,6 +403,7 @@ io.on("connection", async (socket) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Handle request to create a new terminal
|
||||||
socket.on("createTerminal", async (id: string, callback) => {
|
socket.on("createTerminal", async (id: string, callback) => {
|
||||||
try {
|
try {
|
||||||
await lockManager.acquireLock(data.sandboxId, async () => {
|
await lockManager.acquireLock(data.sandboxId, async () => {
|
||||||
@ -387,6 +425,7 @@ io.on("connection", async (socket) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Handle request to resize terminal
|
||||||
socket.on(
|
socket.on(
|
||||||
"resizeTerminal",
|
"resizeTerminal",
|
||||||
(dimensions: { cols: number; rows: number }) => {
|
(dimensions: { cols: number; rows: number }) => {
|
||||||
@ -399,6 +438,7 @@ io.on("connection", async (socket) => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Handle terminal input data
|
||||||
socket.on("terminalData", async (id: string, data: string) => {
|
socket.on("terminalData", async (id: string, data: string) => {
|
||||||
try {
|
try {
|
||||||
await terminalManager.sendTerminalData(id, data)
|
await terminalManager.sendTerminalData(id, data)
|
||||||
@ -408,6 +448,7 @@ io.on("connection", async (socket) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Handle request to close terminal
|
||||||
socket.on("closeTerminal", async (id: string, callback) => {
|
socket.on("closeTerminal", async (id: string, callback) => {
|
||||||
try {
|
try {
|
||||||
await terminalManager.closeTerminal(id)
|
await terminalManager.closeTerminal(id)
|
||||||
@ -418,6 +459,7 @@ io.on("connection", async (socket) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Handle request to generate code
|
||||||
socket.on(
|
socket.on(
|
||||||
"generateCode",
|
"generateCode",
|
||||||
async (
|
async (
|
||||||
@ -442,7 +484,7 @@ io.on("connection", async (socket) => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Generate code from cloudflare workers AI
|
// Generate code from Cloudflare Workers AI
|
||||||
const generateCodePromise = fetch(
|
const generateCodePromise = fetch(
|
||||||
`${process.env.AI_WORKER_URL}/api?fileName=${encodeURIComponent(
|
`${process.env.AI_WORKER_URL}/api?fileName=${encodeURIComponent(
|
||||||
fileName
|
fileName
|
||||||
@ -472,6 +514,7 @@ io.on("connection", async (socket) => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Handle socket disconnection
|
||||||
socket.on("disconnect", async () => {
|
socket.on("disconnect", async () => {
|
||||||
try {
|
try {
|
||||||
if (data.isOwner) {
|
if (data.isOwner) {
|
||||||
@ -498,6 +541,7 @@ io.on("connection", async (socket) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Start the server
|
||||||
httpServer.listen(port, () => {
|
httpServer.listen(port, () => {
|
||||||
console.log(`Server running on port ${port}`)
|
console.log(`Server running on port ${port}`)
|
||||||
})
|
})
|
||||||
|
Loading…
x
Reference in New Issue
Block a user