initPaths(); // Load autoloader for Swoole/Utopia classes require_once $this->paths['vendor']; // Load module classes after autoloader is available require_once __DIR__ . '/Module.php'; require_once __DIR__ . '/Services/Http.php'; require_once __DIR__ . '/Http/Installer/View.php'; require_once __DIR__ . '/Http/Installer/Status.php'; require_once __DIR__ . '/Http/Installer/Validate.php'; require_once __DIR__ . '/Http/Installer/Complete.php'; require_once __DIR__ . '/Http/Installer/Install.php'; require_once __DIR__ . '/Http/Installer/Error.php'; $this->state = new State($this->paths); if (PHP_SAPI === 'cli') { $this->runCli(); return; } } private function initPaths(): void { if (!empty($this->paths)) { return; } $root = dirname(__DIR__, 4); $this->paths = [ 'public' => $root . '/public', 'init' => $root . '/app/init.php', 'views' => $root . '/app/views/install', 'vendor' => $root . '/vendor/autoload.php', 'installPhp' => $root . '/src/Appwrite/Platform/Tasks/Install.php', ]; } private function runCli(): void { $opts = getopt('', ['upgrade', 'locked-database::', 'docker', 'clean']); $cfg = $this->state->buildConfig([], true); $isDocker = isset($opts['docker']); if ($isDocker) { $cfg->setIsLocal(true); if ($cfg->getHostPath() === null) { $cwd = getcwd(); if ($cwd !== false) { $cfg->setHostPath($cwd); } } } if (isset($opts['upgrade'])) { $cfg->setIsUpgrade(true); } if (!empty($opts['locked-database'])) { $cfg->setLockedDatabase($opts['locked-database']); } $this->state->applyEnvConfig($cfg); $host = self::INSTALLER_WEB_HOST; $port = (string) self::INSTALLER_WEB_PORT; if (isset($opts['clean'])) { $this->removeDockerInstallerContainer(self::DEFAULT_CONTAINER); $this->cleanupWebInstallerFiles(); exit(0); } if (isset($opts['docker'])) { $this->printInstallerUrl($host, $port); $this->startDockerInstaller($opts); } $this->printInstallerUrl($host, $port); $this->startSwooleServer($host, (int) $port); } private function printInstallerUrl(string $host, string $port): void { $displayHost = $host === self::INSTALLER_WEB_HOST ? 'localhost' : $host; $url = "http://$displayHost:$port"; fwrite(STDOUT, "Open $url" . PHP_EOL); } private function startSwooleServer(string $host, int $port): void { $server = new \Swoole\Http\Server($host, $port); $server->set([ 'worker_num' => 1, ]); // Preload static files into memory $files = new \Utopia\Http\Files(); $files->load($this->paths['views']); // Register resources for dependency injection into actions $config = $this->state->buildConfig(); $paths = $this->paths; $state = $this->state; \Utopia\Http\Http::setResource('installerState', fn () => $state); \Utopia\Http\Http::setResource('installerConfig', fn () => $config); \Utopia\Http\Http::setResource('installerPaths', fn () => $paths); \Utopia\Http\Http::setResource('swooleServer', fn () => $server); // Register routes via module structure $module = new Module(); $services = $module->getServicesByType(\Utopia\Platform\Service::TYPE_HTTP); foreach ($services as $service) { foreach ($service->getActions() as $action) { $type = $action->getType(); switch ($type) { case \Utopia\Platform\Action::TYPE_ERROR: $hook = \Utopia\Http\Http::error(); break; default: $httpMethod = $action->getHttpMethod(); $httpPath = $action->getHttpPath(); $hook = \Utopia\Http\Http::addRoute($httpMethod, $httpPath); break; } $hook->desc($action->getDesc() ?? ''); foreach ($action->getOptions() as $key => $option) { if ($option['type'] === 'injection') { $hook->inject($option['name']); } } $hook->action($action->getCallback()); } } $server->on('start', function (\Swoole\Http\Server $srv) { \Swoole\Process::signal(SIGINT, function () use ($srv) { $srv->shutdown(); }); }); $server->on('request', function (\Swoole\Http\Request $swooleRequest, \Swoole\Http\Response $swooleResponse) use ($files) { \Utopia\Http\Http::setResource('swooleRequest', fn () => $swooleRequest); \Utopia\Http\Http::setResource('swooleResponse', fn () => $swooleResponse); $request = new \Utopia\Http\Adapter\Swoole\Request($swooleRequest); $response = new \Utopia\Http\Adapter\Swoole\Response($swooleResponse); // Serve static files from memory $uri = $request->getURI(); if ($files->isFileLoaded($uri)) { $response ->setContentType($files->getFileMimeType($uri)) ->send($files->getFileContents($uri)); return; } $app = new \Utopia\Http\Http('UTC'); $app->run($request, $response); }); $server->start(); } private function removeDockerInstallerContainer(string $container): void { $name = escapeshellarg($container); exec("docker rm -f $name >/dev/null 2>&1"); } private function cleanupWebInstallerFiles(): void { $cwd = getcwd(); if ($cwd === false) { return; } $filesToRemove = [ $cwd . '/.env.web-installer', $cwd . '/docker-compose.web-installer.yml', ]; foreach ($filesToRemove as $file) { if (file_exists($file)) { @unlink($file); } } $tempDir = sys_get_temp_dir(); @unlink(self::INSTALLER_LOCK_FILE); @unlink(self::INSTALLER_CONFIG_FILE); foreach ((array) glob($tempDir . '/appwrite-install-*.json') as $file) { @unlink($file); } } private function dockerImageExists(string $image): bool { $result = 1; exec("docker image inspect " . escapeshellarg($image) . " >/dev/null 2>&1", $output, $result); return $result === 0; } private function buildDockerInstallerImage(string $image): void { fwrite(STDOUT, "Building Docker image: {$image}\n"); $buildCommand = 'docker compose build appwrite'; passthru($buildCommand, $status); if ($status !== 0 || !$this->dockerImageExists($image)) { fwrite(STDERR, "Failed to build Docker image: $image\n"); fwrite(STDERR, "Try: docker compose build appwrite\n"); exit(1); } } private function ensureLocalInstallerTag(string $source, string $target): void { $sourceArg = escapeshellarg($source); $targetArg = escapeshellarg($target); exec("docker tag {$sourceArg} {$targetArg}", $tagOutput, $tagStatus); if ($tagStatus !== 0) { fwrite(STDERR, "Failed to tag Docker image {$source} as {$target}\n"); exit(1); } } private function startDockerInstaller(array $opts): void { $image = self::DEFAULT_IMAGE; $container = self::DEFAULT_CONTAINER; if (!$this->dockerImageExists($image)) { $this->buildDockerInstallerImage($image); } $this->ensureLocalInstallerTag($image, 'appwrite/appwrite:local'); $port = (string)self::INSTALLER_WEB_PORT; $entrypoint = isset($opts['upgrade']) ? 'upgrade' : 'install'; $this->removeDockerInstallerContainer($container); $root = realpath(dirname(__DIR__, 4)); $volumePath = $root !== false ? $root : (getcwd() ?: '.'); $dockerConfig = $this->state->buildConfig([], false); $dockerConfig->setIsLocal(true); $dockerConfig->setHostPath($volumePath); if (isset($opts['upgrade'])) { $dockerConfig->setIsUpgrade(true); } if (!empty($opts['locked-database'])) { $dockerConfig->setLockedDatabase($opts['locked-database']); } $configJson = json_encode($dockerConfig->toArray(), JSON_UNESCAPED_SLASHES); if (!is_string($configJson)) { $configJson = '{}'; } $args = [ 'docker', 'run', '-i', '--rm', '--name', $container, '-p', "127.0.0.1:$port:" . self::INSTALLER_WEB_PORT, '--volume', '/var/run/docker.sock:/var/run/docker.sock', '--volume', "$volumePath:/usr/src/code:rw", ]; $args[] = '-e'; $args[] = 'APPWRITE_INSTALLER_CONFIG=' . $configJson; $args[] = '--entrypoint=' . $entrypoint; $args[] = $image; $command = implode(' ', array_map(escapeshellarg(...), $args)); passthru($command, $status); exit($status); } } /** * Run server only on direct CLI execution. */ function shouldRunInstallerServer(): bool { return PHP_SAPI === 'cli' && realpath($_SERVER['SCRIPT_FILENAME'] ?? '') === realpath(__FILE__); } if (shouldRunInstallerServer()) { $server = new Server(); $server->run(); }