diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..88262a2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ + +/vendor/ +src/Appwrite/CLI/.prefs.json \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..43c6fa5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM composer:2.0 as step0 + +WORKDIR /usr/local/code/ +COPY composer.lock /usr/local/code/ +COPY composer.json /usr/local/code/ +RUN composer update --ignore-platform-reqs --optimize-autoloader \ + --no-plugins --no-scripts --prefer-dist \ + `if [ "$TESTING" != "true" ]; then echo "--no-dev"; fi` + +# Add Source Code +COPY ./src /usr/local/code/src +COPY ./bin /usr/local/bin + +# Executables +RUN chmod +x /usr/local/bin/users \ No newline at end of file diff --git a/bin/users b/bin/users new file mode 100644 index 0000000..d64fb1b --- /dev/null +++ b/bin/users @@ -0,0 +1,3 @@ +#!/bin/sh + +php /usr/local/code/src/Appwrite/CLI/services/users.php $@ \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..e3421b5 --- /dev/null +++ b/composer.json @@ -0,0 +1,18 @@ +{ + "name": "appwrite/appwrite-cli", + "description": "A CLI to interact with the appwrite server", + "authors": [ + { + "name": "Christy Jacob", + "email": "christyjacob4@gmail.com" + } + ], + "autoload": { + "psr-4": {"Appwrite\\": "src/Appwrite"} + }, + "require": { + "php": ">=7.4.0", + "utopia-php/cli": "^0.7.1", + "jc21/clitable": "^1.2" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..acaa450 --- /dev/null +++ b/composer.lock @@ -0,0 +1,173 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "5d1311a8b197c05f80c549205714205e", + "packages": [ + { + "name": "jc21/clitable", + "version": "1.2", + "source": { + "type": "git", + "url": "https://github.com/jc21/clitable.git", + "reference": "a9ff1fcdff131d25e90a9ae839a5805f6963b3da" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jc21/clitable/zipball/a9ff1fcdff131d25e90a9ae839a5805f6963b3da", + "reference": "a9ff1fcdff131d25e90a9ae839a5805f6963b3da", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "victorjonsson/markdowndocs": "dev-master" + }, + "type": "library", + "autoload": { + "psr-0": { + "jc21": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-1-Clause" + ], + "authors": [ + { + "name": "Jamie Curnow", + "email": "jc@jc21.com" + } + ], + "description": "CLI Table output for PHP scripts", + "homepage": "https://github.com/jc21/clitable", + "keywords": [ + "cli", + "command", + "line", + "table" + ], + "support": { + "issues": "https://github.com/jc21/clitable/issues", + "source": "https://github.com/jc21/clitable/tree/v1.2" + }, + "time": "2019-04-28T00:06:48+00:00" + }, + { + "name": "utopia-php/cli", + "version": "0.7.3", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/cli.git", + "reference": "0337918242278e0cf98f8dcab2e75b5a3153b856" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/cli/zipball/0337918242278e0cf98f8dcab2e75b5a3153b856", + "reference": "0337918242278e0cf98f8dcab2e75b5a3153b856", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "utopia-php/framework": "0.*.*" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "vimeo/psalm": "4.0.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\CLI\\": "src/CLI" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eldad Fux", + "email": "eldad@appwrite.io" + } + ], + "description": "A simple CLI library to manage command line applications", + "keywords": [ + "cli", + "command line", + "framework", + "php", + "upf", + "utopia" + ], + "support": { + "issues": "https://github.com/utopia-php/cli/issues", + "source": "https://github.com/utopia-php/cli/tree/0.7.3" + }, + "time": "2020-11-02T07:50:18+00:00" + }, + { + "name": "utopia-php/framework", + "version": "0.10.0", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/framework.git", + "reference": "65909bdb24ef6b6c6751abfdea90caf96bbc6c50" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/framework/zipball/65909bdb24ef6b6c6751abfdea90caf96bbc6c50", + "reference": "65909bdb24ef6b6c6751abfdea90caf96bbc6c50", + "shasum": "" + }, + "require": { + "php": ">=7.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.4", + "vimeo/psalm": "4.0.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eldad Fux", + "email": "eldad@appwrite.io" + } + ], + "description": "A simple, light and advanced PHP framework", + "keywords": [ + "framework", + "php", + "upf" + ], + "support": { + "issues": "https://github.com/utopia-php/framework/issues", + "source": "https://github.com/utopia-php/framework/tree/0.10.0" + }, + "time": "2020-12-26T12:02:39+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=7.4.0" + }, + "platform-dev": [], + "plugin-api-version": "2.0.0" +} diff --git a/src/Appwrite/CLI/Client.php b/src/Appwrite/CLI/Client.php new file mode 100644 index 0000000..7dcf4f5 --- /dev/null +++ b/src/Appwrite/CLI/Client.php @@ -0,0 +1,350 @@ + '', + self::PREFERENCE_APPWRITE_PROJECT => '', + self::PREFERENCE_APPWRITE_KEY => '', + self::PREFERENCE_APPWRITE_LOCALE => '' + ]; + + /** + * Is Self Signed Certificates Allowed? + * + * @var bool + */ + private $selfSigned = false; + + /** + * Global Headers + * + * @var array + */ + private $headers = [ + 'content-type' => '', + 'x-sdk-version' => 'appwrite:php:1.1.0', + ]; + + /** + * SDK constructor. + */ + public function __construct() + { + /* Load user defaults from a json file if available + Else Propmt the user to enter the details + */ + if (!$this->loadPreferences()) { + $this->promptUser(); + } + + $this->setProject($this->preferences[self::PREFERENCE_APPWRITE_PROJECT]) + ->setKey($this->preferences[self::PREFERENCE_APPWRITE_KEY]) + ->setLocale($this->preferences[self::PREFERENCE_APPWRITE_LOCALE]); + } + + public function getPreference(string $key): string + { + return $this->preferences[$key]; + } + + public function setPreference(string $key , string $value) + { + $this->preferences[$key] = $value; + } + + private function promptUser() + { + if(empty($this->getPreference(self::PREFERENCE_ENDPOINT))) { + $endpoint = Console::confirm('Choose your API Endpoint: ( default: http://localhost/v1 )'); + $this->setPreference(self::PREFERENCE_ENDPOINT, ($endpoint) ? $endpoint : 'http://localhost/v1'); + } + + if(empty($this->getPreference(self::PREFERENCE_APPWRITE_PROJECT))) { + $project = Console::confirm('Enter your project ID from the Appwrite console: '); + if (empty($project)) { + Console::error("You cannot continue without a project ID. Exiting..."); + exit(); + } + $this->setPreference(self::PREFERENCE_APPWRITE_PROJECT, $project); + } + + if(empty($this->getPreference(self::PREFERENCE_APPWRITE_KEY))) { + $key = Console::confirm('Enter your project key from the Appwrite console: '); + if (empty($key)) { + Console::error("You cannot continue without a project key. Exiting..."); + exit(); + } + $this->setPreference(self::PREFERENCE_APPWRITE_KEY, $key); + } + + if(empty($this->getPreference(self::PREFERENCE_APPWRITE_LOCALE))) { + $locale = Console::confirm('Enter your locale : ( default : en-IN )'); + $this->setPreference(self::PREFERENCE_APPWRITE_LOCALE, $locale ? $locale : "en-IN" ); + } + + $this->savePreferences(); + } + + /** + * Function to load user preferences from + * the JSON file + */ + private function loadPreferences(string $filename = self::USER_PREFERENCES_FILE): bool + { + try { + $jsondata = @file_get_contents($filename); + if($jsondata === FALSE) { + return false; + } + + $arr_data = json_decode($jsondata, true); + $this->preferences = array_replace($this->preferences, $arr_data); + if (!$this->isPreferenceLoaded()) { + return false; + } + } catch (Exception $e) { + return false; + } + + return true; + } + + private function isPreferenceLoaded() : bool { + if(empty($this->getPreference(self::PREFERENCE_ENDPOINT))) return false; + if(empty($this->getPreference(self::PREFERENCE_APPWRITE_PROJECT))) return false; + if(empty($this->getPreference(self::PREFERENCE_APPWRITE_KEY))) return false; + if(empty($this->getPreference(self::PREFERENCE_APPWRITE_LOCALE))) return false; + return true; + } + + /** + * Function to write user preferences to + * the JSON file + */ + private function savePreferences(string $filename = self::USER_PREFERENCES_FILE) + { + $jsondata = json_encode($this->preferences, JSON_PRETTY_PRINT); + if (file_put_contents($filename, $jsondata)) { + echo 'Data successfully saved'; + } else { + echo "error"; + } + } + + /** + * Set Project + * + * Your project ID + * + * @param string $value + * + * @return Client + */ + public function setProject($value) + { + $this->addHeader(self::PREFERENCE_APPWRITE_PROJECT, $value); + + return $this; + } + + /** + * Set Key + * + * Your secret API key + * + * @param string $value + * + * @return Client + */ + public function setKey($value) + { + $this->addHeader(self::PREFERENCE_APPWRITE_KEY, $value); + + return $this; + } + + /** + * Set Locale + * + * @param string $value + * + * @return Client + */ + public function setLocale($value) + { + $this->addHeader(self::PREFERENCE_APPWRITE_LOCALE, $value); + + return $this; + } + + + /*** + * @param bool $status + * @return $this + */ + public function setSelfSigned($status = true) + { + $this->selfSigned = $status; + + return $this; + } + + /*** + * @param $endpoint + * @return $this + */ + public function setEndpoint($endpoint) + { + $this->setPreference(self::PREFERENCE_ENDPOINT. $endpoint); + return $this; + } + + /** + * @param $key + * @param $value + */ + public function addHeader($key, $value) + { + $this->headers[strtolower($key)] = strtolower($value); + + return $this; + } + + /** + * Call + * + * Make an API call + * + * @param string $method + * @param string $path + * @param array $params + * @param array $headers + * @return array|string + * @throws Exception + */ + public function call($method, $path = '', $headers = array(), array $params = array()) + { + $headers = array_merge($this->headers, $headers); + $ch = curl_init($this->getPreference(self::PREFERENCE_ENDPOINT) . $path . (($method == self::METHOD_GET && !empty($params)) ? '?' . http_build_query($params) : '')); + $responseHeaders = []; + $responseStatus = -1; + $responseType = ''; + $responseBody = ''; + + switch ($headers['content-type']) { + case 'application/json': + $query = json_encode($params); + break; + + case 'multipart/form-data': + $query = $this->flatten($params); + break; + + default: + $query = http_build_query($params); + break; + } + + foreach ($headers as $i => $header) { + $headers[] = $i . ':' . $header; + unset($headers[$i]); + } + + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_USERAGENT, php_uname('s') . '-' . php_uname('r') . ':php-' . phpversion()); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$responseHeaders) { + $len = strlen($header); + $header = explode(':', strtolower($header), 2); + + if (count($header) < 2) { // ignore invalid headers + return $len; + } + + $responseHeaders[strtolower(trim($header[0]))] = trim($header[1]); + + return $len; + }); + + if ($method != self::METHOD_GET) { + curl_setopt($ch, CURLOPT_POSTFIELDS, $query); + } + + // Allow self signed certificates + if ($this->selfSigned) { + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + } + + $responseBody = curl_exec($ch); + $responseType = (isset($responseHeaders['content-type'])) ? $responseHeaders['content-type'] : ''; + $responseStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE); + + switch (substr($responseType, 0, strpos($responseType, ';'))) { + case 'application/json': + $responseBody = json_decode($responseBody, true); + break; + } + + if ((curl_errno($ch)/* || 200 != $responseStatus*/)) { + throw new Exception(curl_error($ch) . ' with status code ' . $responseStatus, $responseStatus); + } + + curl_close($ch); + + return $responseBody; + } + + /** + * Flatten params array to PHP multiple format + * + * @param array $data + * @param string $prefix + * @return array + */ + private function flatten(array $data, $prefix = '') + { + $output = []; + + foreach ($data as $key => $value) { + $finalKey = $prefix ? "{$prefix}[{$key}]" : $key; + + if (is_array($value)) { + $output += $this->flatten($value, $finalKey); // @todo: handle name collision here if needed + } else { + $output[$finalKey] = $value; + } + } + + return $output; + } +} diff --git a/src/Appwrite/CLI/Parser.php b/src/Appwrite/CLI/Parser.php new file mode 100644 index 0000000..9addd1d --- /dev/null +++ b/src/Appwrite/CLI/Parser.php @@ -0,0 +1,66 @@ + 15 ? substr($value,0,15).'...' : $value; + } +} + +class Parser { + + private const colors = array( + 'blue', + 'red', + 'green', + 'yellow', + 'magenta', + 'cyan', + 'white', + 'grey' + ); + + private const tableColor = self::colors[0]; + private const headerColor = self::colors[2]; + + public function parseResponse($response, $properties = array()) { + + foreach ($response as $key => $value) { + if (is_array($value)) { + $this->drawTable($value, self::headerColor, self::tableColor); + } + else { + $this->drawKeyValue($key, $value); + } + } + } + + private function drawKeyValue($key, $value){ + printf("%s : %s\n", $key, $value); + } + + private function getColor($index = -1) : string { + if ($index != -1) return self::colors[$index % count(self::colors) ]; + return self::colors[array_rand(self::colors)]; + } + + private function drawTable($data, $headerColor, $tableColor, $columnProps = []) { + if (!is_array($data) || count($data) == 0 || !is_array($data[0])) return; + + $keys = array_keys($data[0]); + + $table = new CliTable(); + $table->setTableColor($tableColor); + $table->setHeaderColor($headerColor); + + foreach ($keys as $key => $value) { + $table->addField(ucwords($value), $value, new Manipulators('truncate'), $this->getColor($key)); + } + + $table->injectData($data); + $table->display(); + } +} \ No newline at end of file diff --git a/src/Appwrite/CLI/services/storage.php b/src/Appwrite/CLI/services/storage.php new file mode 100644 index 0000000..e69de29 diff --git a/src/Appwrite/CLI/services/users.php b/src/Appwrite/CLI/services/users.php new file mode 100644 index 0000000..097ca69 --- /dev/null +++ b/src/Appwrite/CLI/services/users.php @@ -0,0 +1,84 @@ +task('list') + ->param('search', '', new Mock(), 'Search query', true) + ->param('limit', 25, new Mock(), 'Limit', true) + ->param('offset', 0, new Mock(), 'offset', true) + ->param('orderType', 'ASC', new Mock(), 'orderType', true) + ->action(function ($search, $limit, $offset, $orderType) { + global $client; + global $parser; + + $path = str_replace([], [], '/users'); + $params = []; + + $params['search'] = $search; + $params['limit'] = $limit; + $params['offset'] = $offset; + $params['orderType'] = $orderType; + + $response = $client->call(Client::METHOD_GET, $path, [ + 'content-type' => 'application/json', + ], $params); + + $parser->parseResponse($response); + }); + + +$cli + ->task('create') + ->param('email', '', new Mock(), "User's email ID" ,true) + ->param('password', '', new Mock(), "User's password", true) + ->param('name', '', new Mock(), "User's Name", true) + ->action(function($email, $password, $name) { + global $client; + global $parser; + + if (empty($email)) { + Console::error("Cannot proceed without email. Exiting ..."); + exit(); + } + + if (empty($password)) { + Console::error("Cannot proceed without password. Exiting ..."); + exit(); + } + + if (empty($name)) { + Console::error("Cannot proceed without name. Exiting ..."); + exit(); + } + + $path = str_replace([], [], '/users'); + $params = []; + + $params['email'] = $email; + $params['password'] = $password; + $params['name'] = $name; + + $response = $client->call(Client::METHOD_POST, $path, [ + 'content-type' => 'application/json', + ], $params); + + $parser->parseResponse($response); + }); + +$cli->run();