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/config/site-templates.php b/app/config/site-templates.php index b22b559a15..b34394e6e1 100644 --- a/app/config/site-templates.php +++ b/app/config/site-templates.php @@ -121,7 +121,8 @@ return [ 'key' => 'template-for-onelink', 'name' => 'Onelink template', 'useCases' => ['starter'], - 'demoImage' => $url . '/console/images/sites/templates/template-for-onelink.png', + 'screenshotDark' => $url . '/images/sites/templates/template-for-onelink-dark.png', + 'screenshotLight' => $url . '/images/sites/templates/template-for-onelink-light.png', 'frameworks' => [ getFramework('NUXT', [ 'providerRootDirectory' => './onelink', @@ -140,7 +141,8 @@ return [ 'key' => 'starter-for-svelte', 'name' => 'Svelte starter', 'useCases' => ['starter'], - 'demoImage' => $url . '/console/images/sites/templates/starter-for-svelte.png', + 'screenshotDark' => $url . '/images/sites/templates/starter-for-svelte-dark.png', + 'screenshotLight' => $url . '/images/sites/templates/starter-for-svelte-light.png', 'frameworks' => [ getFramework('SVELTEKIT', [ 'providerRootDirectory' => './', @@ -181,7 +183,8 @@ return [ 'key' => 'starter-for-react', 'name' => 'React starter', 'useCases' => ['starter'], - 'demoImage' => $url . '/console/images/sites/templates/starter-for-react.png', + 'screenshotDark' => $url . '/images/sites/templates/starter-for-react-dark.png', + 'screenshotLight' => $url . '/images/sites/templates/starter-for-react-light.png', 'frameworks' => [ getFramework('REACT', [ 'providerRootDirectory' => './', @@ -222,7 +225,8 @@ return [ 'key' => 'starter-for-vue', 'name' => 'Vue starter', 'useCases' => ['starter'], - 'demoImage' => $url . '/console/images/sites/templates/starter-for-vue.png', + 'screenshotDark' => $url . '/images/sites/templates/starter-for-vue-dark.png', + 'screenshotLight' => $url . '/images/sites/templates/starter-for-vue-light.png', 'frameworks' => [ getFramework('VUE', [ 'providerRootDirectory' => './', @@ -263,7 +267,8 @@ return [ 'key' => 'starter-for-react-native', 'name' => 'React Native starter', 'useCases' => ['starter'], - 'demoImage' => $url . '/console/images/sites/templates/starter-for-react-native.png', + 'screenshotDark' => $url . '/images/sites/templates/starter-for-react-native-dark.png', + 'screenshotLight' => $url . '/images/sites/templates/starter-for-react-native-light.png', 'frameworks' => [ getFramework('REACT', [ 'providerRootDirectory' => './', @@ -305,7 +310,8 @@ return [ 'key' => 'starter-for-nextjs', 'name' => 'Next.js starter', 'useCases' => ['starter'], - 'demoImage' => $url . '/console/images/sites/templates/starter-for-nextjs.png', + 'screenshotDark' => $url . '/images/sites/templates/starter-for-nextjs-dark.png', + 'screenshotLight' => $url . '/images/sites/templates/starter-for-nextjs-light.png', 'frameworks' => [ getFramework('NEXTJS', [ 'providerRootDirectory' => './', @@ -346,7 +352,8 @@ return [ 'key' => 'starter-for-nuxt', 'name' => 'Nuxt starter', 'useCases' => ['starter'], - 'demoImage' => $url . '/console/images/sites/templates/starter-for-nuxt.png', + 'screenshotDark' => $url . '/images/sites/templates/starter-for-nuxt-dark.png', + 'screenshotLight' => $url . '/images/sites/templates/starter-for-nuxt-light.png', 'frameworks' => [ getFramework('NUXT', [ 'providerRootDirectory' => './', @@ -387,7 +394,8 @@ return [ 'key' => 'template-for-event', 'name' => 'Event template', 'useCases' => ['starter'], - 'demoImage' => $url . '/console/images/sites/templates/template-for-event.png', + 'screenshotDark' => $url . '/images/sites/templates/template-for-event-dark.png', + 'screenshotLight' => $url . '/images/sites/templates/template-for-event-light.png', 'frameworks' => [ getFramework('NEXTJS', [ 'providerRootDirectory' => './', @@ -422,7 +430,8 @@ return [ 'key' => 'template-for-portfolio', 'name' => 'Portfolio template', 'useCases' => ['starter'], - 'demoImage' => $url . '/console/images/sites/templates/template-for-portfolio.png', + 'screenshotDark' => $url . '/images/sites/templates/template-for-portfolio-dark.png', + 'screenshotLight' => $url . '/images/sites/templates/template-for-portfolio-light.png', 'frameworks' => [ getFramework('NEXTJS', [ 'providerRootDirectory' => './', @@ -438,7 +447,8 @@ return [ 'key' => 'template-for-store', 'name' => 'Store template', 'useCases' => ['starter'], - 'demoImage' => $url . '/console/images/sites/templates/template-for-store.png', + 'screenshotDark' => $url . '/images/sites/templates/template-for-store-dark.png', + 'screenshotLight' => $url . '/images/sites/templates/template-for-store-light.png', 'frameworks' => [ getFramework('SVELTEKIT', [ 'providerRootDirectory' => './', @@ -479,7 +489,8 @@ return [ 'key' => 'template-for-blog', 'name' => 'Blog template', 'useCases' => ['starter'], - 'demoImage' => $url . '/console/images/sites/templates/template-for-blog.png', + 'screenshotDark' => $url . '/images/sites/templates/template-for-blog-dark.png', + 'screenshotLight' => $url . '/images/sites/templates/template-for-blog-light.png', 'frameworks' => [ getFramework('SVELTEKIT', [ 'providerRootDirectory' => './', @@ -495,7 +506,8 @@ return [ 'key' => 'astro-starter', 'name' => 'Astro starter', 'useCases' => ['starter'], - 'demoImage' => '', + 'screenshotDark' => $url . '/images/sites/templates/astro-starter-dark.png', + 'screenshotLight' => $url . '/images/sites/templates/astro-starter-light.png', 'frameworks' => [ getFramework('ASTRO', [ 'providerRootDirectory' => './astro/starter', @@ -511,7 +523,8 @@ return [ 'key' => 'remix-starter', 'name' => 'Remix starter', 'useCases' => ['starter'], - 'demoImage' => '', + 'screenshotDark' => $url . '/images/sites/templates/remix-starter-dark.png', + 'screenshotLight' => $url . '/images/sites/templates/remix-starter-light.png', 'frameworks' => [ getFramework('REMIX', [ 'providerRootDirectory' => './remix/starter', @@ -527,7 +540,8 @@ return [ 'key' => 'flutter-starter', 'name' => 'Flutter starter', 'useCases' => ['starter'], - 'demoImage' => '', + 'screenshotDark' => $url . '/images/sites/templates/flutter-starter-dark.png', + 'screenshotLight' => $url . '/images/sites/templates/flutter-starter-light.png', 'frameworks' => [ getFramework('FLUTTER', [ 'providerRootDirectory' => './flutter/starter', @@ -543,6 +557,8 @@ return [ 'key' => 'nextjs-starter', 'name' => 'Next.js starter website', 'useCases' => ['starter'], + 'screenshotDark' => $url . '/images/sites/templates/nextjs-starter-dark.png', + 'screenshotLight' => $url . '/images/sites/templates/nextjs-starter-light.png', 'frameworks' => [ getFramework('NEXTJS', [ 'providerRootDirectory' => './nextjs/starter', @@ -558,6 +574,8 @@ return [ 'key' => 'nuxt-starter', 'name' => 'Nuxt starter website', 'useCases' => ['starter'], + 'screenshotDark' => $url . '/images/sites/templates/nuxt-starter-dark.png', + 'screenshotLight' => $url . '/images/sites/templates/nuxt-starter-light.png', 'frameworks' => [ getFramework('NUXT', [ 'providerRootDirectory' => './nuxt/starter', @@ -573,6 +591,8 @@ return [ 'key' => 'sveltekit-starter', 'name' => 'SvelteKit starter website', 'useCases' => ['starter'], + 'screenshotDark' => $url . '/images/sites/templates/sveltekit-starter-dark.png', + 'screenshotLight' => $url . '/images/sites/templates/sveltekit-starter-light.png', 'frameworks' => [ getFramework('SVELTEKIT', [ 'providerRootDirectory' => './sveltekit/starter', diff --git a/app/http.php b/app/http.php index 83f56aeb94..4712d4ebf9 100644 --- a/app/http.php +++ b/app/http.php @@ -29,6 +29,8 @@ use Utopia\Pools\Group; use Utopia\Swoole\Files; use Utopia\System\System; +Files::load(__DIR__.'/../public'); + const DOMAIN_SYNC_TIMER = 30; // 30 seconds $domains = new Table(1_000_000); // 1 million rows diff --git a/app/init.php b/app/init.php index 4e36c91cb1..3291b160b9 100644 --- a/app/init.php +++ b/app/init.php @@ -1977,6 +1977,7 @@ App::setResource('previewHostname', function (Request $request, ?Key $apiKey) { } } + return ''; }, ['request', 'apiKey']); 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/docker-compose.yml b/docker-compose.yml index 8349c14ccb..91f7ce2647 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -953,7 +953,7 @@ services: appwrite-browser: container_name: appwrite-browser - image: appwrite/browser:0.2.0 + image: appwrite/browser:0.2.1 networks: - appwrite @@ -962,7 +962,7 @@ services: hostname: exc1 <<: *x-logging stop_signal: SIGINT - image: openruntimes/executor:0.7.6 + image: openruntimes/executor:0.7.7 restart: unless-stopped networks: - appwrite 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..3d064c5cdd 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..3d064c5cdd 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..9b239ff767 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..9b239ff767 Binary files /dev/null and b/public/images/sites/templates/flutter-starter-light.png differ diff --git a/public/images/sites/templates/nextjs-starter-dark.png b/public/images/sites/templates/nextjs-starter-dark.png new file mode 100644 index 0000000000..022e981d08 Binary files /dev/null and b/public/images/sites/templates/nextjs-starter-dark.png differ diff --git a/public/images/sites/templates/nextjs-starter-light.png b/public/images/sites/templates/nextjs-starter-light.png new file mode 100644 index 0000000000..0e19bf5d37 Binary files /dev/null and b/public/images/sites/templates/nextjs-starter-light.png differ diff --git a/public/images/sites/templates/nuxt-starter-dark.png b/public/images/sites/templates/nuxt-starter-dark.png new file mode 100644 index 0000000000..bea04f1cf9 Binary files /dev/null and b/public/images/sites/templates/nuxt-starter-dark.png differ diff --git a/public/images/sites/templates/nuxt-starter-light.png b/public/images/sites/templates/nuxt-starter-light.png new file mode 100644 index 0000000000..006e366a9f Binary files /dev/null and b/public/images/sites/templates/nuxt-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..47c0cc0394 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..73c2d12625 Binary files /dev/null and b/public/images/sites/templates/remix-starter-light.png differ diff --git a/public/images/sites/templates/starter-for-nextjs-dark.png b/public/images/sites/templates/starter-for-nextjs-dark.png new file mode 100644 index 0000000000..7acc80d4d4 Binary files /dev/null and b/public/images/sites/templates/starter-for-nextjs-dark.png differ diff --git a/public/images/sites/templates/starter-for-nextjs-light.png b/public/images/sites/templates/starter-for-nextjs-light.png new file mode 100644 index 0000000000..7acc80d4d4 Binary files /dev/null and b/public/images/sites/templates/starter-for-nextjs-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..dcfc0a5521 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..dcfc0a5521 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-svelte-dark.png b/public/images/sites/templates/starter-for-svelte-dark.png new file mode 100644 index 0000000000..f25943ae48 Binary files /dev/null and b/public/images/sites/templates/starter-for-svelte-dark.png differ diff --git a/public/images/sites/templates/starter-for-svelte-light.png b/public/images/sites/templates/starter-for-svelte-light.png new file mode 100644 index 0000000000..f25943ae48 Binary files /dev/null and b/public/images/sites/templates/starter-for-svelte-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..9d9af29f87 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..9d9af29f87 Binary files /dev/null and b/public/images/sites/templates/starter-for-vue-light.png differ diff --git a/public/images/sites/templates/sveltekit-starter-dark.png b/public/images/sites/templates/sveltekit-starter-dark.png new file mode 100644 index 0000000000..90ac7f96aa Binary files /dev/null and b/public/images/sites/templates/sveltekit-starter-dark.png differ diff --git a/public/images/sites/templates/sveltekit-starter-light.png b/public/images/sites/templates/sveltekit-starter-light.png new file mode 100644 index 0000000000..90ac7f96aa Binary files /dev/null and b/public/images/sites/templates/sveltekit-starter-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..b076b918a8 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..b076b918a8 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-event-dark.png b/public/images/sites/templates/template-for-event-dark.png new file mode 100644 index 0000000000..c3376d3ff6 Binary files /dev/null and b/public/images/sites/templates/template-for-event-dark.png differ diff --git a/public/images/sites/templates/template-for-event-light.png b/public/images/sites/templates/template-for-event-light.png new file mode 100644 index 0000000000..c97901326a Binary files /dev/null and b/public/images/sites/templates/template-for-event-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..4b4382bf80 Binary files /dev/null and b/public/images/sites/templates/template-for-portfolio-light.png differ diff --git a/public/images/sites/templates/template-for-store-dark.png b/public/images/sites/templates/template-for-store-dark.png new file mode 100644 index 0000000000..645671ab55 Binary files /dev/null and b/public/images/sites/templates/template-for-store-dark.png differ diff --git a/public/images/sites/templates/template-for-store-light.png b/public/images/sites/templates/template-for-store-light.png new file mode 100644 index 0000000000..645671ab55 Binary files /dev/null and b/public/images/sites/templates/template-for-store-light.png differ diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Builds/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Builds/Create.php deleted file mode 100644 index 1d85bcb11d..0000000000 --- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Builds/Create.php +++ /dev/null @@ -1,112 +0,0 @@ -setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) - ->setHttpPath('/v1/functions/:functionId/deployments/:deploymentId/build') - ->httpAlias('/v1/functions/:functionId/deployments/:deploymentId/builds/:buildId') - ->desc('Rebuild deployment') - ->groups(['api', 'functions']) - ->label('scope', 'functions.write') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('event', 'functions.[functionId].deployments.[deploymentId].update') - ->label('audits.event', 'deployment.update') - ->label('audits.resource', 'function/{request.functionId}') - ->label('sdk', new Method( - namespace: 'functions', - name: 'createBuild', - description: <<param('functionId', '', new UID(), 'Function ID.') - ->param('deploymentId', '', new UID(), 'Deployment ID.') - ->param('buildId', '', new UID(), 'Build unique ID.', true) // added as optional param for backward compatibility - ->inject('response') - ->inject('dbForProject') - ->inject('queueForEvents') - ->inject('queueForBuilds') - ->inject('deviceForFunctions') - ->callback([$this, 'action']); - } - - public function action(string $functionId, string $deploymentId, string $buildId, Response $response, Database $dbForProject, Event $queueForEvents, Build $queueForBuilds, Device $deviceForFunctions) - { - $function = $dbForProject->getDocument('functions', $functionId); - - if ($function->isEmpty()) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); - } - $deployment = $dbForProject->getDocument('deployments', $deploymentId); - - if ($deployment->isEmpty()) { - throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); - } - - $path = $deployment->getAttribute('path'); - if (empty($path) || !$deviceForFunctions->exists($path)) { - throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); - } - - $deploymentId = ID::unique(); - - $destination = $deviceForFunctions->getPath($deploymentId . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION)); - $deviceForFunctions->transfer($path, $destination, $deviceForFunctions); - - $deployment->removeAttribute('$internalId'); - $deployment = $dbForProject->createDocument('deployments', $deployment->setAttributes([ - '$internalId' => '', - '$id' => $deploymentId, - 'buildId' => '', - 'buildInternalId' => '', - 'path' => $destination, - 'entrypoint' => $function->getAttribute('entrypoint'), - 'commands' => $function->getAttribute('commands', ''), - 'search' => implode(' ', [$deploymentId, $function->getAttribute('entrypoint')]), - ])); - - $queueForBuilds - ->setType(BUILD_TYPE_DEPLOYMENT) - ->setResource($function) - ->setDeployment($deployment); - - $queueForEvents - ->setParam('functionId', $function->getId()) - ->setParam('deploymentId', $deployment->getId()); - - $response->noContent(); - } -} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Builds/Download/Get.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Builds/Download/Get.php deleted file mode 100644 index c58ca946c9..0000000000 --- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Builds/Download/Get.php +++ /dev/null @@ -1,134 +0,0 @@ -setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) - ->setHttpPath('/v1/functions/:functionId/deployments/:deploymentId/build/download') - ->desc('Download build') - ->groups(['api', 'functions']) - ->label('scope', 'functions.read') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('sdk', new Method( - namespace: 'functions', - name: 'getDeploymentBuildDownload', - description: <<param('functionId', '', new UID(), 'Function ID.') - ->param('deploymentId', '', new UID(), 'Deployment ID.') - ->inject('response') - ->inject('request') - ->inject('dbForProject') - ->inject('deviceForBuilds') - ->callback([$this, 'action']); - } - - public function action(string $functionId, string $deploymentId, Response $response, Request $request, Database $dbForProject, Device $deviceForBuilds) - { - $function = $dbForProject->getDocument('functions', $functionId); - if ($function->isEmpty()) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); - } - - $deployment = $dbForProject->getDocument('deployments', $deploymentId); - if ($deployment->isEmpty()) { - throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); - } - - if ($deployment->getAttribute('resourceId') !== $function->getId()) { - throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); - } - - $build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId')); - if ($build->isEmpty()) { - throw new Exception(Exception::BUILD_NOT_FOUND); - } - - $path = $build->getAttribute('path', ''); - if (!$deviceForBuilds->exists($path)) { - throw new Exception(Exception::BUILD_NOT_FOUND); - } - - $response - ->setContentType('application/gzip') - ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT') // 45 days cache - ->addHeader('X-Peak', \memory_get_peak_usage()) - ->addHeader('Content-Disposition', 'attachment; filename="' . $deploymentId . '.tar.gz"'); - - $size = $deviceForBuilds->getFileSize($path); - $rangeHeader = $request->getHeader('range'); - - if (!empty($rangeHeader)) { - $start = $request->getRangeStart(); - $end = $request->getRangeEnd(); - $unit = $request->getRangeUnit(); - - if ($end === null) { - $end = min(($start + MAX_OUTPUT_CHUNK_SIZE - 1), ($size - 1)); - } - - if ($unit !== 'bytes' || $start >= $end || $end >= $size) { - throw new Exception(Exception::STORAGE_INVALID_RANGE); - } - - $response - ->addHeader('Accept-Ranges', 'bytes') - ->addHeader('Content-Range', 'bytes ' . $start . '-' . $end . '/' . $size) - ->addHeader('Content-Length', $end - $start + 1) - ->setStatusCode(Response::STATUS_CODE_PARTIALCONTENT); - - $response->send($deviceForBuilds->read($path, $start, ($end - $start + 1))); - } - - if ($size > APP_STORAGE_READ_BUFFER) { - for ($i = 0; $i < ceil($size / MAX_OUTPUT_CHUNK_SIZE); $i++) { - $response->chunk( - $deviceForBuilds->read( - $path, - ($i * MAX_OUTPUT_CHUNK_SIZE), - min(MAX_OUTPUT_CHUNK_SIZE, $size - ($i * MAX_OUTPUT_CHUNK_SIZE)) - ), - (($i + 1) * MAX_OUTPUT_CHUNK_SIZE) >= $size - ); - } - } else { - $response->send($deviceForBuilds->read($path)); - } - } -} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Builds/Update.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Builds/Update.php deleted file mode 100644 index 85edfd42ca..0000000000 --- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Builds/Update.php +++ /dev/null @@ -1,136 +0,0 @@ -setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) - ->setHttpPath('/v1/functions/:functionId/deployments/:deploymentId/build') - ->desc('Cancel deployment') - ->groups(['api', 'functions']) - ->label('scope', 'functions.write') - ->label('resourceType', RESOURCE_TYPE_FUNCTIONS) - ->label('audits.event', 'deployment.update') - ->label('audits.resource', 'function/{request.functionId}') - ->label('sdk', new Method( - namespace: 'functions', - name: 'updateDeploymentBuild', - description: <<param('functionId', '', new UID(), 'Function ID.') - ->param('deploymentId', '', new UID(), 'Deployment ID.') - ->inject('response') - ->inject('dbForProject') - ->inject('project') - ->inject('queueForEvents') - ->callback([$this, 'action']); - } - - public function action(string $functionId, string $deploymentId, Response $response, Database $dbForProject, Document $project, Event $queueForEvents) - { - $function = $dbForProject->getDocument('functions', $functionId); - - if ($function->isEmpty()) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); - } - - $deployment = $dbForProject->getDocument('deployments', $deploymentId); - - if ($deployment->isEmpty()) { - throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); - } - - $build = Authorization::skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''))); - - if ($build->isEmpty()) { - $buildId = ID::unique(); - $build = $dbForProject->createDocument('builds', new Document([ - '$id' => $buildId, - '$permissions' => [], - 'startTime' => DateTime::now(), - 'deploymentInternalId' => $deployment->getInternalId(), - 'deploymentId' => $deployment->getId(), - 'status' => 'canceled', - 'path' => '', - 'runtime' => $function->getAttribute('runtime'), - 'source' => $deployment->getAttribute('path', ''), - 'sourceType' => '', - 'logs' => '', - 'duration' => 0, - 'size' => 0 - ])); - - $deployment->setAttribute('buildId', $build->getId()); - $deployment->setAttribute('buildInternalId', $build->getInternalId()); - $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment); - } else { - if (\in_array($build->getAttribute('status'), ['ready', 'failed'])) { - throw new Exception(Exception::BUILD_ALREADY_COMPLETED); - } - - $startTime = new \DateTime($build->getAttribute('startTime')); - $endTime = new \DateTime('now'); - $duration = $endTime->getTimestamp() - $startTime->getTimestamp(); - - $build = $dbForProject->updateDocument('builds', $build->getId(), $build->setAttributes([ - 'endTime' => DateTime::now(), - 'duration' => $duration, - 'status' => 'canceled' - ])); - } - - $dbForProject->purgeCachedDocument('deployments', $deployment->getId()); - - try { - $executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST')); - $executor->deleteRuntime($project->getId(), $deploymentId . "-build"); - } catch (\Throwable $th) { - // Don't throw if the deployment doesn't exist - if ($th->getCode() !== 404) { - throw $th; - } - } - - $queueForEvents - ->setParam('functionId', $function->getId()) - ->setParam('deploymentId', $deployment->getId()); - - $response->dynamic($build, Response::MODEL_BUILD); - } -} diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Download/Get.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Download/Get.php index 1a1ff9b938..a63ac18354 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Download/Get.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Download/Get.php @@ -15,6 +15,7 @@ use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; use Utopia\Storage\Device; use Utopia\Swoole\Request; +use Utopia\Validator\WhiteList; class Get extends Action { @@ -30,6 +31,7 @@ class Get extends Action $this ->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) ->setHttpPath('/v1/functions/:functionId/deployments/:deploymentId/download') + ->httpAlias('/v1/functions/:functionId/deployments/:deploymentId/build/download', [ 'type' => 'output' ]) ->groups(['api', 'functions']) ->desc('Download deployment') ->label('scope', 'functions.read') @@ -52,14 +54,16 @@ class Get extends Action )) ->param('functionId', '', new UID(), 'Function ID.') ->param('deploymentId', '', new UID(), 'Deployment ID.') + ->param('type', 'source', new WhiteList(['source', 'output']), 'Deployment file to download. Can be: "source", "output".', true) ->inject('response') ->inject('request') ->inject('dbForProject') ->inject('deviceForFunctions') + ->inject('deviceForBuilds') ->callback([$this, 'action']); } - public function action(string $functionId, string $deploymentId, Response $response, Request $request, Database $dbForProject, Device $deviceForFunctions) + public function action(string $functionId, string $deploymentId, string $type, Response $response, Request $request, Database $dbForProject, Device $deviceForFunctions, Device $deviceForBuilds) { $function = $dbForProject->getDocument('functions', $functionId); if ($function->isEmpty()) { @@ -75,8 +79,23 @@ class Get extends Action throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); } - $path = $deployment->getAttribute('path', ''); - if (!$deviceForFunctions->exists($path)) { + switch ($type) { + case 'output': + $build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId')); + if ($build->isEmpty()) { + throw new Exception(Exception::BUILD_NOT_FOUND); + } + + $path = $build->getAttribute('path', ''); + $device = $deviceForBuilds; + break; + case 'source': + $path = $deployment->getAttribute('path', ''); + $device = $deviceForFunctions; + break; + } + + if (!$device->exists($path)) { throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); } @@ -86,7 +105,7 @@ class Get extends Action ->addHeader('X-Peak', \memory_get_peak_usage()) ->addHeader('Content-Disposition', 'attachment; filename="' . $deploymentId . '.tar.gz"'); - $size = $deviceForFunctions->getFileSize($path); + $size = $device->getFileSize($path); $rangeHeader = $request->getHeader('range'); if (!empty($rangeHeader)) { @@ -108,13 +127,13 @@ class Get extends Action ->addHeader('Content-Length', $end - $start + 1) ->setStatusCode(Response::STATUS_CODE_PARTIALCONTENT); - $response->send($deviceForFunctions->read($path, $start, ($end - $start + 1))); + $response->send($device->read($path, $start, ($end - $start + 1))); } if ($size > APP_STORAGE_READ_BUFFER) { for ($i = 0; $i < ceil($size / MAX_OUTPUT_CHUNK_SIZE); $i++) { $response->chunk( - $deviceForFunctions->read( + $device->read( $path, ($i * MAX_OUTPUT_CHUNK_SIZE), min(MAX_OUTPUT_CHUNK_SIZE, $size - ($i * MAX_OUTPUT_CHUNK_SIZE)) @@ -123,7 +142,7 @@ class Get extends Action ); } } else { - $response->send($deviceForFunctions->read($path)); + $response->send($device->read($path)); } } } diff --git a/src/Appwrite/Platform/Modules/Functions/Services/Http.php b/src/Appwrite/Platform/Modules/Functions/Services/Http.php index a17c80865e..5ae77c5d6b 100644 --- a/src/Appwrite/Platform/Modules/Functions/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Functions/Services/Http.php @@ -3,7 +3,6 @@ namespace Appwrite\Platform\Modules\Functions\Services; use Appwrite\Platform\Modules\Functions\Http\Deployments\Builds\Create as CreateBuild; -use Appwrite\Platform\Modules\Functions\Http\Deployments\Builds\Download\Get as DownloadBuild; use Appwrite\Platform\Modules\Functions\Http\Deployments\Builds\Update as UpdateBuild; use Appwrite\Platform\Modules\Functions\Http\Deployments\Create as CreateDeployment; use Appwrite\Platform\Modules\Functions\Http\Deployments\Delete as DeleteDeployment; @@ -65,7 +64,6 @@ class Http extends Service $this->addAction(DownloadDeployment::getName(), new DownloadDeployment()); $this->addAction(CreateBuild::getName(), new CreateBuild()); $this->addAction(UpdateBuild::getName(), new UpdateBuild()); - $this->addAction(DownloadBuild::getName(), new DownloadBuild()); // Executions $this->addAction(CreateExecution::getName(), new CreateExecution()); diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index 9067e042e4..aa24182e9a 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -740,6 +740,7 @@ class Builds extends Action } $client = new FetchClient(); + $client->setTimeout(\intval($resource->getAttribute('timeout', '15'))); $client->addHeader('content-type', FetchClient::CONTENT_TYPE_APPLICATION_JSON); $bucket = Authorization::skip(fn () => $dbForPlatform->getDocument('buckets', 'screenshots')); diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Builds/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Builds/Create.php deleted file mode 100644 index 2cd2928c96..0000000000 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Builds/Create.php +++ /dev/null @@ -1,136 +0,0 @@ -setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) - ->setHttpPath('/v1/sites/:siteId/deployments/:deploymentId/build') - ->desc('Rebuild deployment') - ->groups(['api', 'sites']) - ->label('scope', 'sites.write') - ->label('resourceType', RESOURCE_TYPE_SITES) - ->label('event', 'sites.[siteId].deployments.[deploymentId].update') - ->label('audits.event', 'deployment.update') - ->label('audits.resource', 'site/{request.siteId}') - ->label('sdk', new Method( - namespace: 'sites', - name: 'createDeploymentBuild', - description: <<param('siteId', '', new UID(), 'Site ID.') - ->param('deploymentId', '', new UID(), 'Deployment ID.') - ->inject('response') - ->inject('project') - ->inject('dbForProject') - ->inject('dbForPlatform') - ->inject('queueForEvents') - ->inject('queueForBuilds') - ->inject('deviceForSites') - ->callback([$this, 'action']); - } - - public function action(string $siteId, string $deploymentId, Response $response, Document $project, Database $dbForProject, Database $dbForPlatform, Event $queueForEvents, Build $queueForBuilds, Device $deviceForSites) - { - $site = $dbForProject->getDocument('sites', $siteId); - - if ($site->isEmpty()) { - throw new Exception(Exception::SITE_NOT_FOUND); - } - $deployment = $dbForProject->getDocument('deployments', $deploymentId); - - if ($deployment->isEmpty()) { - throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); - } - - $path = $deployment->getAttribute('path'); - if (empty($path) || !$deviceForSites->exists($path)) { - throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); - } - - $deploymentId = ID::unique(); - - $destination = $deviceForSites->getPath($deploymentId . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION)); - $deviceForSites->transfer($path, $destination, $deviceForSites); - - $deployment->removeAttribute('$internalId'); - $deployment = $dbForProject->createDocument('deployments', $deployment->setAttributes([ - '$internalId' => '', - '$id' => $deploymentId, - 'buildId' => '', - 'buildInternalId' => '', - 'path' => $destination, - 'buildCommand' => $site->getAttribute('buildCommand', ''), - 'installCommand' => $site->getAttribute('installCommand', ''), - 'outputDirectory' => $site->getAttribute('outputDirectory', ''), - 'search' => implode(' ', [$deploymentId]), - 'screenshotLight' => '', - 'screenshotDark' => '' - ])); - - // Preview deployments for sites - $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); - $domain = ID::unique() . "." . $sitesDomain; - $ruleId = md5($domain); - Authorization::skip( - fn () => $dbForPlatform->createDocument('rules', new Document([ - '$id' => $ruleId, - 'projectId' => $project->getId(), - 'projectInternalId' => $project->getInternalId(), - 'domain' => $domain, - 'type' => 'deployment', - 'value' => $deployment->getId(), - 'status' => 'verified', - 'certificateId' => '', - 'search' => implode(' ', [$ruleId, $domain]), - ])) - ); - - $queueForBuilds - ->setType(BUILD_TYPE_DEPLOYMENT) - ->setResource($site) - ->setDeployment($deployment); - - $queueForEvents - ->setParam('siteId', $site->getId()) - ->setParam('deploymentId', $deployment->getId()); - - $response->noContent(); - } -} diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Builds/Download/Get.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Builds/Download/Get.php deleted file mode 100644 index 1c955d302a..0000000000 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Builds/Download/Get.php +++ /dev/null @@ -1,134 +0,0 @@ -setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) - ->setHttpPath('/v1/sites/:siteId/deployments/:deploymentId/build/download') - ->desc('Download build') - ->groups(['api', 'sites']) - ->label('scope', 'sites.read') - ->label('resourceType', RESOURCE_TYPE_SITES) - ->label('sdk', new Method( - namespace: 'sites', - name: 'getDeploymentBuildDownload', - description: <<param('siteId', '', new UID(), 'Site ID.') - ->param('deploymentId', '', new UID(), 'Deployment ID.') - ->inject('response') - ->inject('request') - ->inject('dbForProject') - ->inject('deviceForBuilds') - ->callback([$this, 'action']); - } - - public function action(string $siteId, string $deploymentId, Response $response, Request $request, Database $dbForProject, Device $deviceForBuilds) - { - $site = $dbForProject->getDocument('sites', $siteId); - if ($site->isEmpty()) { - throw new Exception(Exception::SITE_NOT_FOUND); - } - - $deployment = $dbForProject->getDocument('deployments', $deploymentId); - if ($deployment->isEmpty()) { - throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); - } - - if ($deployment->getAttribute('resourceId') !== $site->getId()) { - throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); - } - - $build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId')); - if ($build->isEmpty()) { - throw new Exception(Exception::BUILD_NOT_FOUND); - } - - $path = $build->getAttribute('path', ''); - if (!$deviceForBuilds->exists($path)) { - throw new Exception(Exception::BUILD_NOT_FOUND); - } - - $response - ->setContentType('application/gzip') - ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT') // 45 days cache - ->addHeader('X-Peak', \memory_get_peak_usage()) - ->addHeader('Content-Disposition', 'attachment; filename="' . $deploymentId . '.tar.gz"'); - - $size = $deviceForBuilds->getFileSize($path); - $rangeHeader = $request->getHeader('range'); - - if (!empty($rangeHeader)) { - $start = $request->getRangeStart(); - $end = $request->getRangeEnd(); - $unit = $request->getRangeUnit(); - - if ($end === null) { - $end = min(($start + MAX_OUTPUT_CHUNK_SIZE - 1), ($size - 1)); - } - - if ($unit !== 'bytes' || $start >= $end || $end >= $size) { - throw new Exception(Exception::STORAGE_INVALID_RANGE); - } - - $response - ->addHeader('Accept-Ranges', 'bytes') - ->addHeader('Content-Range', 'bytes ' . $start . '-' . $end . '/' . $size) - ->addHeader('Content-Length', $end - $start + 1) - ->setStatusCode(Response::STATUS_CODE_PARTIALCONTENT); - - $response->send($deviceForBuilds->read($path, $start, ($end - $start + 1))); - } - - if ($size > APP_STORAGE_READ_BUFFER) { - for ($i = 0; $i < ceil($size / MAX_OUTPUT_CHUNK_SIZE); $i++) { - $response->chunk( - $deviceForBuilds->read( - $path, - ($i * MAX_OUTPUT_CHUNK_SIZE), - min(MAX_OUTPUT_CHUNK_SIZE, $size - ($i * MAX_OUTPUT_CHUNK_SIZE)) - ), - (($i + 1) * MAX_OUTPUT_CHUNK_SIZE) >= $size - ); - } - } else { - $response->send($deviceForBuilds->read($path)); - } - } -} diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Builds/Update.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Builds/Update.php deleted file mode 100644 index 6bebfc66e3..0000000000 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Builds/Update.php +++ /dev/null @@ -1,136 +0,0 @@ -setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) - ->setHttpPath('/v1/sites/:siteId/deployments/:deploymentId/build') - ->desc('Cancel deployment') - ->groups(['api', 'sites']) - ->label('scope', 'sites.write') - ->label('resourceType', RESOURCE_TYPE_SITES) - ->label('audits.event', 'deployment.update') - ->label('audits.resource', 'site/{request.siteId}') - ->label('sdk', new Method( - namespace: 'sites', - name: 'updateDeploymentBuild', - description: <<param('siteId', '', new UID(), 'Site ID.') - ->param('deploymentId', '', new UID(), 'Deployment ID.') - ->inject('response') - ->inject('dbForProject') - ->inject('project') - ->inject('queueForEvents') - ->callback([$this, 'action']); - } - - public function action(string $siteId, string $deploymentId, Response $response, Database $dbForProject, Document $project, Event $queueForEvents) - { - $site = $dbForProject->getDocument('sites', $siteId); - - if ($site->isEmpty()) { - throw new Exception(Exception::SITE_NOT_FOUND); - } - - $deployment = $dbForProject->getDocument('deployments', $deploymentId); - - if ($deployment->isEmpty()) { - throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); - } - - $build = Authorization::skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''))); - - if ($build->isEmpty()) { - $buildId = ID::unique(); - $build = $dbForProject->createDocument('builds', new Document([ - '$id' => $buildId, - '$permissions' => [], - 'startTime' => DateTime::now(), - 'deploymentInternalId' => $deployment->getInternalId(), - 'deploymentId' => $deployment->getId(), - 'status' => 'canceled', - 'path' => '', - 'runtime' => $site->getAttribute('framework'), - 'source' => $deployment->getAttribute('path', ''), - 'sourceType' => '', - 'logs' => '', - 'duration' => 0, - 'size' => 0 - ])); - - $deployment->setAttribute('buildId', $build->getId()); - $deployment->setAttribute('buildInternalId', $build->getInternalId()); - $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment); - } else { - if (\in_array($build->getAttribute('status'), ['ready', 'failed'])) { - throw new Exception(Exception::BUILD_ALREADY_COMPLETED); - } - - $startTime = new \DateTime($build->getAttribute('startTime')); - $endTime = new \DateTime('now'); - $duration = $endTime->getTimestamp() - $startTime->getTimestamp(); - - $build = $dbForProject->updateDocument('builds', $build->getId(), $build->setAttributes([ - 'endTime' => DateTime::now(), - 'duration' => $duration, - 'status' => 'canceled' - ])); - } - - $dbForProject->purgeCachedDocument('deployments', $deployment->getId()); - - try { - $executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST')); - $executor->deleteRuntime($project->getId(), $deploymentId . "-build"); - } catch (\Throwable $th) { - // Don't throw if the deployment doesn't exist - if ($th->getCode() !== 404) { - throw $th; - } - } - - $queueForEvents - ->setParam('siteId', $site->getId()) - ->setParam('deploymentId', $deployment->getId()); - - $response->dynamic($build, Response::MODEL_BUILD); - } -} diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Download/Get.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Download/Get.php index 56c6a36507..b1d67fe26b 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Download/Get.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Download/Get.php @@ -15,6 +15,7 @@ use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; use Utopia\Storage\Device; use Utopia\Swoole\Request; +use Utopia\Validator\WhiteList; class Get extends Action { @@ -30,6 +31,7 @@ class Get extends Action $this ->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) ->setHttpPath('/v1/sites/:siteId/deployments/:deploymentId/download') + ->httpAlias('/v1/sites/:functionId/deployments/:deploymentId/build/download', [ 'type' => 'output' ]) ->desc('Download deployment') ->groups(['api', 'sites']) ->label('scope', 'sites.read') @@ -52,14 +54,16 @@ class Get extends Action )) ->param('siteId', '', new UID(), 'Site ID.') ->param('deploymentId', '', new UID(), 'Deployment ID.') + ->param('type', 'source', new WhiteList(['source', 'output']), 'Deployment file to download. Can be: "source", "output".', true) ->inject('response') ->inject('request') ->inject('dbForProject') ->inject('deviceForSites') + ->inject('deviceForBuilds') ->callback([$this, 'action']); } - public function action(string $siteId, string $deploymentId, Response $response, Request $request, Database $dbForProject, Device $deviceForSites) + public function action(string $siteId, string $deploymentId, string $type, Response $response, Request $request, Database $dbForProject, Device $deviceForSites, Device $deviceForBuilds) { $site = $dbForProject->getDocument('sites', $siteId); if ($site->isEmpty()) { @@ -75,18 +79,33 @@ class Get extends Action throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); } - $path = $deployment->getAttribute('path', ''); - if (!$deviceForSites->exists($path)) { - throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); + switch ($type) { + case 'output': + $build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId')); + if ($build->isEmpty()) { + throw new Exception(Exception::BUILD_NOT_FOUND); + } + + $path = $build->getAttribute('path', ''); + $device = $deviceForBuilds; + break; + case 'source': + $path = $deployment->getAttribute('path', ''); + $device = $deviceForSites; + break; + } + + if (!$device->exists($path)) { + throw new Exception(Exception::BUILD_NOT_FOUND); } $response ->setContentType('application/gzip') ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT') // 45 days cache ->addHeader('X-Peak', \memory_get_peak_usage()) - ->addHeader('Content-Disposition', 'attachment; filename="' . $deploymentId . '.tar.gz"'); + ->addHeader('Content-Disposition', 'attachment; filename="' . $deploymentId . '-' . $type . '.tar.gz"'); - $size = $deviceForSites->getFileSize($path); + $size = $device->getFileSize($path); $rangeHeader = $request->getHeader('range'); if (!empty($rangeHeader)) { @@ -108,13 +127,13 @@ class Get extends Action ->addHeader('Content-Length', $end - $start + 1) ->setStatusCode(Response::STATUS_CODE_PARTIALCONTENT); - $response->send($deviceForSites->read($path, $start, ($end - $start + 1))); + $response->send($device->read($path, $start, ($end - $start + 1))); } if ($size > APP_STORAGE_READ_BUFFER) { for ($i = 0; $i < ceil($size / MAX_OUTPUT_CHUNK_SIZE); $i++) { $response->chunk( - $deviceForSites->read( + $device->read( $path, ($i * MAX_OUTPUT_CHUNK_SIZE), min(MAX_OUTPUT_CHUNK_SIZE, $size - ($i * MAX_OUTPUT_CHUNK_SIZE)) @@ -123,7 +142,7 @@ class Get extends Action ); } } else { - $response->send($deviceForSites->read($path)); + $response->send($device->read($path)); } } } diff --git a/src/Appwrite/Platform/Modules/Sites/Services/Http.php b/src/Appwrite/Platform/Modules/Sites/Services/Http.php index 9df4329b01..7b322900d1 100644 --- a/src/Appwrite/Platform/Modules/Sites/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Sites/Services/Http.php @@ -3,7 +3,6 @@ namespace Appwrite\Platform\Modules\Sites\Services; use Appwrite\Platform\Modules\Sites\Http\Deployments\Builds\Create as CreateBuild; -use Appwrite\Platform\Modules\Sites\Http\Deployments\Builds\Download\Get as DownloadBuild; use Appwrite\Platform\Modules\Sites\Http\Deployments\Builds\Update as UpdateBuild; use Appwrite\Platform\Modules\Sites\Http\Deployments\Create as CreateDeployment; use Appwrite\Platform\Modules\Sites\Http\Deployments\Delete as DeleteDeployment; @@ -58,7 +57,6 @@ class Http extends Service $this->addAction(UpdateDeployment::getName(), new UpdateDeployment()); $this->addAction(DeleteDeployment::getName(), new DeleteDeployment()); $this->addAction(DownloadDeployment::getName(), new DownloadDeployment()); - $this->addAction(DownloadBuild::getName(), new DownloadBuild()); $this->addAction(CreateBuild::getName(), new CreateBuild()); $this->addAction(UpdateBuild::getName(), new UpdateBuild()); 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..43cf434408 --- /dev/null +++ b/src/Appwrite/Platform/Tasks/Screenshot.php @@ -0,0 +1,300 @@ +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"); + + $projectName = 'Demo Project'; + $projectId = ID::unique(); + + // Create project + $project = $client->call(Client::METHOD_POST, '/projects', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => 'console', + 'cookie' => $cookieConsole + ], [ + 'projectId' => $projectId, + 'region' => 'default', + 'name' => $projectName, + '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'], + 'timeout' => 60 + ]); + + if ($site['headers']['status-code'] !== 201) { + Console::error(\json_encode($site)); + throw new \Exception("Failed to create site"); + } + + Console::info("Site created"); + + $siteId = $site['body']['$id']; + + // Create variables + if (!empty($template['variables'] ?? [])) { + foreach ($template['variables'] as $variable) { + if (empty($variable['value'] ?? '')) { + if (($variable['required'] ?? false) === true) { + throw new \Exception("Missing required variable: {$variable['name']}"); + } + + continue; + } + + $value = $variable['value']; + $value = \str_replace('{projectName}', $projectName, $value); + $value = \str_replace('{projectId}', $projectId, $value); + $value = \str_replace('{apiEndpoint}', 'http://localhost/v1', $value); + + $response = $client->call(Client::METHOD_POST, '/sites/' . $siteId . '/variables', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + 'x-appwrite-mode' => 'admin', + 'cookie' => $cookieConsole + ], [ + 'key' => $variable['name'], + 'value' => $value + ]); + + if ($response['headers']['status-code'] !== 201) { + Console::error(\json_encode($response)); + throw new \Exception("Failed to create variable"); + } + } + + Console::info("Variables created"); + } + + // 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"); + } +} diff --git a/src/Appwrite/Specification/Format.php b/src/Appwrite/Specification/Format.php index f4bec326e7..b4d2270eef 100644 --- a/src/Appwrite/Specification/Format.php +++ b/src/Appwrite/Specification/Format.php @@ -210,8 +210,24 @@ abstract class Format break; } break; + case 'functions': + switch ($method) { + case 'getDeploymentDownload': + switch ($param) { + case 'type': + return 'DeploymentDownloadType'; + } + break; + } + break; case 'sites': switch ($method) { + case 'getDeploymentDownload': + switch ($param) { + case 'type': + return 'DeploymentDownloadType'; + } + break; case 'getUsage': case 'listUsage': switch ($param) { diff --git a/src/Appwrite/Utopia/Response/Model/TemplateSite.php b/src/Appwrite/Utopia/Response/Model/TemplateSite.php index b9fe4704c7..fe123bde53 100644 --- a/src/Appwrite/Utopia/Response/Model/TemplateSite.php +++ b/src/Appwrite/Utopia/Response/Model/TemplateSite.php @@ -28,11 +28,17 @@ class TemplateSite extends Model 'default' => '', 'example' => 'https://nextjs-starter.appwrite.network/', ]) - ->addRule('demoImage', [ + ->addRule('screenshotDark', [ 'type' => self::TYPE_STRING, - 'description' => 'File URL with preview screenshot.', + 'description' => 'File URL with preview screenshot in dark theme preference.', 'default' => '', - 'example' => 'https://cloud.appwrite.io/console/images/sites/templates/nextjs-starter.png', + 'example' => 'https://cloud.appwrite.io/images/sites/templates/template-for-blog-dark.png', + ]) + ->addRule('screenshotLight', [ + 'type' => self::TYPE_STRING, + 'description' => 'File URL with preview screenshot in light theme preference.', + 'default' => '', + 'example' => 'https://cloud.appwrite.io/images/sites/templates/template-for-blog-light.png', ]) ->addRule('useCases', [ 'type' => self::TYPE_STRING, diff --git a/tests/e2e/Services/Functions/FunctionsBase.php b/tests/e2e/Services/Functions/FunctionsBase.php index c340032e5b..f97245b0af 100644 --- a/tests/e2e/Services/Functions/FunctionsBase.php +++ b/tests/e2e/Services/Functions/FunctionsBase.php @@ -313,22 +313,14 @@ trait FunctionsBase return $domain; } - protected function getDeploymentDownload(string $functionId, string $deploymentId): mixed + protected function getDeploymentDownload(string $functionId, string $deploymentId, string $type): mixed { $response = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/deployments/' . $deploymentId . '/download', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), []); - - return $response; - } - - protected function getBuildDownload(string $functionId, string $deploymentId): mixed - { - $response = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/deployments/' . $deploymentId . '/build/download', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), []); + ], $this->getHeaders()), [ + 'type' => $type + ]); return $response; } diff --git a/tests/e2e/Services/Functions/FunctionsConsoleClientTest.php b/tests/e2e/Services/Functions/FunctionsConsoleClientTest.php index 5778e9f20a..300090b156 100644 --- a/tests/e2e/Services/Functions/FunctionsConsoleClientTest.php +++ b/tests/e2e/Services/Functions/FunctionsConsoleClientTest.php @@ -517,7 +517,7 @@ class FunctionsConsoleClientTest extends Scope $this->assertNotEmpty($deploymentId); - $response = $this->getDeploymentDownload($functionId, $deploymentId); + $response = $this->getDeploymentDownload($functionId, $deploymentId, 'source'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals('application/gzip', $response['headers']['content-type']); $this->assertGreaterThan(0, $response['headers']['content-length']); @@ -525,7 +525,7 @@ class FunctionsConsoleClientTest extends Scope $deploymentMd5 = \md5($response['body']); - $response = $this->getBuildDownload($functionId, $deploymentId); + $response = $this->getDeploymentDownload($functionId, $deploymentId, 'output'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals('application/gzip', $response['headers']['content-type']); $this->assertGreaterThan(0, $response['headers']['content-length']); diff --git a/tests/e2e/Services/Sites/SitesBase.php b/tests/e2e/Services/Sites/SitesBase.php index cdb7b3e2ad..6e597b31ec 100644 --- a/tests/e2e/Services/Sites/SitesBase.php +++ b/tests/e2e/Services/Sites/SitesBase.php @@ -348,22 +348,14 @@ trait SitesBase return $domain; } - protected function getDeploymentDownload(string $siteId, string $deploymentId): mixed + protected function getDeploymentDownload(string $siteId, string $deploymentId, string $type): mixed { $response = $this->client->call(Client::METHOD_GET, '/sites/' . $siteId . '/deployments/' . $deploymentId . '/download', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), []); - - return $response; - } - - protected function getBuildDownload(string $siteId, string $deploymentId): mixed - { - $response = $this->client->call(Client::METHOD_GET, '/sites/' . $siteId . '/deployments/' . $deploymentId . '/build/download', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), []); + ], $this->getHeaders()), [ + 'type' => $type, + ]); return $response; } diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php index c6eb50fdee..3e6132c845 100644 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -1657,7 +1657,7 @@ class SitesCustomServerTest extends Scope $this->assertNotEmpty($deploymentId); - $response = $this->getDeploymentDownload($siteId, $deploymentId); + $response = $this->getDeploymentDownload($siteId, $deploymentId, 'source'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals('application/gzip', $response['headers']['content-type']); $this->assertGreaterThan(0, $response['headers']['content-length']); @@ -1665,7 +1665,7 @@ class SitesCustomServerTest extends Scope $deploymentMd5 = \md5($response['body']); - $response = $this->getBuildDownload($siteId, $deploymentId); + $response = $this->getDeploymentDownload($siteId, $deploymentId, 'output'); $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals('application/gzip', $response['headers']['content-type']); $this->assertGreaterThan(0, $response['headers']['content-length']);