diff --git a/Dockerfile b/Dockerfile index 2bb9f80d9e..c40ea06d1c 100755 --- a/Dockerfile +++ b/Dockerfile @@ -68,6 +68,7 @@ RUN chmod +x /usr/local/bin/doctor && \ chmod +x /usr/local/bin/sdks && \ chmod +x /usr/local/bin/specs && \ chmod +x /usr/local/bin/ssl && \ + chmod +x /usr/local/bin/screenshot && \ chmod +x /usr/local/bin/test && \ chmod +x /usr/local/bin/upgrade && \ chmod +x /usr/local/bin/vars && \ diff --git a/app/controllers/general.php b/app/controllers/general.php index 132647cd2f..15ae51be67 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -208,6 +208,11 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw 'site' => $runtimes[$resource->getAttribute('buildRuntime')] ?? null, default => null }; + + // Static site enforced runtime + if ($resource->getAttribute('adapter', '') === 'static') { + $runtime = $runtimes['static-1'] ?? null; + } if (\is_null($runtime)) { throw new AppwriteException(AppwriteException::FUNCTION_RUNTIME_UNSUPPORTED, 'Runtime "' . $resource->getAttribute('runtime', '') . '" is not supported'); diff --git a/bin/screenshot b/bin/screenshot new file mode 100755 index 0000000000..4d8ceb998f --- /dev/null +++ b/bin/screenshot @@ -0,0 +1,3 @@ +#!/bin/sh + +php /usr/src/code/app/cli.php screenshot $@ \ No newline at end of file diff --git a/bin/ssl b/bin/ssl index 83dcf6a026..4d8ceb998f 100755 --- a/bin/ssl +++ b/bin/ssl @@ -1,3 +1,3 @@ #!/bin/sh -php /usr/src/code/app/cli.php ssl $@ \ No newline at end of file +php /usr/src/code/app/cli.php screenshot $@ \ No newline at end of file diff --git a/public/images/sites/templates/.gitkeep b/public/images/sites/templates/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/public/images/sites/templates/astro-starter-dark.png b/public/images/sites/templates/astro-starter-dark.png new file mode 100644 index 0000000000..cf7c35f637 Binary files /dev/null and b/public/images/sites/templates/astro-starter-dark.png differ diff --git a/public/images/sites/templates/astro-starter-light.png b/public/images/sites/templates/astro-starter-light.png new file mode 100644 index 0000000000..cf7c35f637 Binary files /dev/null and b/public/images/sites/templates/astro-starter-light.png differ diff --git a/public/images/sites/templates/flutter-starter-dark.png b/public/images/sites/templates/flutter-starter-dark.png new file mode 100644 index 0000000000..82c78d0c7d Binary files /dev/null and b/public/images/sites/templates/flutter-starter-dark.png differ diff --git a/public/images/sites/templates/flutter-starter-light.png b/public/images/sites/templates/flutter-starter-light.png new file mode 100644 index 0000000000..82c78d0c7d Binary files /dev/null and b/public/images/sites/templates/flutter-starter-light.png differ diff --git a/public/images/sites/templates/remix-starter-dark.png b/public/images/sites/templates/remix-starter-dark.png new file mode 100644 index 0000000000..092e96da32 Binary files /dev/null and b/public/images/sites/templates/remix-starter-dark.png differ diff --git a/public/images/sites/templates/remix-starter-light.png b/public/images/sites/templates/remix-starter-light.png new file mode 100644 index 0000000000..f2ef9a9baf Binary files /dev/null and b/public/images/sites/templates/remix-starter-light.png differ diff --git a/public/images/sites/templates/starter-for-nuxt-dark.png b/public/images/sites/templates/starter-for-nuxt-dark.png new file mode 100644 index 0000000000..ab8884febd Binary files /dev/null and b/public/images/sites/templates/starter-for-nuxt-dark.png differ diff --git a/public/images/sites/templates/starter-for-nuxt-light.png b/public/images/sites/templates/starter-for-nuxt-light.png new file mode 100644 index 0000000000..ab8884febd Binary files /dev/null and b/public/images/sites/templates/starter-for-nuxt-light.png differ diff --git a/public/images/sites/templates/starter-for-react-dark.png b/public/images/sites/templates/starter-for-react-dark.png new file mode 100644 index 0000000000..8d5d067b13 Binary files /dev/null and b/public/images/sites/templates/starter-for-react-dark.png differ diff --git a/public/images/sites/templates/starter-for-react-light.png b/public/images/sites/templates/starter-for-react-light.png new file mode 100644 index 0000000000..8d5d067b13 Binary files /dev/null and b/public/images/sites/templates/starter-for-react-light.png differ diff --git a/public/images/sites/templates/starter-for-vue-dark.png b/public/images/sites/templates/starter-for-vue-dark.png new file mode 100644 index 0000000000..8d5d067b13 Binary files /dev/null and b/public/images/sites/templates/starter-for-vue-dark.png differ diff --git a/public/images/sites/templates/starter-for-vue-light.png b/public/images/sites/templates/starter-for-vue-light.png new file mode 100644 index 0000000000..8d5d067b13 Binary files /dev/null and b/public/images/sites/templates/starter-for-vue-light.png differ diff --git a/public/images/sites/templates/template-for-blog-dark.png b/public/images/sites/templates/template-for-blog-dark.png new file mode 100644 index 0000000000..ff29bbd97f Binary files /dev/null and b/public/images/sites/templates/template-for-blog-dark.png differ diff --git a/public/images/sites/templates/template-for-blog-light.png b/public/images/sites/templates/template-for-blog-light.png new file mode 100644 index 0000000000..ff29bbd97f Binary files /dev/null and b/public/images/sites/templates/template-for-blog-light.png differ diff --git a/public/images/sites/templates/template-for-onelink-dark.png b/public/images/sites/templates/template-for-onelink-dark.png new file mode 100644 index 0000000000..4680399672 Binary files /dev/null and b/public/images/sites/templates/template-for-onelink-dark.png differ diff --git a/public/images/sites/templates/template-for-onelink-light.png b/public/images/sites/templates/template-for-onelink-light.png new file mode 100644 index 0000000000..7a040b5d8d Binary files /dev/null and b/public/images/sites/templates/template-for-onelink-light.png differ diff --git a/public/images/sites/templates/template-for-portfolio-dark.png b/public/images/sites/templates/template-for-portfolio-dark.png new file mode 100644 index 0000000000..4b4382bf80 Binary files /dev/null and b/public/images/sites/templates/template-for-portfolio-dark.png differ diff --git a/public/images/sites/templates/template-for-portfolio-light.png b/public/images/sites/templates/template-for-portfolio-light.png new file mode 100644 index 0000000000..046f0af0de Binary files /dev/null and b/public/images/sites/templates/template-for-portfolio-light.png differ diff --git a/src/Appwrite/Platform/Services/Tasks.php b/src/Appwrite/Platform/Services/Tasks.php index 6a6cb3237a..3ada193cf7 100644 --- a/src/Appwrite/Platform/Services/Tasks.php +++ b/src/Appwrite/Platform/Services/Tasks.php @@ -10,6 +10,7 @@ use Appwrite\Platform\Tasks\QueueRetry; use Appwrite\Platform\Tasks\ScheduleExecutions; use Appwrite\Platform\Tasks\ScheduleFunctions; use Appwrite\Platform\Tasks\ScheduleMessages; +use Appwrite\Platform\Tasks\Screenshot; use Appwrite\Platform\Tasks\SDKs; use Appwrite\Platform\Tasks\Specs; use Appwrite\Platform\Tasks\SSL; @@ -32,6 +33,7 @@ class Tasks extends Service ->addAction(QueueRetry::getName(), new QueueRetry()) ->addAction(SDKs::getName(), new SDKs()) ->addAction(SSL::getName(), new SSL()) + ->addAction(Screenshot::getName(), new Screenshot()) ->addAction(ScheduleFunctions::getName(), new ScheduleFunctions()) ->addAction(ScheduleExecutions::getName(), new ScheduleExecutions()) ->addAction(ScheduleMessages::getName(), new ScheduleMessages()) diff --git a/src/Appwrite/Platform/Tasks/Screenshot.php b/src/Appwrite/Platform/Tasks/Screenshot.php new file mode 100644 index 0000000000..6d2ab838c9 --- /dev/null +++ b/src/Appwrite/Platform/Tasks/Screenshot.php @@ -0,0 +1,264 @@ +desc('Create Site template screenshot') + ->param('templateId', '', new Text(128), 'Template ID.') + ->callback(fn (string $templateId) => $this->action($templateId)); + } + + public function action(string $templateId): void + { + $templates = Config::getParam('site-templates', []); + + $allowedTemplates = \array_filter($templates, function ($item) use ($templateId) { + return $item['key'] === $templateId; + }); + $template = \array_shift($allowedTemplates); + + if (empty($template)) { + throw new \Exception("Template {$templateId} not found. Find correct ID in app/config/site-templates.php"); + } + + Console::info("Found: " . $template['name']); + + $client = new Client(); + $client->setEndpoint('http://localhost/v1'); + $client->addHeader('origin', 'http://localhost'); + + // Register + $email = uniqid() . 'user@localhost.test'; + $password = 'password'; + $user = $client->call(Client::METHOD_POST, '/account', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => 'console', + ], [ + 'userId' => ID::unique(), + 'email' => $email, + 'password' => $password, + ]); + + if($user['headers']['status-code'] !== 201) { + Console::error(\json_encode($user)); + throw new \Exception("Failed to register user"); + } + + Console::info("User created"); + + // Login + $session = $client->call(Client::METHOD_POST, '/account/sessions/email', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => 'console', + ], [ + 'email' => $email, + 'password' => $password, + ]); + + if($session['headers']['status-code'] !== 201) { + Console::error(\json_encode($session)); + throw new \Exception("Failed to login user"); + } + + Console::info("Session created"); + + $secret = $session['cookies']['a_session_console']; + $cookieConsole = 'a_session_console=' . $secret; + + // Create organization + $team = $client->call(Client::METHOD_POST, '/teams', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => 'console', + 'cookie' => $cookieConsole + ], [ + 'teamId' => ID::unique(), + 'name' => 'Demo Project Team', + ]); + + if($team['headers']['status-code'] !== 201) { + Console::error(\json_encode($team)); + throw new \Exception("Failed to create team"); + } + + Console::info("Team created"); + + // Create project + $project = $client->call(Client::METHOD_POST, '/projects', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => 'console', + 'cookie' => $cookieConsole + ], [ + 'projectId' => ID::unique(), + 'region' => 'default', + 'name' => 'Demo Project', + 'teamId' => $team['body']['$id'], + 'description' => 'Demo Project Description', + 'url' => 'https://appwrite.io', + ]); + + if($project['headers']['status-code'] !== 201) { + Console::error(\json_encode($project)); + throw new \Exception("Failed to create project"); + } + + Console::info("Project created"); + + $projectId = $project['body']['$id']; + + $framework = $template['frameworks'][0]; + + // Create site + $site = $client->call(Client::METHOD_POST, '/sites', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-mode' => 'admin', + 'cookie' => $cookieConsole + ], [ + 'siteId' => ID::unique(), + 'name' => $template["name"], + 'framework' => $framework['key'], + 'adapter' => $framework['adapter'], + 'buildCommand' => $framework['buildCommand'], + 'buildRuntime' => $framework['buildRuntime'], + 'fallbackFile' => $framework['fallbackFile'], + 'installCommand' => $framework['installCommand'], + 'outputDirectory' => $framework['outputDirectory'], + 'providerRootDirectory' => $framework['providerRootDirectory'], + ]); + + if($site['headers']['status-code'] !== 201) { + Console::error(\json_encode($site)); + throw new \Exception("Failed to create site"); + } + + Console::info("Site created"); + + // TODO: Add variables, and replace placeholders + + $siteId = $site['body']['$id']; + + // Create deployment + $deployment = $client->call(Client::METHOD_POST, '/sites/' . $siteId . '/deployments/template', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-mode' => 'admin', + 'cookie' => $cookieConsole + ], [ + 'owner' => $template['providerOwner'], + 'repository' => $template['providerRepositoryId'], + 'rootDirectory' => $framework['providerRootDirectory'], + 'version' => $template['providerVersion'], + 'activate' => true, + ]); + + if($deployment['headers']['status-code'] !== 202) { + Console::error(\json_encode($deployment)); + throw new \Exception("Failed to create deployment"); + } + + Console::info("Deployment created"); + + $deploymentId = $deployment['body']['$id']; + + // Await screenshot + $attempts = 50; + $sleep = 5; + + $idLight = ''; + $idDark = ''; + + Console::log("Awaiting deployment (every $sleep seconds, $attempts attempts)"); + + for($i = 0; $i < $attempts; $i++) { + $deployment = $client->call(Client::METHOD_GET, '/sites/' . $siteId . '/deployments/' . $deploymentId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-mode' => 'admin', + 'cookie' => $cookieConsole + ]); + + if($deployment['headers']['status-code'] !== 200) { + Console::error(\json_encode($deployment)); + throw new \Exception("Failed to get deployment"); + } + + if($deployment['body']['status'] === 'failed') { + Console::error(\json_encode($deployment)); + throw new \Exception("Deployment build failed"); + } + + if($deployment['body']['status'] !== 'ready') { + Console::log("Deployment not ready yet, status: " . $deployment['body']['status']); + \sleep($sleep); + continue; + } + + + if(empty($deployment['body']['screenshotLight'])) { + Console::log("Light screenshot not available yet"); + \sleep($sleep); + continue; + } + + if(empty($deployment['body']['screenshotDark'])) { + Console::log("Dark screenshot not available yet"); + \sleep($sleep); + continue; + } + + $idLight = $deployment['body']['screenshotLight']; + $idDark = $deployment['body']['screenshotDark']; + break; + } + + if(empty($idLight) || empty($idDark)) { + Console::error(\json_encode($deployment)); + throw new \Exception("Failed to get deployment screenshot"); + } + + Console::info("Screenshots created"); + + $themes = [ + [ 'fileId' => $idLight, 'suffix' => 'light' ], + [ 'fileId' => $idDark, 'suffix' => 'dark' ] + ]; + + foreach($themes as $theme) { + $file = $client->call(Client::METHOD_GET, '/storage/buckets/screenshots/files/' . $theme['fileId'] . '/download', [ + 'x-appwrite-project' => 'console', + 'x-appwrite-mode' => 'admin', + 'cookie' => $cookieConsole + ]); + + if($file['headers']['status-code'] !== 200) { + Console::error(\json_encode($file)); + throw new \Exception("Failed to download {$theme['suffix']} screenshot"); + } + + $path = "/usr/src/code/public/images/sites/templates/{$template['key']}-{$theme['suffix']}.png"; + + if(!\file_put_contents($path, $file['body'])) { + throw new \Exception("Failed to save {$theme['suffix']} screenshot"); + } + } + + Console::success("Screenshots saved"); + } +}