diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c815860 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,16 @@ +name: CI + +on: + push: + pull_request: + workflow_dispatch: + # Every Tuesday at 2:25pm UTC + schedule: + - cron: '25 14 * * 2' + +jobs: + ci: + name: CI + # Only run cron on the silverstripe account + if: (github.event_name == 'schedule' && github.repository_owner == 'silverstripe') || (github.event_name != 'schedule') + uses: silverstripe/gha-ci/.github/workflows/ci.yml@v1 diff --git a/.gitignore b/.gitignore index 1482e5d..19019e2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ build/ vendor/ .DS_Store +.phpunit.result.cache diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 9945986..0000000 --- a/.travis.yml +++ /dev/null @@ -1,27 +0,0 @@ -sudo: false -language: php -php: - - '5.6' - -env: - global: - secure: "fQhSvmkxT2mrzIL9tfY54KVRIXWrMZo1bjOx4mfNOrKyrkG3GnIUndSmTAZQU/NTiXDIH+N8aIEP3VpDEfDv4XxI3A+5CAayJKTGuLDcyXy0vVf54BfzebG38oH93v2VE2YTeoG/L+Nb6A4+/hb+/sG2vWol+IKD1av/HxZdZZo=" - -before_script: - - git config user.email "community@silverstripe.org" - - git config user.name "SilverStripe" - # This inverted conditional format ensures that PRs/non-master branches don't report a failure - - '[ "${TRAVIS_PULL_REQUEST}" != "false" -o "${TRAVIS_BRANCH}" != "master" ] || git config remote.origin.fetch +refs/heads/*:refs/remotes/origin/*' - - '[ "${TRAVIS_PULL_REQUEST}" != "false" -o "${TRAVIS_BRANCH}" != "master" ] || git config push.default simple' - - '[ "${TRAVIS_PULL_REQUEST}" != "false" -o "${TRAVIS_BRANCH}" != "master" ] || git fetch --unshallow' - - '[ "${TRAVIS_PULL_REQUEST}" != "false" -o "${TRAVIS_BRANCH}" != "master" ] || git remote set-url origin "https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}"' - - '[ "${TRAVIS_PULL_REQUEST}" != "false" -o "${TRAVIS_BRANCH}" != "master" ] || git checkout -qf "${TRAVIS_COMMIT}"' - -script: - - composer selfupdate || true - - composer install --dev --no-progress --no-interaction - - bin/build-phar - - vendor/bin/phpunit tests - -after_success: - - '[ "${TRAVIS_PULL_REQUEST}" != "false" -o "${TRAVIS_BRANCH}" != "master" ] || bin/upload-phar > /dev/null 2>&1' diff --git a/README.md b/README.md index 44b0e42..cf35841 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,8 @@ # SSPak -[![Build Status](https://api.travis-ci.com/silverstripe/sspak.svg?branch=master)](https://travis-ci.com/silverstripe/sspak) -[![SilverStripe supported module](https://img.shields.io/badge/silverstripe-supported-0071C4.svg)](https://www.silverstripe.org/software/addons/silverstripe-commercially-supported-module-list/) -[![Code Quality](http://img.shields.io/scrutinizer/g/silverstripe/sspak.svg?style=flat-square)](https://scrutinizer-ci.com/g/silverstripe/sspak) +[![Silverstripe supported module](https://img.shields.io/badge/silverstripe-supported-0071C4.svg)](https://www.silverstripe.org/software/addons/silverstripe-commercially-supported-module-list/) -SSPak is a SilverStripe tool for managing database and assets content, for back-up, restoration, or transfer between +SSPak is a Silverstripe tool for managing database and assets content, for back-up, restoration, or transfer between environments. ## The file format @@ -15,9 +13,9 @@ An sspak file is either a Phar (executable) file or a Tar (non-executable) file, * **assets.tar.gz:** A gzipped tar file containing all assets. The root directory within the tar file must be called "assets". * **git-remote:** A text file of the following form: - remote = (url) - branch = (name) - sha = (sha-hash) + remote = (url) + branch = (name) + sha = (sha-hash) By convention, the file should have the extension `.sspak` for non-executable versions, and `.sspak.phar` for executable versions. @@ -35,98 +33,96 @@ You can install this package globally with Composer (ensure your composer bin is If you have cURL, run this command (everything except for the `$>` part): - $> curl -sS https://silverstripe.github.io/sspak/install | php -- /usr/local/bin + $> curl -sS https://silverstripe.github.io/sspak/install | php -- /usr/local/bin The final argument is the directory that the script will be loaded into. If omitted, the script will be installed into the current directory. If you don't have permission to write to the directory, "sudo" will be used to escalate permissions. For example, this would also work: - $> cd /usr/local/bin - $> curl -sS https://silverstripe.github.io/sspak/install | sudo php + $> cd /usr/local/bin + $> curl -sS https://silverstripe.github.io/sspak/install | sudo php ### Manually If you prefer not to use the installer, you can download the script and copy it to your executable path as follows: - $> wget https://silverstripe.github.io/sspak/sspak.phar - $> chmod +x sspak.phar - $> sudo mv sspak.phar /usr/local/bin/sspak - + $> wget https://silverstripe.github.io/sspak/sspak.phar + $> chmod +x sspak.phar + $> sudo mv sspak.phar /usr/local/bin/sspak ## Common Issues - Creating archive disabled by the php.ini setting phar.readonly + Creating archive disabled by the php.ini setting phar.readonly Set your phar.readonly setting to false in your php.ini (and php-cli.ini) files. - ## Use All sspak commands take the following general form. - $> sspak (command) (from) (to) + $> sspak (command) (from) (to) Create an sspak file and save to /tmp: - $> sspak save /var/www /tmp/site.sspak + $> sspak save /var/www /tmp/site.sspak Create an sspak file based on a remote site: - $> sspak save me@prodserver:/var/www prod-site.sspak + $> sspak save me@prodserver:/var/www prod-site.sspak Create an sspak file based on a remote site using a specific private key to connect: - $> sspak save --identity=prodserver.key me@prodserver:/var/www prod-site.sspak + $> sspak save --identity=prodserver.key me@prodserver:/var/www prod-site.sspak Create an executable sspak file by adding a phar extension: - $> sspak save me@prodserver:/var/www prod-site.sspak.phar + $> sspak save me@prodserver:/var/www prod-site.sspak.phar Create an sspak from existing files: - $> sspak saveexisting --db=/path/to/database.sql --assets=/path/to/assets /tmp/site.sspak + $> sspak saveexisting --db=/path/to/database.sql --assets=/path/to/assets /tmp/site.sspak Extract files from an existing sspak into the specified directory: - $> sspak extract /tmp/site.sspak /destination/path + $> sspak extract /tmp/site.sspak /destination/path Load an sspak file into a local instance: - $> sspak load prod-site.sspak ~/Sites/devsite + $> sspak load prod-site.sspak ~/Sites/devsite Load an sspak file into a local instance, dropping the existing DB first (mysql only): - $> sspak load prod-site.sspak ~/Sites/devsite --drop-db + $> sspak load prod-site.sspak ~/Sites/devsite --drop-db Load an sspak file into a remote instance using a specific private key to connect: - $> sspak save --identity=backupserver.key prod-site.sspak me@backupserver:/var/www + $> sspak save --identity=backupserver.key prod-site.sspak me@backupserver:/var/www Transfer in one step: *(not implemented yet)* - $> sspak transfer me@prodserver:/var/www ~/Sites/devsite + $> sspak transfer me@prodserver:/var/www ~/Sites/devsite Sudo as www-data to perform the actions - $> sspak save --sudo=www-data me@prodserver:/var/www prod-site.sspak - $> sspak load --sudo=www1 prod-site.sspak ~/Sites/devsite - $> sspak transfer --from-sudo=www-data --to-sudo=www1 me@prodserver:/var/www ~/Sites/devsite + $> sspak save --sudo=www-data me@prodserver:/var/www prod-site.sspak + $> sspak load --sudo=www1 prod-site.sspak ~/Sites/devsite + $> sspak transfer --from-sudo=www-data --to-sudo=www1 me@prodserver:/var/www ~/Sites/devsite Save only the database: - $> sspak save --db me@prodserver:/var/www dev.sspak + $> sspak save --db me@prodserver:/var/www dev.sspak Load only the assets: - $> sspak load --assets dev.sspak ~/Sites/devsite + $> sspak load --assets dev.sspak ~/Sites/devsite Install a new site from an sspak (needs to contain a git-remote): - $> sspak install newsite.sspak ~/Sites/newsite + $> sspak install newsite.sspak ~/Sites/newsite Save all while using a custom TMP folder (make sure the folder exists and is writable): - $> TMPDIR="/tmp/my_custom_tmp" sspak save /var/www /tmp/site.sspak + $> TMPDIR="/tmp/my_custom_tmp" sspak save /var/www /tmp/site.sspak ## Caveats @@ -134,7 +130,7 @@ If you don't have PKI passwordless log-in into remote servers, you will be asked ## How it works -sspak relies on the SilverStripe executable code to determine database credentials. It does this by using a small script, sspak-sniffer.php, which it uploads to the /tmp folder of any remote servers. +sspak relies on the Silverstripe executable code to determine database credentials. It does this by using a small script, sspak-sniffer.php, which it uploads to the /tmp folder of any remote servers. This script returns database credentials and the location of the assets path. Once it has that, it will remotely execute mysql, mysqldump and tar commands to archive or restore the content. diff --git a/bin/build-phar b/bin/build-phar index fe69b3f..fe2cfbb 100755 --- a/bin/build-phar +++ b/bin/build-phar @@ -2,20 +2,24 @@ unshiftUnnamed('@self'); - if(!in_array($argObj->getAction(), array("install", "load"))) { - echo "Self-extracting sspaks can only use 'install' and 'load' actions.\n"; - exit(3); - } + $argObj->unshiftUnnamed('@self'); + if(!in_array($argObj->getAction(), array("install", "load"))) { + echo "Self-extracting sspaks can only use 'install' and 'load' actions.\n"; + exit(3); + } } */ @@ -39,15 +39,15 @@ $action = strtolower($argObj->getAction()); if(!$action) $action = 'help'; if(isset($allowedActions[$action])) { - $method = $allowedActions[$action]['method']; - try { - $ssPak->$method($argObj); - } catch(Exception $e) { - echo $e->getMessage() . "\n"; - exit(4); - } + $method = $allowedActions[$action]['method']; + try { + $ssPak->$method($argObj); + } catch(Exception $e) { + echo $e->getMessage() . "\n"; + exit(4); + } } else { - echo "Unrecognised action '" . $action . "'.\n"; - $ssPak->help($argObj); - exit(3); + echo "Unrecognised action '" . $action . "'.\n"; + $ssPak->help($argObj); + exit(3); } diff --git a/bin/upload-phar b/bin/upload-phar index 8bdebe1..2a0df53 100755 --- a/bin/upload-phar +++ b/bin/upload-phar @@ -10,14 +10,14 @@ require_once(PACKAGE_ROOT . 'src/FilesystemEntity.php'); require_once(PACKAGE_ROOT . 'src/SSPakFile.php'); if(empty($_SERVER['argv'][1])) { - $pharFile = dirname(dirname(__FILE__))."/build/sspak.phar"; + $pharFile = dirname(dirname(__FILE__))."/build/sspak.phar"; } else { - $pharFile = $_SERVER['argv'][1]; + $pharFile = $_SERVER['argv'][1]; } if(!file_exists(dirname($pharFile))) { - echo "Creating directory " . dirname($pharFile) . "...\n"; - mkdir(dirname($pharFile)); + echo "Creating directory " . dirname($pharFile) . "...\n"; + mkdir(dirname($pharFile)); } echo "Building sspak executable $pharFile\n"; @@ -27,37 +27,37 @@ $phar = new SSPakFile($pharFile, null, 'sspak-bin.phar'); $phar->makeExecutable(); if(empty($_SERVER['argv'][1])) { - chdir(PACKAGE_ROOT); - - echo "Creating version file...\n"; - $versionFile = dirname($pharFile) . '/version'; - file_put_contents($versionFile, `git log -1 --oneline --pretty=%H`); - - echo "Copying install file to build...\n"; - copy(PACKAGE_ROOT . "bin/install", dirname($pharFile) . "/install"); - - echo "Checking out gh-pages...\n"; - try { - shell_cmd("git checkout gh-pages"); - shell_cmd("git pull"); - - echo "Committing sspak.phar and version into it...\n"; - shell_cmd("cp build/sspak.phar ."); - shell_cmd("cp build/version ."); - shell_cmd("cp build/install ."); - shell_cmd("git add sspak.phar version install"); - shell_cmd("git commit -m 'Updating sspak.phar package'"); - - echo "Pushing to github server...\n"; - shell_cmd("git push"); - echo "Checking out master again...\n"; - shell_cmd("git checkout master"); - } catch(Exception $e) { - $exitCode = 1; - echo $e->getMessage() . "\n"; - echo "Checking out master again...\n"; - shell_cmd("git checkout -f master"); - } + chdir(PACKAGE_ROOT); + + echo "Creating version file...\n"; + $versionFile = dirname($pharFile) . '/version'; + file_put_contents($versionFile, `git log -1 --oneline --pretty=%H`); + + echo "Copying install file to build...\n"; + copy(PACKAGE_ROOT . "bin/install", dirname($pharFile) . "/install"); + + echo "Checking out gh-pages...\n"; + try { + shell_cmd("git checkout gh-pages"); + shell_cmd("git pull"); + + echo "Committing sspak.phar and version into it...\n"; + shell_cmd("cp build/sspak.phar ."); + shell_cmd("cp build/version ."); + shell_cmd("cp build/install ."); + shell_cmd("git add sspak.phar version install"); + shell_cmd("git commit -m 'Updating sspak.phar package'"); + + echo "Pushing to github server...\n"; + shell_cmd("git push"); + echo "Checking out master again...\n"; + shell_cmd("git checkout master"); + } catch(Exception $e) { + $exitCode = 1; + echo $e->getMessage() . "\n"; + echo "Checking out master again...\n"; + shell_cmd("git checkout -f master"); + } } echo "Done.\n"; @@ -65,7 +65,7 @@ echo "Done.\n"; exit($exitCode); function shell_cmd($cmd) { - $retVal = 0; - passthru($cmd, $retVal); - if($retVal != 0) throw new Exception("Error executing '$cmd', return value $retVal"); + $retVal = 0; + passthru($cmd, $retVal); + if($retVal != 0) throw new Exception("Error executing '$cmd', return value $retVal"); } diff --git a/composer.json b/composer.json index a3f0ee0..d550d42 100644 --- a/composer.json +++ b/composer.json @@ -1,19 +1,27 @@ { - "name": "silverstripe/sspak", - "description": "CLI tool for saving & loading data in SilverStripe installations", - "license": "BSD-3-Clause", - "authors": [ - { - "name": "Sam Minnee", - "email": "sam@silverstripe.com" - } - ], - "autoload": { - "classmap": [ "src/" ] - }, - "bin": ["bin-composer/sspak"], - "require": {}, - "require-dev": { - "phpunit/phpunit": "^3.7" - } + "name": "silverstripe/sspak", + "description": "CLI tool for saving & loading data in SilverStripe installations", + "license": "BSD-3-Clause", + "authors": [ + { + "name": "Sam Minnee", + "email": "sam@silverstripe.com" + } + ], + "autoload": { + "classmap": [ "src/" ], + "autoload": { + "psr-4": { + "SilverStripe\\SSPak\\": "src", + "SilverStripe\\SSPak\\Tests\\": "tests" + } + } + }, + "bin": ["bin-composer/sspak"], + "require": { + }, + "require-dev": { + "phpunit/phpunit": "^9.5", + "squizlabs/php_codesniffer": "^3" + } } diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 0000000..3cfc88f --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,16 @@ + + + CodeSniffer ruleset for SilverStripe coding conventions. + + src + tests + + /vendor/* + + + + + + + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..f2c3a73 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,16 @@ + + + + + tests/ + + + + + src/ + + tests/ + + + + diff --git a/src/Args.php b/src/Args.php index 6bf6056..52c2eb4 100644 --- a/src/Args.php +++ b/src/Args.php @@ -1,82 +1,102 @@ namedArgs[$matches[1]] = $matches[2]; - } else if(preg_match('/^--([^=]+)$/', $arg, $matches)) { - $this->namedArgs[$matches[1]] = true; - } else { - $this->unnamedArgs[] = $arg; - } - } + foreach ($args as $arg) { + if (preg_match('/^--([^=]+)=(.*)$/', $arg, $matches)) { + $this->namedArgs[$matches[1]] = $matches[2]; + } elseif (preg_match('/^--([^=]+)$/', $arg, $matches)) { + $this->namedArgs[$matches[1]] = true; + } else { + $this->unnamedArgs[] = $arg; + } + } - $this->action = array_shift($this->unnamedArgs); - } + $this->action = array_shift($this->unnamedArgs); + } - public function unshiftUnnamed($arg) { - array_unshift($this->unnamedArgs, $arg); - } + public function unshiftUnnamed($arg) + { + array_unshift($this->unnamedArgs, $arg); + } - public function getNamedArgs() { - return $this->namedArgs; - } + public function getNamedArgs() + { + return $this->namedArgs; + } - public function getUnnamedArgs() { - return $this->unnamedArgs; - } + public function getUnnamedArgs() + { + return $this->unnamedArgs; + } - public function getAction() { - return $this->action; - } + public function getAction() + { + return $this->action; + } - /** - * Return the unnamed arg of the given index (0 = first) - */ - public function unnamed($idx) { - return isset($this->unnamedArgs[$idx]) ? $this->unnamedArgs[$idx] : null; - } + /** + * Return the unnamed arg of the given index (0 = first) + */ + public function unnamed($idx) + { + return isset($this->unnamedArgs[$idx]) ? $this->unnamedArgs[$idx] : null; + } - /** - * Return the sudo argument, preferring a more specific one with the given optional prefix - */ - public function sudo($optionalPrefix) { - if(!empty($this->namedArgs[$optionalPrefix . '-sudo'])) return $this->namedArgs[$optionalPrefix . '-sudo']; - else if(!empty($this->namedArgs['sudo'])) return $this->namedArgs['sudo']; - else return null; - } + /** + * Return the sudo argument, preferring a more specific one with the given optional prefix + */ + public function sudo($optionalPrefix) + { + if (!empty($this->namedArgs[$optionalPrefix . '-sudo'])) { + return $this->namedArgs[$optionalPrefix . '-sudo']; + } elseif (!empty($this->namedArgs['sudo'])) { + return $this->namedArgs['sudo']; + } else { + return null; + } + } - /** - * Return the pak-parks arguments, as a map of part => boolean - */ - public function pakParts() { - // Look up which parts of the sspak are going to be saved - $pakParks = array(); - foreach(array('assets','db','git-remote') as $part) { - $pakParts[$part] = !empty($this->namedArgs[$part]); - } + /** + * Return the pak-parks arguments, as a map of part => boolean + */ + public function pakParts() + { + // Look up which parts of the sspak are going to be saved + $pakParks = array(); + foreach (array('assets','db','git-remote') as $part) { + $pakParts[$part] = !empty($this->namedArgs[$part]); + } - // Default to db and assets - if(!array_filter($pakParts)) $pakParts = array('db' => true, 'assets' => true, 'git-remote' => true); - return $pakParts; - } + // Default to db and assets + if (!array_filter($pakParts)) { + $pakParts = array('db' => true, 'assets' => true, 'git-remote' => true); + } + return $pakParts; + } - public function requireUnnamed($items) { - if(sizeof($this->unnamedArgs) < sizeof($items)) { - echo "Usage: {$_SERVER['argv'][0]} " . $this->action . " ("; - echo implode(") (", $items); - echo ")\n"; - throw new Exception('Arguments missing.'); - } - } + public function requireUnnamed($items) + { + if (sizeof($this->unnamedArgs) < sizeof($items)) { + echo "Usage: {$_SERVER['argv'][0]} " . $this->action . " ("; + echo implode(") (", $items); + echo ")\n"; + throw new Exception('Arguments missing.'); + } + } } diff --git a/src/DataExtractor/CsvTableReader.php b/src/DataExtractor/CsvTableReader.php index e6f3ab9..abc96cc 100644 --- a/src/DataExtractor/CsvTableReader.php +++ b/src/DataExtractor/CsvTableReader.php @@ -1,70 +1,76 @@ filename = $filename; + } - public function __construct($filename) { - $this->filename = $filename; - } + public function getColumns() + { + if (!$this->columns) { + $this->initColumns(); + } + return $this->columns; + } - public function getColumns() { - if (!$this->columns) { - $this->initColumns(); - } - return $this->columns; - } + public function getIterator() + { + $this->columns = null; + $this->initColumns(); - public function getIterator() { - $this->columns = null; - $this->initColumns(); + while (($row = $this->getRow()) !== false) { + yield $this->mapToColumns($row); + } - while(($row = $this->getRow()) !== false) { - yield $this->mapToColumns($row); - } + $this->close(); + } - $this->close(); - } + private function mapToColumns($row) + { + $record = []; + foreach ($row as $i => $value) { + if (isset($this->columns[$i])) { + $record[$this->columns[$i]] = $value; + } else { + throw new \LogicException("Row contains invalid column #$i\n" . var_export($row, true)); + } + } + return $record; + } - private function mapToColumns($row) { - $record = []; - foreach($row as $i => $value) - { - if(isset($this->columns[$i])) { - $record[$this->columns[$i]] = $value; - } else { - throw new \LogicException("Row contains invalid column #$i\n" . var_export($row, true)); - } - } - return $record; - } + private function initColumns() + { + $this->open(); + $this->columns = $this->getRow(); + } - private function initColumns() { - $this->open(); - $this->columns = $this->getRow(); - } + private function getRow() + { + return fgetcsv($this->handle); + } - private function getRow() { - return fgetcsv($this->handle); - } + private function open() + { + if ($this->handle) { + fclose($this->handle); + $this->handle = null; + } + $this->handle = fopen($this->filename, 'r'); + } - private function open() { - if ($this->handle) { - fclose($this->handle); - $this->handle = null; - } - $this->handle = fopen($this->filename, 'r'); - } - - private function close() { - if ($this->handle) { - fclose($this->handle); - $this->handle = null; - } - } + private function close() + { + if ($this->handle) { + fclose($this->handle); + $this->handle = null; + } + } } diff --git a/src/DataExtractor/CsvTableWriter.php b/src/DataExtractor/CsvTableWriter.php index f8e147f..87ab92f 100644 --- a/src/DataExtractor/CsvTableWriter.php +++ b/src/DataExtractor/CsvTableWriter.php @@ -1,64 +1,70 @@ filename = $filename; + } - public function __construct($filename) { - $this->filename = $filename; - } + public function start($columns) + { + $this->open(); + $this->putRow($columns); + $this->columns = $columns; + } - public function start($columns) { - $this->open(); - $this->putRow($columns); - $this->columns = $columns; - } + public function finish() + { + $this->close(); + } - public function finish() { - $this->close(); - } + public function writeRecord($record) + { + if (!$this->columns) { + $this->start(array_keys($record)); + } - public function writeRecord($record) { - if (!$this->columns) { - $this->start(array_keys($record)); - } + $this->putRow($this->mapFromColumns($record)); + } - $this->putRow($this->mapFromColumns($record)); - } + private function mapFromColumns($record) + { + $row = []; + foreach ($this->columns as $i => $column) { + $row[$i] = isset($record[$column]) ? $record[$column] : null; + } + return $row; + } - private function mapFromColumns($record) { - $row = []; - foreach($this->columns as $i => $column) - { - $row[$i] = isset($record[$column]) ? $record[$column] : null; - } - return $row; - } + private function putRow($row) + { + return fputcsv($this->handle, $row); + } - private function putRow($row) { - return fputcsv($this->handle, $row); - } + private function open() + { + if ($this->handle) { + fclose($this->handle); + $this->handle = null; + } + $this->handle = fopen($this->filename, 'w'); + if (!$this->handle) { + throw new \LogicException("Can't open $this->filename for writing."); + } + } - private function open() { - if ($this->handle) { - fclose($this->handle); - $this->handle = null; - } - $this->handle = fopen($this->filename, 'w'); - if (!$this->handle) { - throw new \LogicException("Can't open $this->filename for writing."); - } - } - - private function close() { - if ($this->handle) { - fclose($this->handle); - $this->handle = null; - } - } + private function close() + { + if ($this->handle) { + fclose($this->handle); + $this->handle = null; + } + } } diff --git a/src/DataExtractor/DatabaseConnector.php b/src/DataExtractor/DatabaseConnector.php index 27b76d9..a518efe 100644 --- a/src/DataExtractor/DatabaseConnector.php +++ b/src/DataExtractor/DatabaseConnector.php @@ -1,6 +1,6 @@ basePath = $basePath; - } - - public function connect() { - if ($this->isConnected) { - return; - } - - $this->isConnected = true; - - // Necessary for SilverStripe's _ss_environment.php loader to work - $_SERVER['SCRIPT_FILENAME'] = $this->basePath . '/dummy.php'; - - global $databaseConfig; - - // require composers autoloader - if (file_exists($this->basePath . '/vendor/autoload.php')) { - require_once $this->basePath . '/vendor/autoload.php'; - } - - if (file_exists($this->basePath . '/framework/core/Core.php')) { - require_once($this->basePath . '/framework/core/Core.php'); - } elseif (file_exists($this->basePath . '/sapphire/core/Core.php')) { - require_once($this->basePath . '/sapphire/core/Core.php'); - } else { - throw new \LogicException("No framework/core/Core.php or sapphire/core/Core.php included in project. Perhaps $this->basePath is not a SilverStripe project?"); - } - - // Connect to database - require_once('model/DB.php'); - - if ($databaseConfig) { - DB::connect($databaseConfig); - } else { - throw new \LogicException("No \$databaseConfig found"); - } - } - - public function getDatabase() { - $this->connect(); - - if(method_exists('DB', 'get_conn')) { - return DB::get_conn(); - } else { - return DB::getConn(); - } - } - - /** - * Get a list of tables from the database - */ - public function getTables() { - $this->connect(); - - if(method_exists('DB', 'table_list')) { - return DB::table_list(); - } else { - return DB::tableList(); - } - } - - /** - * Get a list of tables from the database - */ - public function getFieldsForTable($tableName) { - $this->connect(); - - if(method_exists('DB', 'field_list')) { - return DB::field_list($tableName); - } else { - return DB::fieldList($tableName); - } - } - - /** - * Save the named table to the given table write - */ - public function saveTable($tableName, TableWriter $writer) { - $query = $this->getDatabase()->query("SELECT * FROM \"$tableName\""); - - foreach ($query as $record) { - $writer->writeRecord($record); - } - - $writer->finish(); - } - - /** - * Save the named table to the given table write - */ - public function loadTable($tableName, TableReader $reader) { - $this->getDatabase()->clearTable($tableName); - - $fields = $this->getFieldsForTable($tableName); - - foreach($reader as $record) { - foreach ($record as $k => $v) { - if (!isset($fields[$k])) { - unset($record[$k]); - } - } - // TODO: Batch records - $manipulation = [ - $tableName => [ - 'command' => 'insert', - 'fields' => $record, - ], - ]; - DB::manipulate($manipulation); - } - } + private $basePath; + private $isConnected = false; + + public function __construct($basePath) + { + $this->basePath = $basePath; + } + + public function connect() + { + if ($this->isConnected) { + return; + } + + $this->isConnected = true; + + // Necessary for SilverStripe's _ss_environment.php loader to work + $_SERVER['SCRIPT_FILENAME'] = $this->basePath . '/dummy.php'; + + global $databaseConfig; + + // require composers autoloader + if (file_exists($this->basePath . '/vendor/autoload.php')) { + require_once $this->basePath . '/vendor/autoload.php'; + } + + if (file_exists($this->basePath . '/framework/core/Core.php')) { + require_once($this->basePath . '/framework/core/Core.php'); + } elseif (file_exists($this->basePath . '/sapphire/core/Core.php')) { + require_once($this->basePath . '/sapphire/core/Core.php'); + } else { + throw new \LogicException( + "No framework/core/Core.php or sapphire/core/Core.php included in project. " . + "Perhaps $this->basePath is not a SilverStripe project?" + ); + } + + // Connect to database + require_once('model/DB.php'); + + if ($databaseConfig) { + DB::connect($databaseConfig); + } else { + throw new \LogicException("No \$databaseConfig found"); + } + } + + public function getDatabase() + { + $this->connect(); + + if (method_exists('DB', 'get_conn')) { + return DB::get_conn(); + } else { + return DB::getConn(); + } + } + + /** + * Get a list of tables from the database + */ + public function getTables() + { + $this->connect(); + + if (method_exists('DB', 'table_list')) { + return DB::table_list(); + } else { + return DB::tableList(); + } + } + + /** + * Get a list of tables from the database + */ + public function getFieldsForTable($tableName) + { + $this->connect(); + + if (method_exists('DB', 'field_list')) { + return DB::field_list($tableName); + } else { + return DB::fieldList($tableName); + } + } + + /** + * Save the named table to the given table write + */ + public function saveTable($tableName, TableWriter $writer) + { + $query = $this->getDatabase()->query("SELECT * FROM \"$tableName\""); + + foreach ($query as $record) { + $writer->writeRecord($record); + } + + $writer->finish(); + } + + /** + * Save the named table to the given table write + */ + public function loadTable($tableName, TableReader $reader) + { + $this->getDatabase()->clearTable($tableName); + + $fields = $this->getFieldsForTable($tableName); + + foreach ($reader as $record) { + foreach ($record as $k => $v) { + if (!isset($fields[$k])) { + unset($record[$k]); + } + } + // TODO: Batch records + $manipulation = [ + $tableName => [ + 'command' => 'insert', + 'fields' => $record, + ], + ]; + DB::manipulate($manipulation); + } + } } diff --git a/src/DataExtractor/TableReader.php b/src/DataExtractor/TableReader.php index f4b9d43..821313b 100644 --- a/src/DataExtractor/TableReader.php +++ b/src/DataExtractor/TableReader.php @@ -1,18 +1,18 @@ value data - */ - public function writeRecord($record); + /** + * Write a single record. + * @param array $record A map of column => value data + */ + public function writeRecord($record); - /** - * Finish writing. - * writeRecord() must not be called after this - */ - public function finish(); + /** + * Finish writing. + * writeRecord() must not be called after this + */ + public function finish(); } diff --git a/src/Executor.php b/src/Executor.php index 4e9550a..74112c1 100644 --- a/src/Executor.php +++ b/src/Executor.php @@ -1,153 +1,71 @@ true, - 'inputContent' => null, - 'inputFile' => null, - 'inputStream' => null, - 'outputFile' => null, - 'outputFileAppend' => false, - 'outputStream' => null, - ); - - /** - * @param string $command The command - * @param boolean $throwException If true, an Exception will be thrown on a nonzero error code - * @param boolean $returnOutput If true, output will be captured - * @param boolean $inputContent Content for STDIN. Otherwise the parent script's STDIN is used - * @return A map containing 'return', 'output', and 'error' - */ - public function execLocal($command, $options = array()) { - $process = $this->createLocal($command, $options); - return $process->exec(); - } - - public function execRemote($command, $options = array()) { - $process = $this->createRemote($command, $options); - return $process->exec(); - } - - public function createLocal($command, $options) { - $options = array_merge($this->defaultOptions, $options); - if(is_array($command)) $command = $this->commandArrayToString($command); - - return new Process($command, $options); - } - - public function createRemote($server, $command, $options = array()) { - $process = $this->createLocal($command, $options); - $process->setRemoteServer($server); - return $process; - } - - /** - * Turn an array command in a string, escaping and concatenating each item - * @param array $command Command array. First element is the command and all remaining are the arguments. - * @return string String command - */ - public function commandArrayToString($command) { - $string = escapeshellcmd(array_shift($command)); - foreach($command as $arg) { - $string .= ' ' . escapeshellarg($arg); - } - return $string; - } - -} - - -class Process { - protected $command; - protected $options; - protected $remoteServer = null; - - public function __construct($command, $options = array()) { - $this->command = $command; - $this->options = $options; - } - - public function setRemoteServer($remoteServer) { - $this->remoteServer = $remoteServer; - } - - public function exec($options = array()) { - $options = array_merge($this->options, $options); - - // Modify command for remote execution, if necessary. - if($this->remoteServer) { - if(!empty($options['outputFile']) || !empty($options['outputStream'])) $ssh = "ssh -T "; - else $ssh = "ssh -t "; - if (!empty($options['identity'])) { - $ssh .= '-i ' . escapeshellarg($options['identity']) . ' '; - } - $command = $ssh . escapeshellarg($this->remoteServer) . ' ' . escapeshellarg($this->command); - } else { - $command = $this->command; - } - - $pipes = array(); - $pipeSpec = array( - 0 => STDIN, - 1 => array('pipe', 'w'), - 2 => STDERR, - ); - - // Alternatives - if($options['inputContent'] || $options['inputStream']) $pipeSpec[0] = array('pipe', 'r'); - - if($options['outputFile']) { - $pipeSpec[1] = array('file', - $options['outputFile'], - $options['outputFileAppend'] ? 'a' : 'w'); - } - - $process = proc_open($command, $pipeSpec, $pipes); - - if($options['inputContent']) { - fwrite($pipes[0], $options['inputContent']); - - } else if($options['inputStream']) { - while($content = fread($options['inputStream'], 8192)) { - fwrite($pipes[0], $content); - } - } - if(isset($pipes[0])) fclose($pipes[0]); - - $result = array(); - - if(isset($pipes[1])) { - // If a stream was provided, then pipe all the content - // Doing it this way rather than passing outputStream to $pipeSpec - // Means that streams as well as simple FDs can be used - if($options['outputStream']) { - while($content = fread($pipes[1], 8192)) { - fwrite($options['outputStream'], $content); - } - - // Otherwise save to a string - } else { - $result['output'] = stream_get_contents($pipes[1]); - } - fclose($pipes[1]); - } - if(isset($pipes[2])) { - $result['error'] = stream_get_contents($pipes[2]); - fclose($pipes[2]); - } - - $result['return'] = proc_close($process); - - if($options['throwException'] && $result['return'] != 0) { - throw new Exception("Command: $command\nExecution failed: returned {$result['return']}.\n" - . (empty($result['output']) ? "" : "Output:\n{$result['output']}")); - } - - return $result; - } +class Executor +{ + protected $defaultOptions = array( + 'throwException' => true, + 'inputContent' => null, + 'inputFile' => null, + 'inputStream' => null, + 'outputFile' => null, + 'outputFileAppend' => false, + 'outputStream' => null, + ); + + /** + * @param string $command The command + * @param boolean $throwException If true, an Exception will be thrown on a nonzero error code + * @param boolean $returnOutput If true, output will be captured + * @param boolean $inputContent Content for STDIN. Otherwise the parent script's STDIN is used + * @return A map containing 'return', 'output', and 'error' + */ + public function execLocal($command, $options = array()) + { + $process = $this->createLocal($command, $options); + return $process->exec(); + } + + public function execRemote($command, $options = array()) + { + $process = $this->createRemote($command, $options); + return $process->exec(); + } + + public function createLocal($command, $options) + { + $options = array_merge($this->defaultOptions, $options); + if (is_array($command)) { + $command = $this->commandArrayToString($command); + } + + return new Process($command, $options); + } + + public function createRemote($server, $command, $options = array()) + { + $process = $this->createLocal($command, $options); + $process->setRemoteServer($server); + return $process; + } + + /** + * Turn an array command in a string, escaping and concatenating each item + * @param array $command Command array. First element is the command and all remaining are the arguments. + * @return string String command + */ + public function commandArrayToString($command) + { + $string = escapeshellcmd(array_shift($command)); + foreach ($command as $arg) { + $string .= ' ' . escapeshellarg($arg); + } + return $string; + } } diff --git a/src/FilesystemEntity.php b/src/FilesystemEntity.php index 7880f58..197d75e 100644 --- a/src/FilesystemEntity.php +++ b/src/FilesystemEntity.php @@ -1,136 +1,157 @@ executor = $executor; - - if(strpos($path,':') !== false) { - list($this->server,$this->path) = explode(':', $path, 2); - } else { - $this->server = null; - $this->path = $path; - } - } - - public function isLocal() { - return $this->server == null; - } - public function getPath() { - return $this->path; - } - public function getServer() { - return $this->server; - } - public function setSSHItentityFile($filename) { - $this->identity = $filename; - } - - /** - * Execute a command on the relevant server - * @param string $command Shell command, either a fully escaped string or an array - */ - public function exec($command, $options = array()) { - return $this->createProcess($command, $options)->exec(); - } - - /** - * Create a process for later exection - * @param string $command Shell command, either a fully escaped string or an array - * @return Process - */ - public function createProcess($command, $options = array()) { - if($this->server) { - if ($this->identity && !isset($options['identity'])) { - $options['identity'] = $this->identity; - } - return $this->executor->createRemote($this->server, $command, $options); - } - - return $this->executor->createLocal($command, $options); - } - - /** - * Upload a file to the given destination on the server - * @param string $file The file to upload - * @param string $dest The remote filename/dir to upload to - */ - public function upload($source, $dest) { - if($this->server) { - $this->executor->execLocal(array("scp", $source, "$this->server:$dest")); - } else { - $this->executor->execLocal(array("cp", $source, $dest)); - } - } - - /** - * Create a file with the given content at the given destination on the server - * @param string $content The content of the file - * @param string $dest The remote filename/dir to upload to - */ - public function uploadContent($content, $dest) { - $this->exec("echo " . escapeshellarg($content) . " > " . escapeshellarg($dest)); - } - - /** - * Download a file from the given source on the server to the given file - * @param string $source The remote filename to download - * @param string $dest The local filename/dir to download to - */ - public function download($source, $dest) { - if($this->server) { - $this->executor->execLocal(array("scp", "$this->server:$source", $dest)); - } else { - $this->executor->execLocal(array("cp", $file, $dest)); - } - } - - /** - * Returns true if the given file or directory exists - * @param string $file The file/dir to look for - * @return boolean - */ - public function exists($file = null) { - if(!$file) $file = $this->path; - if($file == '@self') return true; - - if($this->server) { - $result = $this->exec("if [ -e " . escapeshellarg($file) . " ]; then echo yes; fi"); - return (trim($result['output']) == 'yes'); - - } else { - return file_exists($file); - } - } - - /** - * Create the given file with the given content - */ - public function writeFile($file, $content) { - if($this->server) { - $this->exec("echo " . escapeshellarg($content) . " > " . escapeshellarg($file)); - - } else { - file_put_contents($file, $content); - } - } - - /** - * Remove a file or folder from the webroot's server - * - * @param string $file The file to remove - */ - public function unlink($file) { - if(!$file || $file == '/' || $file == '.') throw new Exception("Can't unlink file '$file'"); - $this->exec(array('rm', '-rf', $file)); - return true; - } +class FilesystemEntity +{ + protected $server; + protected $path; + protected $executor; + protected $identity = null; + + public function __construct($path, $executor) + { + $this->executor = $executor; + + if (strpos($path, ':') !== false) { + list($this->server,$this->path) = explode(':', $path, 2); + } else { + $this->server = null; + $this->path = $path; + } + } + + public function isLocal() + { + return $this->server == null; + } + public function getPath() + { + return $this->path; + } + public function getServer() + { + return $this->server; + } + public function setSSHItentityFile($filename) + { + $this->identity = $filename; + } + + /** + * Execute a command on the relevant server + * @param string $command Shell command, either a fully escaped string or an array + */ + public function exec($command, $options = array()) + { + return $this->createProcess($command, $options)->exec(); + } + + /** + * Create a process for later exection + * @param string $command Shell command, either a fully escaped string or an array + * @return Process + */ + public function createProcess($command, $options = array()) + { + if ($this->server) { + if ($this->identity && !isset($options['identity'])) { + $options['identity'] = $this->identity; + } + return $this->executor->createRemote($this->server, $command, $options); + } + + return $this->executor->createLocal($command, $options); + } + + /** + * Upload a file to the given destination on the server + * @param string $file The file to upload + * @param string $dest The remote filename/dir to upload to + */ + public function upload($source, $dest) + { + if ($this->server) { + $this->executor->execLocal(array("scp", $source, "$this->server:$dest")); + } else { + $this->executor->execLocal(array("cp", $source, $dest)); + } + } + + /** + * Create a file with the given content at the given destination on the server + * @param string $content The content of the file + * @param string $dest The remote filename/dir to upload to + */ + public function uploadContent($content, $dest) + { + $this->exec("echo " . escapeshellarg($content) . " > " . escapeshellarg($dest)); + } + + /** + * Download a file from the given source on the server to the given file + * @param string $source The remote filename to download + * @param string $dest The local filename/dir to download to + */ + public function download($source, $dest) + { + if ($this->server) { + $this->executor->execLocal(array("scp", "$this->server:$source", $dest)); + } else { + $this->executor->execLocal(array("cp", $file, $dest)); + } + } + + /** + * Returns true if the given file or directory exists + * @param string $file The file/dir to look for + * @return boolean + */ + public function exists($file = null) + { + if (!$file) { + $file = $this->path; + } + if ($file == '@self') { + return true; + } + + if ($this->server) { + $result = $this->exec("if [ -e " . escapeshellarg($file) . " ]; then echo yes; fi"); + return (trim($result['output']) == 'yes'); + } else { + return file_exists($file); + } + } + + /** + * Create the given file with the given content + */ + public function writeFile($file, $content) + { + if ($this->server) { + $this->exec("echo " . escapeshellarg($content) . " > " . escapeshellarg($file)); + } else { + file_put_contents($file, $content); + } + } + + /** + * Remove a file or folder from the webroot's server + * + * @param string $file The file to remove + */ + public function unlink($file) + { + if (!$file || $file == '/' || $file == '.') { + throw new Exception("Can't unlink file '$file'"); + } + $this->exec(array('rm', '-rf', $file)); + return true; + } } - diff --git a/src/Process.php b/src/Process.php new file mode 100644 index 0000000..f411d71 --- /dev/null +++ b/src/Process.php @@ -0,0 +1,105 @@ +command = $command; + $this->options = $options; + } + + public function setRemoteServer($remoteServer) + { + $this->remoteServer = $remoteServer; + } + + public function exec($options = array()) + { + $options = array_merge($this->options, $options); + + // Modify command for remote execution, if necessary. + if ($this->remoteServer) { + if (!empty($options['outputFile']) || !empty($options['outputStream'])) { + $ssh = "ssh -T "; + } else { + $ssh = "ssh -t "; + } + if (!empty($options['identity'])) { + $ssh .= '-i ' . escapeshellarg($options['identity']) . ' '; + } + $command = $ssh . escapeshellarg($this->remoteServer) . ' ' . escapeshellarg($this->command); + } else { + $command = $this->command; + } + + $pipes = array(); + $pipeSpec = array( + 0 => STDIN, + 1 => array('pipe', 'w'), + 2 => STDERR, + ); + + // Alternatives + if ($options['inputContent'] || $options['inputStream']) { + $pipeSpec[0] = array('pipe', 'r'); + } + + if ($options['outputFile']) { + $pipeSpec[1] = array('file', + $options['outputFile'], + $options['outputFileAppend'] ? 'a' : 'w'); + } + + $process = proc_open($command, $pipeSpec, $pipes); + + if ($options['inputContent']) { + fwrite($pipes[0], $options['inputContent']); + } elseif ($options['inputStream']) { + while ($content = fread($options['inputStream'], 8192)) { + fwrite($pipes[0], $content); + } + } + if (isset($pipes[0])) { + fclose($pipes[0]); + } + + $result = array(); + + if (isset($pipes[1])) { + // If a stream was provided, then pipe all the content + // Doing it this way rather than passing outputStream to $pipeSpec + // Means that streams as well as simple FDs can be used + if ($options['outputStream']) { + while ($content = fread($pipes[1], 8192)) { + fwrite($options['outputStream'], $content); + } + + // Otherwise save to a string + } else { + $result['output'] = stream_get_contents($pipes[1]); + } + fclose($pipes[1]); + } + if (isset($pipes[2])) { + $result['error'] = stream_get_contents($pipes[2]); + fclose($pipes[2]); + } + + $result['return'] = proc_close($process); + + if ($options['throwException'] && $result['return'] != 0) { + throw new Exception("Command: $command\nExecution failed: returned {$result['return']}.\n" + . (empty($result['output']) ? "" : "Output:\n{$result['output']}")); + } + + return $result; + } +} diff --git a/src/SSPak.php b/src/SSPak.php index 8e168cd..9fa3a65 100644 --- a/src/SSPak.php +++ b/src/SSPak.php @@ -1,515 +1,574 @@ executor = $executor; - } - - public function getActions() { - return array( - "help" => array( - "description" => "Show this help message.", - "method" => "help", - ), - "save" => array( - "description" => "Save an .sspak file from a SilverStripe site.", - "unnamedArgs" => array("webroot", "sspak file"), - "namedArgs" => array("identity"), - "method" => "save", - ), - "load" => array( - "description" => "Load an .sspak file into a SilverStripe site. Does not backup - be careful!", - "unnamedArgs" => array("sspak file", "[webroot]"), - "namedArgs" => array("identity"), - "namedFlags" => array("drop-db"), - "method" => "load", - ), - "saveexisting" => array( - "description" => "Create an .sspak file from database SQL dump and/or assets. Does not require a SilverStripe site.", - "unnamedArgs" => array("sspak file"), - "namedArgs" => array("db", "assets"), - "method" => "saveexisting" - ), - "extract" => array( - "description" => "Extract an .sspak file into the current working directory. Does not require a SilverStripe site.", - "unnamedArgs" => array("sspak file", "destination path"), - "method" => "extract" - ), - "listtables" => array( - "description" => "List tables in the database", - "unnamedArgs" => array("webroot"), - "method" => "listTables" - ), - - "savecsv" => array( - "description" => "Save tables in the database to a collection of CSV files", - "unnamedArgs" => array("webroot", "output-path"), - "method" => "saveCsv" - ), - - "loadcsv" => array( - "description" => "Load tables from collection of CSV files to a webroot", - "unnamedArgs" => array("input-path", "webroot"), - "method" => "loadCsv" - ), - /* - - "install" => array( - "description" => "Install a .sspak file into a new environment.", - "unnamedArgs" => array("sspak file", "new webroot"), - "method" => "install", - ), - "bundle" => array( - "description" => "Bundle a .sspak file into a self-extracting executable .sspak.phar installer.", - "unnamedArgs" => array("sspak file", "executable"), - "method" => "bundle", - ), - "transfer" => array( - "description" => "Transfer db & assets from one site to another (not implemented yet).", - "unnamedArgs" => array("src webroot", "dest webroot"), - "method" => "transfer", - ), - */ - ); - } - - public function help($args) { - echo "SSPak: manage SilverStripe .sspak archives.\n\nUsage:\n"; - foreach($this->getActions() as $action => $info) { - echo "sspak $action"; - if(!empty($info['unnamedArgs'])) { - foreach($info['unnamedArgs'] as $arg) echo " ($arg)"; - } - if(!empty($info['namedFlags'])) { - foreach($info['namedFlags'] as $arg) echo " (--$arg)"; - } - if(!empty($info['namedArgs'])) { - foreach($info['namedArgs'] as $arg) echo " --$arg=\"$arg value\""; - } - echo "\n {$info['description']}\n\n"; - } - } - - /** - * Save an existing database and/or assets into an .sspak.phar file. - * Does the same as {@link save()} but doesn't require an existing site. - */ - public function saveexisting($args) { - $executor = $this->executor; - - $args->requireUnnamed(array('sspak file')); - $unnamedArgs = $args->getUnnamedArgs(); - $namedArgs = $args->getNamedArgs(); - - $sspak = new SSPakFile($unnamedArgs[0], $executor); - - // Look up which parts of the sspak are going to be saved - $pakParts = $args->pakParts(); - - $filesystem = new FilesystemEntity(null, $executor); - - if($pakParts['db']) { - $dbPath = escapeshellarg($namedArgs['db']); - $process = $filesystem->createProcess("cat $dbPath | gzip -c"); - $sspak->writeFileFromProcess('database.sql.gz', $process); - } - - if($pakParts['assets']) { - $assetsParentArg = escapeshellarg(dirname($namedArgs['assets'])); - $assetsBaseArg = escapeshellarg(basename($namedArgs['assets'])); - $process = $filesystem->createProcess("cd $assetsParentArg && tar cfh - $assetsBaseArg | gzip -c"); - $sspak->writeFileFromProcess('assets.tar.gz', $process); - } - } - - /** - * Extracts an existing database and/or assets from a sspak into the given directory, - * defaulting the current working directory if the destination is not given. - */ - public function extract($args) { - $executor = $this->executor; - - $args->requireUnnamed(array('source sspak file')); - $unnamedArgs = $args->getUnnamedArgs(); - $file = $unnamedArgs[0]; - $dest = !empty($unnamedArgs[1]) ? $unnamedArgs[1] : getcwd(); - - // Phar and PharData use "ustar" format for tar archives (http://php.net/manual/pl/phar.fileformat.tar.php). - // Ustar does not support files larger than 8 GB. - // If the sspak has been created through tar and gz directly, it will probably be in POSIX, PAX or GNU formats, - // which do support >8 GB files. Such archive cannot be accessed by Phar/PharData, and needs to be handled - // manually - it will just spew checksum errors where PHP expects to see ustar headers, but finds garbage - // from other formats. - // There is no cross-platform way of checking the assets.tar.gz size without unpacking, so we assume the size - // of database is negligible which lets us approximate the size of assets. - if (filesize($file) > 8*1024*1024*1024) { - $msg = <<executor = $executor; + } + + public function getActions() + { + return array( + "help" => array( + "description" => "Show this help message.", + "method" => "help", + ), + "save" => array( + "description" => "Save an .sspak file from a SilverStripe site.", + "unnamedArgs" => array("webroot", "sspak file"), + "namedArgs" => array("identity"), + "method" => "save", + ), + "load" => array( + "description" => "Load an .sspak file into a SilverStripe site. Does not backup - be careful!", + "unnamedArgs" => array("sspak file", "[webroot]"), + "namedArgs" => array("identity"), + "namedFlags" => array("drop-db"), + "method" => "load", + ), + "saveexisting" => array( + "description" => "Create an .sspak file from database SQL dump and/or assets. " . + "Does not require a SilverStripe site.", + "unnamedArgs" => array("sspak file"), + "namedArgs" => array("db", "assets"), + "method" => "saveexisting" + ), + "extract" => array( + "description" => "Extract an .sspak file into the current working directory. Does not require a " . + "SilverStripe site.", + "unnamedArgs" => array("sspak file", "destination path"), + "method" => "extract" + ), + "listtables" => array( + "description" => "List tables in the database", + "unnamedArgs" => array("webroot"), + "method" => "listTables" + ), + + "savecsv" => array( + "description" => "Save tables in the database to a collection of CSV files", + "unnamedArgs" => array("webroot", "output-path"), + "method" => "saveCsv" + ), + + "loadcsv" => array( + "description" => "Load tables from collection of CSV files to a webroot", + "unnamedArgs" => array("input-path", "webroot"), + "method" => "loadCsv" + ), + /* + + "install" => array( + "description" => "Install a .sspak file into a new environment.", + "unnamedArgs" => array("sspak file", "new webroot"), + "method" => "install", + ), + "bundle" => array( + "description" => "Bundle a .sspak file into a self-extracting executable .sspak.phar installer.", + "unnamedArgs" => array("sspak file", "executable"), + "method" => "bundle", + ), + "transfer" => array( + "description" => "Transfer db & assets from one site to another (not implemented yet).", + "unnamedArgs" => array("src webroot", "dest webroot"), + "method" => "transfer", + ), + */ + ); + } + + public function help($args) + { + echo "SSPak: manage SilverStripe .sspak archives.\n\nUsage:\n"; + foreach ($this->getActions() as $action => $info) { + echo "sspak $action"; + if (!empty($info['unnamedArgs'])) { + foreach ($info['unnamedArgs'] as $arg) { + echo " ($arg)"; + } + } + if (!empty($info['namedFlags'])) { + foreach ($info['namedFlags'] as $arg) { + echo " (--$arg)"; + } + } + if (!empty($info['namedArgs'])) { + foreach ($info['namedArgs'] as $arg) { + echo " --$arg=\"$arg value\""; + } + } + echo "\n {$info['description']}\n\n"; + } + } + + /** + * Save an existing database and/or assets into an .sspak.phar file. + * Does the same as {@link save()} but doesn't require an existing site. + */ + public function saveexisting($args) + { + $executor = $this->executor; + + $args->requireUnnamed(array('sspak file')); + $unnamedArgs = $args->getUnnamedArgs(); + $namedArgs = $args->getNamedArgs(); + + $sspak = new SSPakFile($unnamedArgs[0], $executor); + + // Look up which parts of the sspak are going to be saved + $pakParts = $args->pakParts(); + + $filesystem = new FilesystemEntity(null, $executor); + + if ($pakParts['db']) { + $dbPath = escapeshellarg($namedArgs['db']); + $process = $filesystem->createProcess("cat $dbPath | gzip -c"); + $sspak->writeFileFromProcess('database.sql.gz', $process); + } + + if ($pakParts['assets']) { + $assetsParentArg = escapeshellarg(dirname($namedArgs['assets'])); + $assetsBaseArg = escapeshellarg(basename($namedArgs['assets'])); + $process = $filesystem->createProcess("cd $assetsParentArg && tar cfh - $assetsBaseArg | gzip -c"); + $sspak->writeFileFromProcess('assets.tar.gz', $process); + } + } + + /** + * Extracts an existing database and/or assets from a sspak into the given directory, + * defaulting the current working directory if the destination is not given. + */ + public function extract($args) + { + $executor = $this->executor; + + $args->requireUnnamed(array('source sspak file')); + $unnamedArgs = $args->getUnnamedArgs(); + $file = $unnamedArgs[0]; + $dest = !empty($unnamedArgs[1]) ? $unnamedArgs[1] : getcwd(); + + // Phar and PharData use "ustar" format for tar archives (http://php.net/manual/pl/phar.fileformat.tar.php). + // Ustar does not support files larger than 8 GB. + // If the sspak has been created through tar and gz directly, it will probably be in POSIX, PAX or GNU formats, + // which do support >8 GB files. Such archive cannot be accessed by Phar/PharData, and needs to be handled + // manually - it will just spew checksum errors where PHP expects to see ustar headers, but finds garbage + // from other formats. + // There is no cross-platform way of checking the assets.tar.gz size without unpacking, so we assume the size + // of database is negligible which lets us approximate the size of assets. + if (filesize($file) > 8*1024*1024*1024) { + $msg = <<exists()) throw new Exception("File '$file' doesn't exist."); - - $phar = $sspak->getPhar(); - $phar->extractTo($dest); - } - - public function listTables($args) { - $args->requireUnnamed(array('webroot')); - $unnamedArgs = $args->getUnnamedArgs(); - $webroot = $unnamedArgs[0]; - - $db = new DatabaseConnector($webroot); - - print_r($db->getTables()); - } - - public function saveCsv($args) { - $args->requireUnnamed(array('webroot', 'path')); - $unnamedArgs = $args->getUnnamedArgs(); - $webroot = $unnamedArgs[0]; - $destPath = $unnamedArgs[1]; - - if (!file_exists($destPath)) { - mkdir($destPath) || die("Can't create $destPath"); - } - if (!is_dir($destPath)) { - die("$destPath isn't a directory"); - } - - $db = new DatabaseConnector($webroot); - - foreach($db->getTables() as $table) { - $filename = $destPath . '/' . $table . '.csv'; - echo $filename . "...\n"; - touch($filename); - $writer = new CsvTableWriter($filename); - $db->saveTable($table, $writer); - } - echo "Done!"; - } - - public function loadCsv($args) { - $args->requireUnnamed(array('input-path', 'webroot')); - $unnamedArgs = $args->getUnnamedArgs(); - - $srcPath = $unnamedArgs[0]; - $webroot = $unnamedArgs[1]; - - if (!is_dir($srcPath)) { - die("$srcPath isn't a directory"); - } - - $db = new DatabaseConnector($webroot); - - foreach($db->getTables() as $table) { - $filename = $srcPath . '/' . $table . '.csv'; - if(file_exists($filename)) { - echo $filename . "...\n"; - $reader = new CsvTableReader($filename); - $db->loadTable($table, $reader); - } else { - echo "$filename doesn't exist; skipping.\n"; - } - } - echo "Done!"; - } - /** - * Save a .sspak.phar file - */ - public function save($args) { - $executor = $this->executor; - - $args->requireUnnamed(array('source webroot', 'dest sspak file')); - - $unnamedArgs = $args->getUnnamedArgs(); - $namedArgs = $args->getNamedArgs(); - - $webroot = new Webroot($unnamedArgs[0], $executor); - $file = $unnamedArgs[1]; - if(file_exists($file)) throw new Exception( "File '$file' already exists."); - - $sspak = new SSPakFile($file, $executor); - - if(!empty($namedArgs['identity'])) { - // SSH private key - $webroot->setSSHItentityFile($namedArgs['identity']); - } - if(!empty($namedArgs['from-sudo'])) $webroot->setSudo($namedArgs['from-sudo']); - else if(!empty($namedArgs['sudo'])) $webroot->setSudo($namedArgs['sudo']); - - // Look up which parts of the sspak are going to be saved - $pakParts = $args->pakParts(); - - // Get the environment details - $details = $webroot->sniff(); - - // Create a build folder for the sspak file - $buildFolder = sprintf("%s/sspak-%d", sys_get_temp_dir(), rand(100000,999999)); - $webroot->exec(array('mkdir', $buildFolder)); - - $dbFile = "$buildFolder/database.sql.gz"; - $assetsFile = "$buildFolder/assets.tar.gz"; - $gitRemoteFile = "$buildFolder/git-remote"; - - // Files to include in the .sspak.phar file - $fileList = array(); - - // Save DB - if($pakParts['db']) { - // Check the database type - $dbFunction = 'getdb_'.$details['db_type']; - if(!method_exists($this,$dbFunction)) { - throw new Exception("Can't process database type '" . $details['db_type'] . "'"); - } - $this->$dbFunction($webroot, $details, $sspak, basename($dbFile)); - } - - // Save Assets - if($pakParts['assets']) { - $this->getassets($webroot, $details['assets_path'], $sspak, basename($assetsFile)); - } - - // Save git-remote - if($pakParts['git-remote']) { - $this->getgitremote($webroot, $sspak, basename($gitRemoteFile)); - } - - // Remove the build folder - $webroot->unlink($buildFolder); - } - - public function getdb_MySQLPDODatabase($webroot, $conf, $sspak, $filename) { - return $this->getdb_MySQLDatabase($webroot, $conf, $sspak, $filename); - } - - public function getdb_MySQLDatabase($webroot, $conf, $sspak, $filename) { - $usernameArg = escapeshellarg("--user=".$conf['db_username']); - $passwordArg = escapeshellarg("--password=".$conf['db_password']); - $databaseArg = escapeshellarg($conf['db_database']); - - $hostArg = ''; - $portArg = ''; - if (!empty($conf['db_server']) && $conf['db_server'] != 'localhost') { - if (strpos($conf['db_server'], ':')!==false) { - // Handle "server:port" format. - $server = explode(':', $conf['db_server'], 2); - $hostArg = escapeshellarg("--host=".$server[0]); - $portArg = escapeshellarg("--port=".$server[1]); - } else { - $hostArg = escapeshellarg("--host=".$conf['db_server']); - } - } - - $filenameArg = escapeshellarg($filename); - - $process = $webroot->createProcess("mysqldump --no-tablespaces --skip-opt --add-drop-table --extended-insert --create-options --quick --set-charset --default-character-set=utf8 --column-statistics=0 $usernameArg $passwordArg $hostArg $portArg $databaseArg | gzip -c"); - $sspak->writeFileFromProcess($filename, $process); - return true; - } - - public function getdb_PostgreSQLDatabase($webroot, $conf, $sspak, $filename) { - $usernameArg = escapeshellarg("--username=".$conf['db_username']); - $passwordArg = "PGPASSWORD=".escapeshellarg($conf['db_password']); - $databaseArg = escapeshellarg($conf['db_database']); - $hostArg = escapeshellarg("--host=".$conf['db_server']); - $filenameArg = escapeshellarg($filename); - - $process = $webroot->createProcess("$passwordArg pg_dump --clean --no-owner --no-tablespaces $usernameArg $hostArg $databaseArg | gzip -c"); - $sspak->writeFileFromProcess($filename, $process); - return true; - } - - public function getassets($webroot, $assetsPath, $sspak, $filename) { - $assetsParentArg = escapeshellarg(dirname($assetsPath)); - $assetsBaseArg = escapeshellarg(basename($assetsPath)); - - $process = $webroot->createProcess("cd $assetsParentArg && tar cfh - $assetsBaseArg | gzip -c"); - $sspak->writeFileFromProcess($filename, $process); - } - - public function getgitremote($webroot, $sspak, $gitRemoteFile) { - // Only do anything if we're copying from a git checkout - $gitRepo = $webroot->getPath() .'/.git'; - if($webroot->exists($gitRepo)) { - // Identify current branch - $output = $webroot->exec(array('git', '--git-dir='.$gitRepo, 'branch')); - if(preg_match("/\* ([^ \n]*)/", $output['output'], $matches) && strpos("(no branch)", $matches[1])===false) { - // If there is a current branch, use that branch's remove - $currentBranch = trim($matches[1]); - $output = $webroot->exec(array('git', '--git-dir='.$gitRepo, 'config','--get',"branch.$currentBranch.remote")); - $remoteName = trim($output['output']); - if(!$remoteName) $remoteName = 'origin'; - - // Default to origin - } else { - $currentBranch = null; - $remoteName = 'origin'; - } - - // Determine the URL of that remote - $output = $webroot->exec(array('git', '--git-dir='.$gitRepo, 'config','--get',"remote.$remoteName.url")); - $remoteURL = trim($output['output']); - - // Determine the current SHA - $output = $webroot->exec(array('git', '--git-dir='.$gitRepo, 'log','-1','--format=%H')); - $sha = trim($output['output']); - - $content = "remote = $remoteURL\nbranch = $currentBranch\nsha = $sha\n"; - - $sspak->writeFile($gitRemoteFile, $content); - - return true; - } - return false; - } - - /** - * Load an .sspak into an environment. - * Does not backup - be careful! */ - public function load($args) { - $executor = $this->executor; - - $args->requireUnnamed(array('source sspak file')); - - // Set-up - $file = $args->unnamed(0); - $sspak = new SSPakFile($file, $executor); - $webroot = new Webroot(($args->unnamed(1) ?: '.'), $executor); - $webroot->setSudo($args->sudo('to')); - $pakParts = $args->pakParts(); - - $namedArgs = $args->getNamedArgs(); - if(!empty($namedArgs['identity'])) { - // SSH private key - $webroot->setSSHItentityFile($namedArgs['identity']); - } - - // Validation - if(!$sspak->exists()) throw new Exception( "File '$file' doesn't exist."); - - // Push database, if necessary - $namedArgs = $args->getNamedArgs(); - if($pakParts['db'] && $sspak->contains('database.sql.gz')) { - $webroot->putdb($sspak, isset($namedArgs['drop-db'])); - } - - // Push assets, if neccessary - if($pakParts['assets'] && $sspak->contains('assets.tar.gz')) { - $webroot->putassets($sspak); - } - } - - /** - * Install an .sspak into a new environment. - */ - public function install($args) { - $executor = $this->executor; - - $args->requireUnnamed(array('source sspak file', 'dest new webroot')); - - // Set-up - $file = $args->unnamed(0); - $webrootDir = $args->unnamed(1); - $sspak = new SSPakFile($file, $executor); - $webroot = new Webroot($webrootDir, $executor); - $webroot->setSudo($args->sudo('to')); - $pakParts = $args->pakParts(); - - // Validation - if($webroot->exists($webroot->getPath())) throw new Exception( "Webroot '$webrootDir' already exists."); - if(!$sspak->exists()) throw new Exception( "File '$file' doesn't exist."); - - // Create new dir - $webroot->exec(array('mkdir', $webroot->getPath())); - - if($sspak->contains('git-remote')) { - $details = $sspak->gitRemoteDetails(); - $webroot->putgit($details); - } - - // TODO: composer install needed. - - // Push database, if necessary - $namedArgs = $args->getNamedArgs(); - if($pakParts['db'] && $sspak->contains('database.sql.gz')) { - $webroot->putdb($sspak, isset($namedArgs['drop-db'])); - } - - // Push assets, if neccessary - if($pakParts['assets'] && $sspak->contains('assets.tar.gz')) { - $webroot->putassets($sspak); - } - } - - /** - * Bundle a .sspak into a self-extracting executable installer. - */ - public function bundle($args) { - // TODO: throws require_once errors, fix before re-enabling. - - $executor = $this->executor; - - $args->requireUnnamed(array('source sspak file', 'dest executable file')); - - // Set-up - $sourceFile = $args->unnamed(0); - $destFile = $args->unnamed(1); - - $sspakScript = file_get_contents($_SERVER['argv'][0]); - // Broken up to not get detected by our sed command - $sspakScript .= "\n__halt_compiler();\n"."//"." TAR START?>\n"; - - // Mark as self-extracting - $sspakScript = str_replace('$isSelfExtracting = false;', '$isSelfExtracting = true;', $sspakScript); - - // Load the sniffer file - $snifferFile = dirname(__FILE__) . '/sspak-sniffer.php'; - $sspakScript = str_replace("\$snifferFileContent = '';\n", - "\$snifferFileContent = '" - . str_replace(array("\\","'"),array("\\\\", "\\'"), file_get_contents($snifferFile)) . "';\n", $sspakScript); - - file_put_contents($destFile, $sspakScript); - chmod($destFile, 0775); - - $executor->execLocal(array('cat', $sourceFile), array( - 'outputFile' => $destFile, - 'outputFileAppend' => true - )); - } - - /** - * Transfer between environments without creating an sspak file - */ - public function transfer($args) { - echo "Not implemented yet.\n"; - } + printf($msg, $file); + die(1); + } + + $sspak = new SSPakFile($file, $executor); + + // Validation + if (!$sspak->exists()) { + throw new Exception("File '$file' doesn't exist."); + } + + $phar = $sspak->getPhar(); + $phar->extractTo($dest); + } + + public function listTables($args) + { + $args->requireUnnamed(array('webroot')); + $unnamedArgs = $args->getUnnamedArgs(); + $webroot = $unnamedArgs[0]; + + $db = new DatabaseConnector($webroot); + + print_r($db->getTables()); + } + + public function saveCsv($args) + { + $args->requireUnnamed(array('webroot', 'path')); + $unnamedArgs = $args->getUnnamedArgs(); + $webroot = $unnamedArgs[0]; + $destPath = $unnamedArgs[1]; + + if (!file_exists($destPath)) { + mkdir($destPath) || die("Can't create $destPath"); + } + if (!is_dir($destPath)) { + die("$destPath isn't a directory"); + } + + $db = new DatabaseConnector($webroot); + + foreach ($db->getTables() as $table) { + $filename = $destPath . '/' . $table . '.csv'; + echo $filename . "...\n"; + touch($filename); + $writer = new CsvTableWriter($filename); + $db->saveTable($table, $writer); + } + echo "Done!"; + } + + public function loadCsv($args) + { + $args->requireUnnamed(array('input-path', 'webroot')); + $unnamedArgs = $args->getUnnamedArgs(); + + $srcPath = $unnamedArgs[0]; + $webroot = $unnamedArgs[1]; + + if (!is_dir($srcPath)) { + die("$srcPath isn't a directory"); + } + + $db = new DatabaseConnector($webroot); + + foreach ($db->getTables() as $table) { + $filename = $srcPath . '/' . $table . '.csv'; + if (file_exists($filename)) { + echo $filename . "...\n"; + $reader = new CsvTableReader($filename); + $db->loadTable($table, $reader); + } else { + echo "$filename doesn't exist; skipping.\n"; + } + } + echo "Done!"; + } + /** + * Save a .sspak.phar file + */ + public function save($args) + { + $executor = $this->executor; + + $args->requireUnnamed(array('source webroot', 'dest sspak file')); + + $unnamedArgs = $args->getUnnamedArgs(); + $namedArgs = $args->getNamedArgs(); + + $webroot = new Webroot($unnamedArgs[0], $executor); + $file = $unnamedArgs[1]; + if (file_exists($file)) { + throw new Exception("File '$file' already exists."); + } + + $sspak = new SSPakFile($file, $executor); + + if (!empty($namedArgs['identity'])) { + // SSH private key + $webroot->setSSHItentityFile($namedArgs['identity']); + } + if (!empty($namedArgs['from-sudo'])) { + $webroot->setSudo($namedArgs['from-sudo']); + } elseif (!empty($namedArgs['sudo'])) { + $webroot->setSudo($namedArgs['sudo']); + } + + // Look up which parts of the sspak are going to be saved + $pakParts = $args->pakParts(); + + // Get the environment details + $details = $webroot->sniff(); + + // Create a build folder for the sspak file + $buildFolder = sprintf("%s/sspak-%d", sys_get_temp_dir(), rand(100000, 999999)); + $webroot->exec(array('mkdir', $buildFolder)); + + $dbFile = "$buildFolder/database.sql.gz"; + $assetsFile = "$buildFolder/assets.tar.gz"; + $gitRemoteFile = "$buildFolder/git-remote"; + + // Files to include in the .sspak.phar file + $fileList = array(); + + // Save DB + if ($pakParts['db']) { + // Check the database type + $dbFunction = 'getdb_'.$details['db_type']; + if (!method_exists($this, $dbFunction)) { + throw new Exception("Can't process database type '" . $details['db_type'] . "'"); + } + $this->$dbFunction($webroot, $details, $sspak, basename($dbFile)); + } + + // Save Assets + if ($pakParts['assets']) { + $this->getassets($webroot, $details['assets_path'], $sspak, basename($assetsFile)); + } + + // Save git-remote + if ($pakParts['git-remote']) { + $this->getgitremote($webroot, $sspak, basename($gitRemoteFile)); + } + + // Remove the build folder + $webroot->unlink($buildFolder); + } + + public function getdb_MySQLPDODatabase($webroot, $conf, $sspak, $filename) + { + return $this->getdb_MySQLDatabase($webroot, $conf, $sspak, $filename); + } + + public function getdb_MySQLDatabase($webroot, $conf, $sspak, $filename) + { + $usernameArg = escapeshellarg("--user=".$conf['db_username']); + $passwordArg = escapeshellarg("--password=".$conf['db_password']); + $databaseArg = escapeshellarg($conf['db_database']); + + $hostArg = ''; + $portArg = ''; + if (!empty($conf['db_server']) && $conf['db_server'] != 'localhost') { + if (strpos($conf['db_server'], ':')!==false) { + // Handle "server:port" format. + $server = explode(':', $conf['db_server'], 2); + $hostArg = escapeshellarg("--host=".$server[0]); + $portArg = escapeshellarg("--port=".$server[1]); + } else { + $hostArg = escapeshellarg("--host=".$conf['db_server']); + } + } + + $filenameArg = escapeshellarg($filename); + + $process = $webroot->createProcess( + "mysqldump --no-tablespaces --skip-opt --add-drop-table --extended-insert --create-options --quick " . + "--set-charset --default-character-set=utf8 --column-statistics=0 $usernameArg $passwordArg $hostArg " . + "$portArg $databaseArg | gzip -c" + ); + $sspak->writeFileFromProcess($filename, $process); + return true; + } + + public function getdb_PostgreSQLDatabase($webroot, $conf, $sspak, $filename) + { + $usernameArg = escapeshellarg("--username=".$conf['db_username']); + $passwordArg = "PGPASSWORD=".escapeshellarg($conf['db_password']); + $databaseArg = escapeshellarg($conf['db_database']); + $hostArg = escapeshellarg("--host=".$conf['db_server']); + $filenameArg = escapeshellarg($filename); + + $process = $webroot->createProcess( + "$passwordArg pg_dump --clean --no-owner --no-tablespaces $usernameArg $hostArg $databaseArg | gzip -c" + ); + $sspak->writeFileFromProcess($filename, $process); + return true; + } + + public function getassets($webroot, $assetsPath, $sspak, $filename) + { + $assetsParentArg = escapeshellarg(dirname($assetsPath)); + $assetsBaseArg = escapeshellarg(basename($assetsPath)); + + $process = $webroot->createProcess("cd $assetsParentArg && tar cfh - $assetsBaseArg | gzip -c"); + $sspak->writeFileFromProcess($filename, $process); + } + + public function getgitremote($webroot, $sspak, $gitRemoteFile) + { + // Only do anything if we're copying from a git checkout + $gitRepo = $webroot->getPath() .'/.git'; + if ($webroot->exists($gitRepo)) { + // Identify current branch + $output = $webroot->exec(array('git', '--git-dir='.$gitRepo, 'branch')); + if (preg_match("/\* ([^ \n]*)/", $output['output'], $matches) && + strpos("(no branch)", $matches[1])===false + ) { + // If there is a current branch, use that branch's remove + $currentBranch = trim($matches[1]); + $output = $webroot->exec( + array('git', '--git-dir='.$gitRepo, 'config','--get',"branch.$currentBranch.remote") + ); + $remoteName = trim($output['output']); + if (!$remoteName) { + $remoteName = 'origin'; + } + + // Default to origin + } else { + $currentBranch = null; + $remoteName = 'origin'; + } + + // Determine the URL of that remote + $output = $webroot->exec(array('git', '--git-dir='.$gitRepo, 'config','--get',"remote.$remoteName.url")); + $remoteURL = trim($output['output']); + + // Determine the current SHA + $output = $webroot->exec(array('git', '--git-dir='.$gitRepo, 'log','-1','--format=%H')); + $sha = trim($output['output']); + + $content = "remote = $remoteURL\nbranch = $currentBranch\nsha = $sha\n"; + + $sspak->writeFile($gitRemoteFile, $content); + + return true; + } + return false; + } + + /** + * Load an .sspak into an environment. + * Does not backup - be careful! */ + public function load($args) + { + $executor = $this->executor; + + $args->requireUnnamed(array('source sspak file')); + + // Set-up + $file = $args->unnamed(0); + $sspak = new SSPakFile($file, $executor); + $webroot = new Webroot(($args->unnamed(1) ?: '.'), $executor); + $webroot->setSudo($args->sudo('to')); + $pakParts = $args->pakParts(); + + $namedArgs = $args->getNamedArgs(); + if (!empty($namedArgs['identity'])) { + // SSH private key + $webroot->setSSHItentityFile($namedArgs['identity']); + } + + // Validation + if (!$sspak->exists()) { + throw new Exception("File '$file' doesn't exist."); + } + + // Push database, if necessary + $namedArgs = $args->getNamedArgs(); + if ($pakParts['db'] && $sspak->contains('database.sql.gz')) { + $webroot->putdb($sspak, isset($namedArgs['drop-db'])); + } + + // Push assets, if neccessary + if ($pakParts['assets'] && $sspak->contains('assets.tar.gz')) { + $webroot->putassets($sspak); + } + } + + /** + * Install an .sspak into a new environment. + */ + public function install($args) + { + $executor = $this->executor; + + $args->requireUnnamed(array('source sspak file', 'dest new webroot')); + + // Set-up + $file = $args->unnamed(0); + $webrootDir = $args->unnamed(1); + $sspak = new SSPakFile($file, $executor); + $webroot = new Webroot($webrootDir, $executor); + $webroot->setSudo($args->sudo('to')); + $pakParts = $args->pakParts(); + + // Validation + if ($webroot->exists($webroot->getPath())) { + throw new Exception("Webroot '$webrootDir' already exists."); + } + if (!$sspak->exists()) { + throw new Exception("File '$file' doesn't exist."); + } + + // Create new dir + $webroot->exec(array('mkdir', $webroot->getPath())); + + if ($sspak->contains('git-remote')) { + $details = $sspak->gitRemoteDetails(); + $webroot->putgit($details); + } + + // TODO: composer install needed. + + // Push database, if necessary + $namedArgs = $args->getNamedArgs(); + if ($pakParts['db'] && $sspak->contains('database.sql.gz')) { + $webroot->putdb($sspak, isset($namedArgs['drop-db'])); + } + + // Push assets, if neccessary + if ($pakParts['assets'] && $sspak->contains('assets.tar.gz')) { + $webroot->putassets($sspak); + } + } + + /** + * Bundle a .sspak into a self-extracting executable installer. + */ + public function bundle($args) + { + // TODO: throws require_once errors, fix before re-enabling. + + $executor = $this->executor; + + $args->requireUnnamed(array('source sspak file', 'dest executable file')); + + // Set-up + $sourceFile = $args->unnamed(0); + $destFile = $args->unnamed(1); + + $sspakScript = file_get_contents($_SERVER['argv'][0]); + // Broken up to not get detected by our sed command + $sspakScript .= "\n__halt_compiler();\n"."//"." TAR START?>\n"; + + // Mark as self-extracting + $sspakScript = str_replace('$isSelfExtracting = false;', '$isSelfExtracting = true;', $sspakScript); + + // Load the sniffer file + $snifferFile = dirname(__FILE__) . '/sspak-sniffer.php'; + $sspakScript = str_replace( + "\$snifferFileContent = '';\n", + "\$snifferFileContent = '" + . str_replace(array("\\","'"), array("\\\\", "\\'"), file_get_contents($snifferFile)) . "';\n", + $sspakScript + ); + + file_put_contents($destFile, $sspakScript); + chmod($destFile, 0775); + + $executor->execLocal(array('cat', $sourceFile), array( + 'outputFile' => $destFile, + 'outputFileAppend' => true + )); + } + + /** + * Transfer between environments without creating an sspak file + */ + public function transfer($args) + { + echo "Not implemented yet.\n"; + } } diff --git a/src/SSPakFile.php b/src/SSPakFile.php index 9c222d1..3d3c0b9 100644 --- a/src/SSPakFile.php +++ b/src/SSPakFile.php @@ -1,68 +1,95 @@ isLocal()) throw new LogicException("Can't manipulate remote .sspak.phar files, only remote webroots."); - - $this->pharAlias = $pharAlias; - $this->pharPath = $path; - - // Executable Phar version - if(substr($path,-5) === '.phar') { - $this->phar = new Phar($path, FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::KEY_AS_FILENAME, - $this->pharAlias); - if(!file_exists($this->path)) $this->makeExecutable(); - - // Non-executable Tar version - } else { - $this->phar = new PharData($path, FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::KEY_AS_FILENAME, - $this->pharAlias); - } - } - - public function getPhar() { - return $this->phar; - } - - /** - * Add the SSPak executable information into this SSPak file - */ - public function makeExecutable() { - if(ini_get('phar.readonly')) { - throw new Exception("Please set phar.readonly to false in your php.ini."); - } - - passthru("composer install -d " . escapeshellarg(PACKAGE_ROOT) . " --no-dev"); - - $root = PACKAGE_ROOT; - $srcRoots = [ - 'src/', - 'vendor/', - ]; - - // Add the bin file, but strip of the #! exec header. - $this->phar['bin/sspak'] = preg_replace("/^#!\/usr\/bin\/env php\n/", '', file_get_contents($root . "bin/sspak")); - - foreach($srcRoots as $srcRoot) { - foreach(new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root . $srcRoot)) as $fileObj) { - if($fileObj->isFile()) { - $file = $fileObj->getRealPath(); - - $relativeFile = str_replace($root, '', $file); - - echo "Adding $relativeFile\n"; - $this->phar[$relativeFile] = file_get_contents($file); - } - } - } - - $stub = <<isLocal()) { + throw new LogicException("Can't manipulate remote .sspak.phar files, only remote webroots."); + } + + $this->pharAlias = $pharAlias; + $this->pharPath = $path; + + // Executable Phar version + if (substr($path, -5) === '.phar') { + $this->phar = new Phar( + $path, + FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::KEY_AS_FILENAME, + $this->pharAlias + ); + if (!file_exists($this->path)) { + $this->makeExecutable(); + } + + // Non-executable Tar version + } else { + $this->phar = new PharData( + $path, + FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::KEY_AS_FILENAME, + $this->pharAlias + ); + } + } + + public function getPhar() + { + return $this->phar; + } + + /** + * Add the SSPak executable information into this SSPak file + */ + public function makeExecutable() + { + if (ini_get('phar.readonly')) { + throw new Exception("Please set phar.readonly to false in your php.ini."); + } + + passthru("composer install -d " . escapeshellarg(PACKAGE_ROOT) . " --no-dev"); + + $root = PACKAGE_ROOT; + $srcRoots = [ + 'src/', + 'vendor/', + ]; + + // Add the bin file, but strip of the #! exec header. + $this->phar['bin/sspak'] = preg_replace( + "/^#!\/usr\/bin\/env php\n/", + '', + file_get_contents($root . "bin/sspak") + ); + + foreach ($srcRoots as $srcRoot) { + foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root . $srcRoot)) as $fileObj) { + if ($fileObj->isFile()) { + $file = $fileObj->getRealPath(); + + $relativeFile = str_replace($root, '', $file); + + echo "Adding $relativeFile\n"; + $this->phar[$relativeFile] = file_get_contents($file); + } + } + } + + $stub = <<pharAlias/'); @@ -71,97 +98,106 @@ public function makeExecutable() { __HALT_COMPILER(); STUB; - $this->phar->setStub($stub); - chmod($this->path, 0775); - - passthru("composer install -d " . escapeshellarg(PACKAGE_ROOT)); - } - - /** - * Returns true if this sspak file contains the given file. - * @param string $file The filename to look for - * @return boolean - */ - public function contains($file) { - return $this->phar->offsetExists($file); - } - - /** - * Returns the content of a file from this sspak - */ - public function content($file) { - return file_get_contents($this->phar[$file]); - } - - /** - * Pipe the output of the given process into a file within this SSPak - * @param string $filename The file to create within the SSPak - * @param Process $process The process to execute and take the output from - * @return null - */ - public function writeFileFromProcess($filename, Process $process) { - // Non-executable Phars can't have content streamed into them - // This means that we need to create a temp file, which is a pain, if that file happens to be a 3GB - // asset dump. :-/ - if($this->phar instanceof PharData) { - $tmpFile = '/tmp/sspak-content-' .rand(100000,999999); - $process->exec(array('outputFile' => $tmpFile)); - $this->phar->addFile($tmpFile, $filename); - unlink($tmpFile); - - // So, where we *can* use write streams, we do so. - } else { - $stream = $this->writeStreamForFile($filename); - $process->exec(array('outputStream' => $stream)); - fclose($stream); - } - } - - /** - * Return a writeable stream corresponding to the given file within the .sspak - * @param string $filename The name of the file within the .sspak - * @return Stream context - */ - public function writeStreamForFile($filename) { - return fopen('phar://' . $this->pharAlias . '/' . $filename, 'w'); - } - - /** - * Return a readable stream corresponding to the given file within the .sspak - * @param string $filename The name of the file within the .sspak - * @return Stream context - */ - public function readStreamForFile($filename) { - // Note: using pharAlias here doesn't work on Debian Wheezy (nor on Windows for that matter). - //return fopen('phar://' . $this->pharAlias . '/' . $filename, 'r'); - return fopen('phar://' . $this->pharPath . '/' . $filename, 'r'); - } - - /** - * Create a file in the .sspak with the given content - * @param string $filename The name of the file within the .sspak - * @param string $content The content of the file - * @return null - */ - public function writeFile($filename, $content) { - $this->phar[$filename] = $content; - } - - /** - * Extracts the git remote details and reutrns them as a map - */ - public function gitRemoteDetails() { - $content = $this->content('git-remote'); - $details = array(); - foreach(explode("\n", trim($content)) as $line) { - if(!$line) continue; - - if(preg_match('/^([^ ]+) *= *(.*)$/', $line, $matches)) { - $details[$matches[1]] = $matches[2]; - } else { - throw new Exception("Bad line '$line'"); - } - } - return $details; - } + $this->phar->setStub($stub); + chmod($this->path, 0775); + + passthru("composer install -d " . escapeshellarg(PACKAGE_ROOT)); + } + + /** + * Returns true if this sspak file contains the given file. + * @param string $file The filename to look for + * @return boolean + */ + public function contains($file) + { + return $this->phar->offsetExists($file); + } + + /** + * Returns the content of a file from this sspak + */ + public function content($file) + { + return file_get_contents($this->phar[$file]); + } + + /** + * Pipe the output of the given process into a file within this SSPak + * @param string $filename The file to create within the SSPak + * @param Process $process The process to execute and take the output from + * @return null + */ + public function writeFileFromProcess($filename, Process $process) + { + // Non-executable Phars can't have content streamed into them + // This means that we need to create a temp file, which is a pain, if that file happens to be a 3GB + // asset dump. :-/ + if ($this->phar instanceof PharData) { + $tmpFile = '/tmp/sspak-content-' .rand(100000, 999999); + $process->exec(array('outputFile' => $tmpFile)); + $this->phar->addFile($tmpFile, $filename); + unlink($tmpFile); + + // So, where we *can* use write streams, we do so. + } else { + $stream = $this->writeStreamForFile($filename); + $process->exec(array('outputStream' => $stream)); + fclose($stream); + } + } + + /** + * Return a writeable stream corresponding to the given file within the .sspak + * @param string $filename The name of the file within the .sspak + * @return Stream context + */ + public function writeStreamForFile($filename) + { + return fopen('phar://' . $this->pharAlias . '/' . $filename, 'w'); + } + + /** + * Return a readable stream corresponding to the given file within the .sspak + * @param string $filename The name of the file within the .sspak + * @return Stream context + */ + public function readStreamForFile($filename) + { + // Note: using pharAlias here doesn't work on Debian Wheezy (nor on Windows for that matter). + //return fopen('phar://' . $this->pharAlias . '/' . $filename, 'r'); + return fopen('phar://' . $this->pharPath . '/' . $filename, 'r'); + } + + /** + * Create a file in the .sspak with the given content + * @param string $filename The name of the file within the .sspak + * @param string $content The content of the file + * @return null + */ + public function writeFile($filename, $content) + { + $this->phar[$filename] = $content; + } + + /** + * Extracts the git remote details and reutrns them as a map + */ + public function gitRemoteDetails() + { + $content = $this->content('git-remote'); + $details = array(); + foreach (explode("\n", trim($content)) as $line) { + if (!$line) { + continue; + } + + if (preg_match('/^([^ ]+) *= *(.*)$/', $line, $matches)) { + $details[$matches[1]] = $matches[2]; + } else { + throw new Exception("Bad line '$line'"); + } + } + return $details; + } } diff --git a/src/Webroot.php b/src/Webroot.php index cdf2640..14b8ed2 100644 --- a/src/Webroot.php +++ b/src/Webroot.php @@ -1,185 +1,217 @@ sudo = $sudo; - } - - /** - * Return a map of the db & asset config details. - * Calls sniff once and then caches - */ - public function details() { - if(!$this->details) $this->details = $this->sniff(); - return $this->details; - } - - /** - * Return a map of the db & asset config details, acquired with ssnap-sniffer - */ - public function sniff() { - global $snifferFileContent; - - if(!$snifferFileContent) $snifferFileContent = file_get_contents(PACKAGE_ROOT . 'src/sspak-sniffer.php'); - - $remoteSniffer = '/tmp/sspak-sniffer-' . rand(100000,999999) . '.php'; - $this->uploadContent($snifferFileContent, $remoteSniffer); - - $result = $this->execSudo(array('/usr/bin/env', 'php', $remoteSniffer, $this->path)); - $this->unlink($remoteSniffer); - - $parsed = @unserialize($result['output']); - if(!$parsed) throw new Exception("Could not parse sspak-sniffer content:\n{$result['output']}\n"); - return $parsed; - } - - /** - * Execute a command on the relevant server, using the given sudo option - * @param string|array $command Shell command, either a fully escaped string or an array - * @see Process::exec @param $options (optional) Extra options - * @return array A map containing 'return', 'output', and 'error' - */ - public function execSudo($command, $options = array()) { - if($this->sudo) { - if(is_array($command)) $command = $this->executor->commandArrayToString($command); - // Try running sudo without asking for a password - try { - return $this->exec("sudo -n -u " . escapeshellarg($this->sudo) . " " . $command, $options); - - // Otherwise capture SUDO password ourselves and pass it in through STDIN - } catch(Exception $e) { - echo "[sspak sudo] Enter your password: "; - $stdin = fopen( 'php://stdin', 'r'); - $password = fgets($stdin); - - return $this->exec("sudo -S -p '' -u " . escapeshellarg($this->sudo) . " " . $command, array('inputContent' => $password)); - } - - } else { - return $this->exec($command, $options); - } - } - - /** - * Put the database from the given sspak file into this webroot. - * @param array $details The previously sniffed details of this webroot - * @param bool $dropdb Drop the DB prior to install - * @param string $sspakFile Filename - */ - public function putdb($sspak, $dropdb) { - $details = $this->details(); - - // Check the database type - $dbFunction = 'putdb_'.$details['db_type']; - if(!method_exists($this,$dbFunction)) { - throw new Exception("Can't process database type '" . $details['db_type'] . "'"); - } - - // Extract DB direct from sspak file - return $this->$dbFunction($details, $sspak, $dropdb); - } - - public function putdb_MySQLPDODatabase($conf, $sspak, $dropdb) { - return $this->putdb_MySQLDatabase($conf, $sspak, $dropdb); - } - - public function putdb_MySQLDatabase($conf, $sspak, $dropdb) { - $usernameArg = escapeshellarg("--user=".$conf['db_username']); - $passwordArg = escapeshellarg("--password=".$conf['db_password']); - $databaseArg = escapeshellarg($conf['db_database']); - - $hostArg = ''; - $portArg = ''; - if (!empty($conf['db_server']) && $conf['db_server'] != 'localhost') { - if (strpos($conf['db_server'], ':')!==false) { - // Handle "server:port" format. - $server = explode(':', $conf['db_server'], 2); - $hostArg = escapeshellarg("--host=".$server[0]); - $portArg = escapeshellarg("--port=".$server[1]); - } else { - $hostArg = escapeshellarg("--host=".$conf['db_server']); - } - } - $dbCommand = "create database if not exists `" . addslashes($conf['db_database']) . "`"; - if($dropdb) { - $dbCommand = "drop database if exists `" . addslashes($conf['db_database']) . "`; " . $dbCommand; - } - - $this->exec("echo '$dbCommand' | mysql $usernameArg $passwordArg $hostArg $portArg"); - - $stream = $sspak->readStreamForFile('database.sql.gz'); - $this->exec("gunzip -c | sed '/^CREATE DATABASE/d;/^USE/d' | mysql --default-character-set=utf8 $usernameArg $passwordArg $hostArg $portArg $databaseArg", array('inputStream' => $stream)); - fclose($stream); - return true; - } - - public function putdb_PostgreSQLDatabase($conf, $sspak, $dropdb) { - // TODO: Support dropdb for postgresql - $usernameArg = escapeshellarg("--username=".$conf['db_username']); - $passwordArg = "PGPASSWORD=".escapeshellarg($conf['db_password']); - $databaseArg = escapeshellarg($conf['db_database']); - $hostArg = escapeshellarg("--host=".$conf['db_server']); - - // Create database if needed - $result = $this->exec("echo \"select count(*) from pg_catalog.pg_database where datname = $databaseArg\" | $passwordArg psql $usernameArg $hostArg $databaseArg -qt"); - if(trim($result['output']) == '0') { - $this->exec("$passwordArg createdb $usernameArg $hostArg $databaseArg"); - } - - $stream = $sspak->readStreamForFile('database.sql.gz'); - return $this->exec("gunzip -c | $passwordArg psql $usernameArg $hostArg $databaseArg", array('inputStream' => $stream)); - fclose($stream); - } - - /** - * @param $sspak SSPakFile SSPak file to extract assets from - * @todo There should be a return value or exception thrown to indicate success or failure to put assets - */ - public function putassets($sspak) { - $details = $this->details(); - $assetsPath = $details['assets_path']; - $assetsPath = escapeshellarg($assetsPath); - - // Check for symlink - this was more reliable than is_link - $assetsPathExec = $this->exec("if [ -L {$assetsPath} ]; then readlink -f {$assetsPath}; else echo {$assetsPath}; fi"); - $assetsPath = trim($assetsPathExec["output"]); - - $assetsOldPath = $assetsPath . '.old'; - $assetsParentArg = escapeshellarg(dirname($assetsPath)); - - // Move existing assets to assets.old - $assetsExist = $this->execSudo("test -d '$assetsPath'", ['throwException' => false]); - if ($assetsExist['return'] == 0) { - $this->execSudo("mv {$assetsPath} {$assetsOldPath}"); - } - - // Extract assets - $stream = $sspak->readStreamForFile('assets.tar.gz'); - $this->execSudo("tar xzf - -C {$assetsParentArg}", array('inputStream' => $stream)); - fclose($stream); - - // Remove assets.old - $oldAssetsExist = $this->execSudo("test -d '$assetsOldPath'", ['throwException' => false]); - if ($oldAssetsExist['return'] == 0) { - $this->execSudo("rm -rf {$assetsOldPath}"); - } - } - - /** - * Load a git remote into this webroot. - * It expects that this remote is an empty directory. - * - * @param array $details Map of git details - */ - public function putgit($details) { - $this->exec(array('git', 'clone', $details['remote'], $this->path)); - $this->exec("cd $this->path && git checkout " . escapeshellarg($details['branch'])); - return true; - } +class Webroot extends FilesystemEntity +{ + protected $sudo = null; + protected $details = null; + + public function setSudo($sudo) + { + $this->sudo = $sudo; + } + + /** + * Return a map of the db & asset config details. + * Calls sniff once and then caches + */ + public function details() + { + if (!$this->details) { + $this->details = $this->sniff(); + } + return $this->details; + } + + /** + * Return a map of the db & asset config details, acquired with ssnap-sniffer + */ + public function sniff() + { + global $snifferFileContent; + + if (!$snifferFileContent) { + $snifferFileContent = file_get_contents(PACKAGE_ROOT . 'src/sspak-sniffer.php'); + } + + $remoteSniffer = '/tmp/sspak-sniffer-' . rand(100000, 999999) . '.php'; + $this->uploadContent($snifferFileContent, $remoteSniffer); + + $result = $this->execSudo(array('/usr/bin/env', 'php', $remoteSniffer, $this->path)); + $this->unlink($remoteSniffer); + + $parsed = @unserialize($result['output']); + if (!$parsed) { + throw new Exception("Could not parse sspak-sniffer content:\n{$result['output']}\n"); + } + return $parsed; + } + + /** + * Execute a command on the relevant server, using the given sudo option + * @param string|array $command Shell command, either a fully escaped string or an array + * @see Process::exec @param $options (optional) Extra options + * @return array A map containing 'return', 'output', and 'error' + */ + public function execSudo($command, $options = array()) + { + if ($this->sudo) { + if (is_array($command)) { + $command = $this->executor->commandArrayToString($command); + } + // Try running sudo without asking for a password + try { + return $this->exec("sudo -n -u " . escapeshellarg($this->sudo) . " " . $command, $options); + + // Otherwise capture SUDO password ourselves and pass it in through STDIN + } catch (Exception $e) { + echo "[sspak sudo] Enter your password: "; + $stdin = fopen('php://stdin', 'r'); + $password = fgets($stdin); + + return $this->exec( + "sudo -S -p '' -u " . escapeshellarg($this->sudo) . " " . $command, + array('inputContent' => $password) + ); + } + } else { + return $this->exec($command, $options); + } + } + + /** + * Put the database from the given sspak file into this webroot. + * @param array $details The previously sniffed details of this webroot + * @param bool $dropdb Drop the DB prior to install + * @param string $sspakFile Filename + */ + public function putdb($sspak, $dropdb) + { + $details = $this->details(); + + // Check the database type + $dbFunction = 'putdb_'.$details['db_type']; + if (!method_exists($this, $dbFunction)) { + throw new Exception("Can't process database type '" . $details['db_type'] . "'"); + } + + // Extract DB direct from sspak file + return $this->$dbFunction($details, $sspak, $dropdb); + } + + public function putdb_MySQLPDODatabase($conf, $sspak, $dropdb) + { + return $this->putdb_MySQLDatabase($conf, $sspak, $dropdb); + } + + public function putdb_MySQLDatabase($conf, $sspak, $dropdb) + { + $usernameArg = escapeshellarg("--user=".$conf['db_username']); + $passwordArg = escapeshellarg("--password=".$conf['db_password']); + $databaseArg = escapeshellarg($conf['db_database']); + + $hostArg = ''; + $portArg = ''; + if (!empty($conf['db_server']) && $conf['db_server'] != 'localhost') { + if (strpos($conf['db_server'], ':')!==false) { + // Handle "server:port" format. + $server = explode(':', $conf['db_server'], 2); + $hostArg = escapeshellarg("--host=".$server[0]); + $portArg = escapeshellarg("--port=".$server[1]); + } else { + $hostArg = escapeshellarg("--host=".$conf['db_server']); + } + } + $dbCommand = "create database if not exists `" . addslashes($conf['db_database']) . "`"; + if ($dropdb) { + $dbCommand = "drop database if exists `" . addslashes($conf['db_database']) . "`; " . $dbCommand; + } + + $this->exec("echo '$dbCommand' | mysql $usernameArg $passwordArg $hostArg $portArg"); + + $stream = $sspak->readStreamForFile('database.sql.gz'); + $this->exec("gunzip -c | sed '/^CREATE DATABASE/d;/^USE/d' | mysql --default-character-set=utf8 " . + "$usernameArg $passwordArg $hostArg $portArg $databaseArg", array('inputStream' => $stream)); + fclose($stream); + return true; + } + + public function putdb_PostgreSQLDatabase($conf, $sspak, $dropdb) + { + // TODO: Support dropdb for postgresql + $usernameArg = escapeshellarg("--username=".$conf['db_username']); + $passwordArg = "PGPASSWORD=".escapeshellarg($conf['db_password']); + $databaseArg = escapeshellarg($conf['db_database']); + $hostArg = escapeshellarg("--host=".$conf['db_server']); + + // Create database if needed + $result = $this->exec("echo \"select count(*) from pg_catalog.pg_database where datname = $databaseArg\" | " . + "$passwordArg psql $usernameArg $hostArg $databaseArg -qt"); + if (trim($result['output']) == '0') { + $this->exec("$passwordArg createdb $usernameArg $hostArg $databaseArg"); + } + + $stream = $sspak->readStreamForFile('database.sql.gz'); + return $this->exec( + "gunzip -c | $passwordArg psql $usernameArg $hostArg $databaseArg", + array('inputStream' => $stream) + ); + fclose($stream); + } + + /** + * @param $sspak SSPakFile SSPak file to extract assets from + * @todo There should be a return value or exception thrown to indicate success or failure to put assets + */ + public function putassets($sspak) + { + $details = $this->details(); + $assetsPath = $details['assets_path']; + $assetsPath = escapeshellarg($assetsPath); + + // Check for symlink - this was more reliable than is_link + $assetsPathExec = $this->exec( + "if [ -L {$assetsPath} ]; then readlink -f {$assetsPath}; else echo {$assetsPath}; fi" + ); + $assetsPath = trim($assetsPathExec["output"]); + + $assetsOldPath = $assetsPath . '.old'; + $assetsParentArg = escapeshellarg(dirname($assetsPath)); + + // Move existing assets to assets.old + $assetsExist = $this->execSudo("test -d '$assetsPath'", ['throwException' => false]); + if ($assetsExist['return'] == 0) { + $this->execSudo("mv {$assetsPath} {$assetsOldPath}"); + } + + // Extract assets + $stream = $sspak->readStreamForFile('assets.tar.gz'); + $this->execSudo("tar xzf - -C {$assetsParentArg}", array('inputStream' => $stream)); + fclose($stream); + + // Remove assets.old + $oldAssetsExist = $this->execSudo("test -d '$assetsOldPath'", ['throwException' => false]); + if ($oldAssetsExist['return'] == 0) { + $this->execSudo("rm -rf {$assetsOldPath}"); + } + } + + /** + * Load a git remote into this webroot. + * It expects that this remote is an empty directory. + * + * @param array $details Map of git details + */ + public function putgit($details) + { + $this->exec(array('git', 'clone', $details['remote'], $this->path)); + $this->exec("cd $this->path && git checkout " . escapeshellarg($details['branch'])); + return true; + } } diff --git a/src/sspak-sniffer.php b/src/sspak-sniffer.php index 013449d..d1a144e 100644 --- a/src/sspak-sniffer.php +++ b/src/sspak-sniffer.php @@ -7,47 +7,49 @@ */ // Argument parsing -if(empty($_SERVER['argv'][1])) { - echo "Usage: {$_SERVER['argv'][0]} (site-docroot)\n"; - exit(1); +if (empty($_SERVER['argv'][1])) { + echo "Usage: {$_SERVER['argv'][0]} (site-docroot)\n"; + exit(1); } $basePath = $_SERVER['argv'][1]; -if($basePath[0] != '/') $basePath = getcwd() . '/' . $basePath; +if ($basePath[0] != '/') { + $basePath = getcwd() . '/' . $basePath; +} // SilverStripe bootstrap define('BASE_PATH', realpath($basePath)); if (!defined('BASE_URL')) { - define('BASE_URL', '/'); + define('BASE_URL', '/'); } $_SERVER['HTTP_HOST'] = 'localhost'; chdir(BASE_PATH); -if(file_exists(BASE_PATH.'/sapphire/core/Core.php')) { - //SS 2.x - require_once(BASE_PATH . '/sapphire/core/Core.php'); -} else if(file_exists(BASE_PATH.'/framework/core/Core.php')) { - //SS 3.x - require_once(BASE_PATH. '/framework/core/Core.php'); -} else if(file_exists(BASE_PATH.'/vendor/silverstripe/framework')) { - //SS 4.x - require_once(BASE_PATH. '/vendor/autoload.php'); - $kernel = new SilverStripe\Core\CoreKernel(BASE_PATH); - //boot the parts of the kernel to populate the DB config - foreach (array('bootDatabaseEnvVars', 'bootDatabaseGlobals') as $bootMethod) { - $reflectedBootMethod = new ReflectionMethod($kernel, $bootMethod); - $reflectedBootMethod->setAccessible(true); - $reflectedBootMethod->invoke($kernel); - } - $databaseConfig = SilverStripe\ORM\DB::getConfig(); +if (file_exists(BASE_PATH.'/sapphire/core/Core.php')) { + //SS 2.x + require_once(BASE_PATH . '/sapphire/core/Core.php'); +} elseif (file_exists(BASE_PATH.'/framework/core/Core.php')) { + //SS 3.x + require_once(BASE_PATH. '/framework/core/Core.php'); +} elseif (file_exists(BASE_PATH.'/vendor/silverstripe/framework')) { + //SS 4.x + require_once(BASE_PATH. '/vendor/autoload.php'); + $kernel = new SilverStripe\Core\CoreKernel(BASE_PATH); + //boot the parts of the kernel to populate the DB config + foreach (array('bootDatabaseEnvVars', 'bootDatabaseGlobals') as $bootMethod) { + $reflectedBootMethod = new ReflectionMethod($kernel, $bootMethod); + $reflectedBootMethod->setAccessible(true); + $reflectedBootMethod->invoke($kernel); + } + $databaseConfig = SilverStripe\ORM\DB::getConfig(); } else { - echo "Couldn't locate framework's Core.php. Perhaps " . BASE_PATH . " is not a SilverStripe project?\n"; - exit(2); + echo "Couldn't locate framework's Core.php. Perhaps " . BASE_PATH . " is not a SilverStripe project?\n"; + exit(2); } $output = array(); -foreach($databaseConfig as $k => $v) { - $output['db_' . $k] = $v; +foreach ($databaseConfig as $k => $v) { + $output['db_' . $k] = $v; } $output['assets_path'] = ASSETS_PATH; diff --git a/tests/DataExtractor/CsvTableReaderTest.php b/tests/DataExtractor/CsvTableReaderTest.php index a2fb68d..bfc6e14 100644 --- a/tests/DataExtractor/CsvTableReaderTest.php +++ b/tests/DataExtractor/CsvTableReaderTest.php @@ -1,26 +1,29 @@ assertEquals(['Col1', 'Col2', 'Col3'], $csv->getColumns()); +class CsvTableReaderTest extends TestCase +{ + public function testCsvReading() + { - $extractedData = []; - foreach($csv as $record) { - $extractedData[] = $record; - } + $csv = new CsvTableReader(__DIR__ . '/fixture/input.csv'); + $this->assertEquals(['Col1', 'Col2', 'Col3'], $csv->getColumns()); - $this->assertEquals( - [ - [ 'Col1' => 'One', 'Col2' => 2, 'Col3' => 'Three' ], - [ 'Col1' => 'Hello, Sam', 'Col2' => 5, 'Col3' => "Nice to meet you\nWhat is your name?" ] - ], - $extractedData - ); + $extractedData = []; + foreach ($csv as $record) { + $extractedData[] = $record; + } - } + $this->assertEquals( + [ + [ 'Col1' => 'One', 'Col2' => 2, 'Col3' => 'Three' ], + [ 'Col1' => 'Hello, Sam', 'Col2' => 5, 'Col3' => "Nice to meet you\nWhat is your name?" ] + ], + $extractedData + ); + } } diff --git a/tests/DataExtractor/CsvTableWriterTest.php b/tests/DataExtractor/CsvTableWriterTest.php index 358af3f..ac25867 100644 --- a/tests/DataExtractor/CsvTableWriterTest.php +++ b/tests/DataExtractor/CsvTableWriterTest.php @@ -1,48 +1,51 @@ start(['Col1', 'Col2', 'Col3']); - $csv->writeRecord([ 'Col1' => 'One', 'Col2' => 2, 'Col3' => 'Three' ]); - $csv->writeRecord([ 'Col1' => 'Hello, Sam', 'Col2' => 5, 'Col3' => "Nice to meet you\nWhat is your name?" ]); - $csv->finish(); + $csv = new CsvTableWriter('/tmp/output.csv'); - $csvContent = file_get_contents('/tmp/output.csv'); - unlink('/tmp/output.csv'); + $csv->start(['Col1', 'Col2', 'Col3']); + $csv->writeRecord([ 'Col1' => 'One', 'Col2' => 2, 'Col3' => 'Three' ]); + $csv->writeRecord([ 'Col1' => 'Hello, Sam', 'Col2' => 5, 'Col3' => "Nice to meet you\nWhat is your name?" ]); + $csv->finish(); - $fixture = file_get_contents(__DIR__ . '/fixture/input.csv'); + $csvContent = file_get_contents('/tmp/output.csv'); + unlink('/tmp/output.csv'); - $this->assertEquals($fixture, $csvContent); - } + $fixture = file_get_contents(__DIR__ . '/fixture/input.csv'); - public function testNoStartCall() { + $this->assertEquals($fixture, $csvContent); + } - if (file_exists('/tmp/output.csv')) { - unlink('/tmp/output.csv'); - } + public function testNoStartCall() + { - $csv = new CsvTableWriter('/tmp/output.csv'); + if (file_exists('/tmp/output.csv')) { + unlink('/tmp/output.csv'); + } - $csv->writeRecord([ 'Col1' => 'One', 'Col2' => 2, 'Col3' => 'Three' ]); - $csv->writeRecord([ 'Col1' => 'Hello, Sam', 'Col2' => 5, 'Col3' => "Nice to meet you\nWhat is your name?" ]); - $csv->finish(); + $csv = new CsvTableWriter('/tmp/output.csv'); - $csvContent = file_get_contents('/tmp/output.csv'); - unlink('/tmp/output.csv'); + $csv->writeRecord([ 'Col1' => 'One', 'Col2' => 2, 'Col3' => 'Three' ]); + $csv->writeRecord([ 'Col1' => 'Hello, Sam', 'Col2' => 5, 'Col3' => "Nice to meet you\nWhat is your name?" ]); + $csv->finish(); - $fixture = file_get_contents(__DIR__ . '/fixture/input.csv'); + $csvContent = file_get_contents('/tmp/output.csv'); + unlink('/tmp/output.csv'); - $this->assertEquals($fixture, $csvContent); - } + $fixture = file_get_contents(__DIR__ . '/fixture/input.csv'); + $this->assertEquals($fixture, $csvContent); + } } diff --git a/tests/SmokeTest.php b/tests/SmokeTest.php index a65ff9d..834a78f 100644 --- a/tests/SmokeTest.php +++ b/tests/SmokeTest.php @@ -1,26 +1,29 @@ help(array()); - $helpText = ob_get_contents(); - ob_end_clean(); + // Internal call + ob_start(); + $ssPak->help(array()); + $helpText = ob_get_contents(); + ob_end_clean(); - // Call to binary - $this->assertEquals($helpText, `build/sspak.phar help &> /dev/stdout`); - } + // Call to binary + $this->assertEquals($helpText, `build/sspak.phar help &> /dev/stdout`); + } } diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..fd349ea --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,3 @@ +