diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..37ae30d --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/.idea +/vendor +/storage +.backap.yaml +php_errors.log +box.phar +backap.phar +backap.phar.gz \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4bc0fee --- /dev/null +++ b/LICENSE @@ -0,0 +1,18 @@ +Copyright (c) 2016 Tecactus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/bin/backap b/bin/backap new file mode 100755 index 0000000..8f39f9c --- /dev/null +++ b/bin/backap @@ -0,0 +1,10 @@ +#!/usr/bin/env php +=5.3.2" + }, + "require-dev": { + "phpunit/phpunit": "4.*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-0": { + "Doctrine\\Common\\Inflector\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Common String Manipulations with regard to casing and singular/plural rules.", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "inflection", + "pluralize", + "singularize", + "string" + ], + "time": "2015-11-06 14:35:42" + }, + { + "name": "dropbox/dropbox-sdk", + "version": "v1.1.7", + "source": { + "type": "git", + "url": "https://github.com/dropbox/dropbox-sdk-php.git", + "reference": "0f1a178ca9c0271bca6426dde8f5a2241578deae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dropbox/dropbox-sdk-php/zipball/0f1a178ca9c0271bca6426dde8f5a2241578deae", + "reference": "0f1a178ca9c0271bca6426dde8f5a2241578deae", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "php": ">= 5.3" + }, + "require-dev": { + "apigen/apigen": "4.1.2", + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "2.0.0RC3" + }, + "type": "library", + "autoload": { + "psr-0": { + "Dropbox": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Dropbox SDK for PHP", + "keywords": [ + "dropbox" + ], + "time": "2016-08-08 23:48:49" + }, + { + "name": "herrera-io/json", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/kherge-abandoned/php-json.git", + "reference": "60c696c9370a1e5136816ca557c17f82a6fa83f1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/kherge-abandoned/php-json/zipball/60c696c9370a1e5136816ca557c17f82a6fa83f1", + "reference": "60c696c9370a1e5136816ca557c17f82a6fa83f1", + "shasum": "" + }, + "require": { + "ext-json": "*", + "justinrainbow/json-schema": ">=1.0,<2.0-dev", + "php": ">=5.3.3", + "seld/jsonlint": ">=1.0,<2.0-dev" + }, + "require-dev": { + "herrera-io/phpunit-test-case": "1.*", + "mikey179/vfsstream": "1.1.0", + "phpunit/phpunit": "3.7.*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "files": [ + "src/lib/json_version.php" + ], + "psr-0": { + "Herrera\\Json": "src/lib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kevin Herrera", + "email": "kevin@herrera.io", + "homepage": "http://kevin.herrera.io" + } + ], + "description": "A library for simplifying JSON linting and validation.", + "homepage": "http://herrera-io.github.com/php-json", + "keywords": [ + "json", + "lint", + "schema", + "validate" + ], + "time": "2013-10-30 16:51:34" + }, + { + "name": "herrera-io/phar-update", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/kherge-abandoned/php-phar-update.git", + "reference": "15643c90d3d43620a4f45c910e6afb7a0ad4b488" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/kherge-abandoned/php-phar-update/zipball/15643c90d3d43620a4f45c910e6afb7a0ad4b488", + "reference": "15643c90d3d43620a4f45c910e6afb7a0ad4b488", + "shasum": "" + }, + "require": { + "herrera-io/json": "1.*", + "herrera-io/version": "1.*", + "php": ">=5.3.3" + }, + "require-dev": { + "herrera-io/phpunit-test-case": "1.*", + "mikey179/vfsstream": "1.1.0", + "phpunit/phpunit": "3.7.*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "files": [ + "src/lib/constants.php" + ], + "psr-0": { + "Herrera\\Phar\\Update": "src/lib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kevin Herrera", + "email": "kevin@herrera.io", + "homepage": "http://kevin.herrera.io" + } + ], + "description": "A library for self-updating Phars.", + "homepage": "http://herrera-io.github.com/php-phar-update", + "keywords": [ + "phar", + "update" + ], + "time": "2013-11-09 17:13:13" + }, + { + "name": "herrera-io/version", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/kherge-abandoned/php-version.git", + "reference": "d39d9642b92a04d8b8a28b871b797a35a2545e85" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/kherge-abandoned/php-version/zipball/d39d9642b92a04d8b8a28b871b797a35a2545e85", + "reference": "d39d9642b92a04d8b8a28b871b797a35a2545e85", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "herrera-io/phpunit-test-case": "1.*", + "phpunit/phpunit": "3.7.*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-0": { + "Herrera\\Version": "src/lib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kevin Herrera", + "email": "kevin@herrera.io", + "homepage": "http://kevin.herrera.io" + } + ], + "description": "A library for creating, editing, and comparing semantic versioning numbers.", + "homepage": "http://github.com/herrera-io/php-version", + "keywords": [ + "semantic", + "version" + ], + "time": "2014-05-27 05:29:25" + }, + { + "name": "illuminate/contracts", + "version": "v5.2.43", + "source": { + "type": "git", + "url": "https://github.com/illuminate/contracts.git", + "reference": "22bde7b048a33c702d9737fc1446234fff9b1363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/contracts/zipball/22bde7b048a33c702d9737fc1446234fff9b1363", + "reference": "22bde7b048a33c702d9737fc1446234fff9b1363", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.2-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Contracts\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Contracts package.", + "homepage": "http://laravel.com", + "time": "2016-08-08 11:46:08" + }, + { + "name": "illuminate/support", + "version": "v5.2.43", + "source": { + "type": "git", + "url": "https://github.com/illuminate/support.git", + "reference": "510230ab62a7d85dc70203f4fdca6fb71a19e08a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/support/zipball/510230ab62a7d85dc70203f4fdca6fb71a19e08a", + "reference": "510230ab62a7d85dc70203f4fdca6fb71a19e08a", + "shasum": "" + }, + "require": { + "doctrine/inflector": "~1.0", + "ext-mbstring": "*", + "illuminate/contracts": "5.2.*", + "paragonie/random_compat": "~1.4", + "php": ">=5.5.9" + }, + "replace": { + "tightenco/collect": "self.version" + }, + "suggest": { + "illuminate/filesystem": "Required to use the composer class (5.2.*).", + "jeremeamia/superclosure": "Required to be able to serialize closures (~2.2).", + "symfony/polyfill-php56": "Required to use the hash_equals function on PHP 5.5 (~1.0).", + "symfony/process": "Required to use the composer class (2.8.*|3.0.*).", + "symfony/var-dumper": "Improves the dd function (2.8.*|3.0.*)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.2-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Support\\": "" + }, + "files": [ + "helpers.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Support package.", + "homepage": "http://laravel.com", + "time": "2016-08-05 14:49:58" + }, + { + "name": "justinrainbow/json-schema", + "version": "1.6.1", + "source": { + "type": "git", + "url": "https://github.com/justinrainbow/json-schema.git", + "reference": "cc84765fb7317f6b07bd8ac78364747f95b86341" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/cc84765fb7317f6b07bd8ac78364747f95b86341", + "reference": "cc84765fb7317f6b07bd8ac78364747f95b86341", + "shasum": "" + }, + "require": { + "php": ">=5.3.29" + }, + "require-dev": { + "json-schema/json-schema-test-suite": "1.1.0", + "phpdocumentor/phpdocumentor": "~2", + "phpunit/phpunit": "~3.7" + }, + "bin": [ + "bin/validate-json" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6.x-dev" + } + }, + "autoload": { + "psr-4": { + "JsonSchema\\": "src/JsonSchema/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Bruno Prieto Reis", + "email": "bruno.p.reis@gmail.com" + }, + { + "name": "Justin Rainbow", + "email": "justin.rainbow@gmail.com" + }, + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + }, + { + "name": "Robert Schönthal", + "email": "seroscho@googlemail.com" + } + ], + "description": "A library to validate a json schema.", + "homepage": "https://github.com/justinrainbow/json-schema", + "keywords": [ + "json", + "schema" + ], + "time": "2016-01-25 15:43:01" + }, + { + "name": "league/flysystem", + "version": "1.0.27", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "50e2045ed70a7e75a5e30bc3662904f3b67af8a9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/50e2045ed70a7e75a5e30bc3662904f3b67af8a9", + "reference": "50e2045ed70a7e75a5e30bc3662904f3b67af8a9", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "conflict": { + "league/flysystem-sftp": "<1.0.6" + }, + "require-dev": { + "ext-fileinfo": "*", + "mockery/mockery": "~0.9", + "phpspec/phpspec": "^2.2", + "phpunit/phpunit": "~4.8" + }, + "suggest": { + "ext-fileinfo": "Required for MimeType", + "league/flysystem-aws-s3-v2": "Allows you to use S3 storage with AWS SDK v2", + "league/flysystem-aws-s3-v3": "Allows you to use S3 storage with AWS SDK v3", + "league/flysystem-azure": "Allows you to use Windows Azure Blob storage", + "league/flysystem-cached-adapter": "Flysystem adapter decorator for metadata caching", + "league/flysystem-copy": "Allows you to use Copy.com storage", + "league/flysystem-dropbox": "Allows you to use Dropbox storage", + "league/flysystem-eventable-filesystem": "Allows you to use EventableFilesystem", + "league/flysystem-rackspace": "Allows you to use Rackspace Cloud Files", + "league/flysystem-sftp": "Allows you to use SFTP server storage via phpseclib", + "league/flysystem-webdav": "Allows you to use WebDAV storage", + "league/flysystem-ziparchive": "Allows you to use ZipArchive adapter" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Flysystem\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frenky.net" + } + ], + "description": "Filesystem abstraction: Many filesystems, one API.", + "keywords": [ + "Cloud Files", + "WebDAV", + "abstraction", + "aws", + "cloud", + "copy.com", + "dropbox", + "file systems", + "files", + "filesystem", + "filesystems", + "ftp", + "rackspace", + "remote", + "s3", + "sftp", + "storage" + ], + "time": "2016-08-10 08:55:11" + }, + { + "name": "league/flysystem-dropbox", + "version": "1.0.4", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-dropbox.git", + "reference": "939f91ca00d0255d9b3aa313e191480d00f09382" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-dropbox/zipball/939f91ca00d0255d9b3aa313e191480d00f09382", + "reference": "939f91ca00d0255d9b3aa313e191480d00f09382", + "shasum": "" + }, + "require": { + "dropbox/dropbox-sdk": "~1.1", + "league/flysystem": "~1.0", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Flysystem\\Dropbox\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frenky.net" + } + ], + "description": "Flysystem adapter for Dropbox", + "time": "2016-04-25 18:51:39" + }, + { + "name": "nesbot/carbon", + "version": "1.21.0", + "source": { + "type": "git", + "url": "https://github.com/briannesbitt/Carbon.git", + "reference": "7b08ec6f75791e130012f206e3f7b0e76e18e3d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/7b08ec6f75791e130012f206e3f7b0e76e18e3d7", + "reference": "7b08ec6f75791e130012f206e3f7b0e76e18e3d7", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "symfony/translation": "~2.6|~3.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.0|~5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Carbon\\": "src/Carbon/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "http://nesbot.com" + } + ], + "description": "A simple API extension for DateTime.", + "homepage": "http://carbon.nesbot.com", + "keywords": [ + "date", + "datetime", + "time" + ], + "time": "2015-11-04 20:07:17" + }, + { + "name": "paragonie/random_compat", + "version": "v1.4.1", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "c7e26a21ba357863de030f0b9e701c7d04593774" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/c7e26a21ba357863de030f0b9e701c7d04593774", + "reference": "c7e26a21ba357863de030f0b9e701c7d04593774", + "shasum": "" + }, + "require": { + "php": ">=5.2.0" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "autoload": { + "files": [ + "lib/random.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "pseudorandom", + "random" + ], + "time": "2016-03-18 20:34:03" + }, + { + "name": "seld/jsonlint", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/jsonlint.git", + "reference": "66834d3e3566bb5798db7294619388786ae99394" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/66834d3e3566bb5798db7294619388786ae99394", + "reference": "66834d3e3566bb5798db7294619388786ae99394", + "shasum": "" + }, + "require": { + "php": "^5.3 || ^7.0" + }, + "bin": [ + "bin/jsonlint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Seld\\JsonLint\\": "src/Seld/JsonLint/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "JSON Linter", + "keywords": [ + "json", + "linter", + "parser", + "validator" + ], + "time": "2015-11-21 02:21:41" + }, + { + "name": "symfony/console", + "version": "v3.1.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "f9e638e8149e9e41b570ff092f8007c477ef0ce5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/f9e638e8149e9e41b570ff092f8007c477ef0ce5", + "reference": "f9e638e8149e9e41b570ff092f8007c477ef0ce5", + "shasum": "" + }, + "require": { + "php": ">=5.5.9", + "symfony/polyfill-mbstring": "~1.0" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/event-dispatcher": "~2.8|~3.0", + "symfony/process": "~2.8|~3.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/process": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Console Component", + "homepage": "https://symfony.com", + "time": "2016-07-26 08:04:17" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "dff51f72b0706335131b00a7f49606168c582594" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/dff51f72b0706335131b00a7f49606168c582594", + "reference": "dff51f72b0706335131b00a7f49606168c582594", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "time": "2016-05-18 14:26:46" + }, + { + "name": "symfony/translation", + "version": "v3.1.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "7713ddf81518d0823b027fe74ec390b80f6b6536" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/7713ddf81518d0823b027fe74ec390b80f6b6536", + "reference": "7713ddf81518d0823b027fe74ec390b80f6b6536", + "shasum": "" + }, + "require": { + "php": ">=5.5.9", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/config": "<2.8" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "~2.8|~3.0", + "symfony/intl": "~2.8|~3.0", + "symfony/yaml": "~2.8|~3.0" + }, + "suggest": { + "psr/log": "To use logging capability in translator", + "symfony/config": "", + "symfony/yaml": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Translation Component", + "homepage": "https://symfony.com", + "time": "2016-07-26 08:04:17" + }, + { + "name": "symfony/yaml", + "version": "v3.1.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "1819adf2066880c7967df7180f4f662b6f0567ac" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/1819adf2066880c7967df7180f4f662b6f0567ac", + "reference": "1819adf2066880c7967df7180f4f662b6f0567ac", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Yaml Component", + "homepage": "https://symfony.com", + "time": "2016-07-17 14:02:08" + }, + { + "name": "vlucas/phpdotenv", + "version": "v2.3.0", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "9ca5644c536654e9509b9d257f53c58630eb2a6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/9ca5644c536654e9509b9d257f53c58630eb2a6a", + "reference": "9ca5644c536654e9509b9d257f53c58630eb2a6a", + "shasum": "" + }, + "require": { + "php": ">=5.3.9" + }, + "require-dev": { + "phpunit/phpunit": "^4.8 || ^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.3-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause-Attribution" + ], + "authors": [ + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "http://www.vancelucas.com" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "time": "2016-06-14 14:14:52" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "^5.3.2 || ^7.0" + }, + "platform-dev": [] +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..f7c3ccf --- /dev/null +++ b/readme.md @@ -0,0 +1,379 @@ +# **Backap** + +Backap is a MySQL database backup manager written in PHP that can be bundled into a PHAR file. Backap aims to simply the process of dumping, restoring and syncing MySQL databases using simple CLI commands. + +## **How do I get started?** +You can use Backap in one of three ways: + +### As a Phar (Recommended) + +You may download a ready-to-use version of Backap as a Phar: +```sh +$ curl -LSs https://tecactus.github.io/backap/installer.php | php +``` +The command will check your PHP settings, warn you of any issues, and the download it to the current directory. From there, you may place it anywhere that will make it easier for you to access (such as `/usr/local/bin`) and chmod it to `755`. You can even rename it to just `backap` to avoid having to type the `.phar` extension every time. +```sh +$ backap --version +``` +Whenever a new version of the application is released, you can simply run the `update` command to get the latest version: +```sh +$ backap update +``` + +### As a Global Composer Install + +This is probably the best way when you have other tools like phpunit and other tools installed in this way: +```sh +$ composer global require tecactus/backap --prefer-source +``` + +### As a Composer Dependency + +You may also install Backap as a dependency for your Composer managed project: +```sh +$ composer require --dev tecactus/backap +``` +or +```json +{ + "require-dev": { + "tecactus/backap": "~1.0" + } +} +``` +Once you have installed the application, you can run the `help` command to get detailed information about all of the available commands. This should be your go-to place for information about how to use Backap. +```sh +$ backap help +``` + +## **Available Commands** + +### **init** + +the `init` command creates a yaml configuration file called `.backap.yaml` into the current directory. +```sh +$ backap init +``` +The `.backap.yaml` file structure is as follows: + +#### backap_storage_path (optional) +In this attribute you can define the *path* where all the backup files generated with Backap will be stored. + +This path *MUST* be *ABOSULTE* if is defined. + +```yaml +backap_storage_path: /absolute/path/to/backups/folder +``` +If any path is defined or if you omit this attribute, Backap will create a `storage/database` folder into the current directory. + +#### mysqldump_path (optional) +In this attribute you can define the path where `mysqldump` is located. + +This path *MUST* be *ABOSULTE* if is defined. +```yaml +mysqldump_path: /path/to/mysqldump +``` +If any path is defined or if you omit this attribute, Backap will try to call the globally reference to `mysqldump`. + +#### mysql_path (optional) +In this attribute you can define the path where `mysql` is located. + +This path *MUST* be *ABOSULTE* if is defined. +```yaml +mysql_path: /path/to/mysql +``` +If any path is defined or if you omit this attribute, Backap will try to call the globally reference to `mysql`. + +#### timezone (optional) +In this attribute you can define an specific *timezone*, this to know when the backup files were generated. + +This timezone *MUST* have a *VALID* name. + +For example: +```yaml +timezone: America/Lima +``` +If any timezone is defined or if you omit this attribute, Backap will use *UTC*. + +#### enable_compression (optional) +Backap generates `.sql` files by default, you can tell Backap to compress the generated backup file enabling compression then Backap will generate `.sql.gz` files. + +This value *MUST* be *BOOLEAN* if is defined. +```yaml +enable_compression: true +``` +If any value is defined or if you omit this attribute, Backap sets compression as `false` by default. + +#### default_connection (mandatory) +Backap needs to know which of your database connections will dump or restore, that's why you have to define a default connection to work on when you do not explicits define one. + +The connection name *MUST* be *DECLARED* as an element on the *connections* array attribute. +```yaml +default_connection: myconnection +``` + +#### connections (mandatory) +Backap can handle multiple database connections at the same time but first you have to define each one and asing them a diferent name. + +Each connection *MUST* have *HOSTNAME, DATABASE and USERNAME * declared attribtues. +PORT and PASSWORD are optional. + +For example: +```yaml +connections: + myconnection: + hostname: 192.168.1.27 + port: 3306 + database: important_db + username: userdb + password: supersecretpassword +``` +In the example we defined a connection named `myconnection`. +Of course you can define many as you want: +```yaml +connections: + +... + + myconnection: + hostname: 192.168.1.27 + port: 3306 + database: important_db + username: userdb + +... + + otherconnection: + hostname: 177.200.100.9 + port: 3306 + database: other_db + username: admindb + password: supersecretpassword + +... +``` + +#### cloud (optional) +Backap allows you to sync your backup files with cloud providers as *Dropbox*. +To enable this feature you must declare and array atribute called `cloud` and inside them declare, with a unique name, each of the cloud adapters, as an array too, that will be available to sync. + +For example: +```yaml +cloud: + adapaterone: + ... + adapatertwo: + ... +``` +Each provider requires diferent parameters thats why every adapter required diferent attributes but all of them *MUST* have an *ATTRIBUTE* called `provider`. + +For example: +```yaml +cloud: + adapaterone: + provider: dropbox + ... +``` + +##### Dropbox Adapter + +To declare a Dropbox adapter you must define the following attributes: +- **provider** as ***dropbox*** +- **access_token** generated on [Dropbox for Developers](https://www.dropbox.com/developers) +- **app_secret** generated on [Dropbox for Developers](https://www.dropbox.com/developers) +- **path** will be the path inside your Dropbox + +For example: +```yaml +cloud: + dropbox: + provider: dropbox + access_token: your_access_token + app_secret: your_secret + path: /path/on/your/dropbox +``` + + +### **mysql:dump** +The `mysql:dump` command dumps the database for the `default_connection` . +```sh +$ backap mysql:dump +``` + +#### **--connection, -c** +You can explicit define one or more connections to be dumped +```sh +$ backap mysql:dump --conection myconnection --connection otherconnetion +``` +or +```sh +$ backap mysql:dump -c myconnection -c otherconnection +``` + +#### **--no-compress** +Disable file compression regardless if is enabled in `.backap.yaml` file. This option will be always overwrited by `--compress` option. +```sh +$ backap mysql:dump --no-compress +``` + +#### **--compress** +Enable file compression regardless if is disabled in `.backap.yaml` file. This option will always overwrite `--no-compress` option. +```sh +$ backap mysql:dump --compress +``` + +#### **--sync, -s** +You can sync dump files with one or more cloud provider at the moment the dump file is generated. This option will be always overwrited by `--sync-all` option. +```sh +$ backap mysql:dump --sync dropboxone --sync dropboxtwo +``` +or +```sh +$ backap mysql:dump -s dropboxone -s dropboxtwo +``` + +#### **--sync-all, -S** +Also you can sync dump files with all the defined cloud provider at the same time at the moment the dump file is generated. This option will always overwrite `--sync` option. +```sh +$ backap mysql:dump --sync-all +``` +or +```sh +$ backap mysql:dump -S +``` + +### **mysql:restore** +The `mysql:restore` command restores the `default_connection` database from a backup file. +```sh +$ backap mysql:restore +``` +The `mysql:restore` command displays a list of all the backup files available *only* for the connection's database. Latest backup file is selected as default. +Then Backap ask for your confirmation to proceed with the database restoration. + +#### **--conection, -c** +You can explicit define the connection name to be restored +```sh +$ backap mysql:restore --conection otherconnetion +``` +or +```sh +$ backap mysql:restore -c otherconnection +``` + +#### **--filename, -f** +You can explicit define the backup file name to be restored +```sh +$ backap mysql:restore --filename mybackupfile.sql +``` +or +```sh +$ backap mysql:restore -f mybackupfile.sql +``` + +#### **--all-backup-files, -A** +The `mysql:restore` command by default displays a list of all the backup files available *only* for the defined connection's database but you can use the `--all-backup-files` option to return a list of all backup file generated by Backap. Latest backup file is selected as default. +```sh +$ backap mysql:restore --all-backup-files +``` +or +```sh +$ backap mysql:restore -A + +``` +#### **--restore-latest-backup , -L** +Explicit restore the latest backup file for the connection's database. +```sh +$ backap mysql:restore --restore-latest-backup +``` +or +```sh +$ backap mysql:restore -L +``` + +#### **--yes, -y** +The `mysql:restore` command always ask for your confirmation to proceed but you can confirm it without seeing the confirmation prompt using the `--yes` option. +```sh +$ backap mysql:restore --yes +``` +or +```sh +$ backap mysql:restore -y +``` + +#### **--from-cloud, -C** +Display a list of cloud providers where to retrieve backup files. +```sh +$ backap mysql:restore --from-cloud +``` +or +```sh +$ backap mysql:restore -C +``` + +#### **--from-provider, -p** +Explicit define the cloud provider where to retrieve backup files +```sh +$ backap mysql:restore --from-provider dropboxone +``` +or +```sh +$ backap mysql:restore -p dropboxone +``` + +### **files** +The `files` command displays a table with detailed data about all the backup files stored on your `backap_storage_path` +```sh +$ backap files +``` + +#### **--from-cloud, -C** +Display a list of cloud providers where to retrieve backup files. +```sh +$ backap files --from-cloud +``` +or +```sh +$ backap files -C +``` + +#### **--from-provider, -p** +Explicit define the cloud provider where to retrieve backup files +```sh +$ backap files --from-provider dropboxone +``` +or +```sh +$ backap files -p dropboxone +``` + +### **sync** +The `sync` command allows you to synchronize backup files with cloud providers. Pull files from cloud or push file to remote storage providers. +By default the `sync` asks you to choose a provider from a list of current configured providers but you can explicit define a provider using the `--provider` option. + +#### **push** +The `push` action will sync all your backup files stored locally to the remote selected provider. +```sh +$ backap sync push +``` +or +```sh +$ backap sync push --provider dropboxone +``` +or +```sh +$ backap sync push -p dropboxone +``` + +#### **pull** +The `pull` action will sync all your backup files stored on the selected provider to your local storage folder. +```sh +$ backap sync pull +``` +or +```sh +$ backap sync pull --provider dropboxone +``` +or +```sh +$ backap sync pull -p dropboxone +``` diff --git a/src/.backap.yaml.example b/src/.backap.yaml.example new file mode 100644 index 0000000..3488eea --- /dev/null +++ b/src/.backap.yaml.example @@ -0,0 +1,50 @@ +#/ Backap storage path is the base path where all your backups will be stored. +#/ The defined path must be absolute is you need to define one. +#/ If any path is defined Backap will create a /storage/database folder +#/ relative to the execution path. +# backap_storage_path: + +#/ You can specify your mysqldump path if is not defined globally. +# mysqldump_path: + +#/ You can specify your mysqldump path if is not defined globally. +# mysql_path: + +#/ You can enable compression for all your backups by default. +#/ Compression is disabled by default. +# enable_compression: true + +#/ You can declare a specific timezone. +#/ UTC will be used as default. +# timezone: America/Lima + +#/ Here you must declare your default connection name from the +#/ connections declared below in the connections array +default_connection: myconnection + +#/ Here you can define your database connections, you can name it like you want +connections: + myconnection: + hostname: + port: + database: + username: + password: + # secondconnection: + # hostname: + # port: + # database: + # username: + # password: + +#/ Cloud providers are defined to use the cloud-storage feature. +#/ By the moment this file was written Backap only supports +#/ Dropbox as cloud provider, different providers +#/ will be eventually added. + +# cloud: +# dropbox: +# provider: dropbox +# access_token: your_access_token +# app_secret: your_secret +# path: /path/on/your/dropbox \ No newline at end of file diff --git a/src/Application.php b/src/Application.php new file mode 100644 index 0000000..49cc18e --- /dev/null +++ b/src/Application.php @@ -0,0 +1,43 @@ +defineConstants(); + $this->output = new ConsoleOutput(); + + $application = new ConsoleApplication(); + $application->add(new Init()); + $application->add(new Files()); + $application->add(new Sync()); + $application->add(new MysqlDump()); + $application->add(new MysqlRestore()); + if (isPhar()) { + $application->add(new Update()); + } + $application->run(); + } + + protected function defineConstants() + { + define('CONFIG_YAML_PATH', WORKING_DIR . DIRECTORY_SEPARATOR . ".backap.yaml"); + define('CONFIG_YAML_EXAMPLE_PATH', __DIR__ . DIRECTORY_SEPARATOR . ".backap.yaml.example"); + } +} \ No newline at end of file diff --git a/src/Backap.php b/src/Backap.php new file mode 100644 index 0000000..8b95ad2 --- /dev/null +++ b/src/Backap.php @@ -0,0 +1,13 @@ +srcRoot = __DIR__ . DIRECTORY_SEPARATOR ."..".DIRECTORY_SEPARATOR.'src'; + $this->buildRoot = __DIR__ . DIRECTORY_SEPARATOR ."..".DIRECTORY_SEPARATOR; + } + + public function complie() + { + try { + $phar = new Phar($this->buildRoot . "/backap.phar", 0, "backap.phar"); + + + $iterator = new RecursiveDirectoryIterator($this->buildRoot, RecursiveDirectoryIterator::SKIP_DOTS); + + $filterIterator = new ExcludeDevFilterIterator($iterator); + + $phar->buildFromIterator(new RecursiveIteratorIterator($filterIterator), $this->buildRoot . DIRECTORY_SEPARATOR); + + $this->addBackapBin($phar); + + $phar->compressFiles(Phar::GZ); + + $phar->setStub($this->getStub()); + + printf("Backap Phar file succesfully created at " . $this->buildRoot . PHP_EOL); + } catch (Exception $e) { + echo $e->getMessage(); + } + } + + private function addBackapBin($phar) + { + $content = file_get_contents(__DIR__.'/../bin/backap'); + $content = preg_replace('{^#!/usr/bin/env php\s*}', '', $content); + $phar->addFromString('bin/backap', $content); + } + + private function getStub() + { + $stub = <<<'EOF' +#!/usr/bin/env php + + ____ ___ ________ __ ___ ____ + / __ )/ | / ____/ //_// | / __ \ + / __ / /| |/ / / ,< / /| | / /_/ / + / /_/ / ___ / /___/ /| |/ ___ |/ ____/ +/_____/_/ |_\____/_/ |_/_/ |_/_/ + +'; + + public function __construct() + { + parent::__construct('Backap', Backap::getVersion()); + } + + /** + * {@inheritDoc} + */ + public function run(InputInterface $input = null, OutputInterface $output = null) + { + if (null === $output) { + $output = new ConsoleOutput(); + } + return parent::run($input, $output); + } + + public function getHelp() + { + return self::$logo . parent::getHelp(); + } +} \ No newline at end of file diff --git a/src/Console/Command/Files.php b/src/Console/Command/Files.php new file mode 100644 index 0000000..ca65d72 --- /dev/null +++ b/src/Console/Command/Files.php @@ -0,0 +1,134 @@ +validator = new ConfigurationValidator(); + } + + protected function configure() + { + $this + ->setName('files') + ->setDescription('View all backups files') + ->addOption( + 'from-cloud', + 'C', + InputOption::VALUE_NONE, + "Display a list of cloud providers to retrieve files from", + null + ) + ->addOption( + 'from-provider', + 'p', + InputOption::VALUE_REQUIRED, + "Especifiy the cloud provider to retrieve files from", + null + ); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->validator->validate(); + $this->storage = new Storage(); + + $this->questioner = $this->getHelper('question'); + + $this->setCloudProvider($input, $output); + + $files = $this->storage->disk($this->cloudProvider ? $this->cloudProvider : 'local')->files(); + + if (count($files) == 0) { + $output->displayErrorAndDie("There are no backup files"); + } + + $headers = ['name', 'size', 'created at']; + $rows = []; + + foreach ($files as $file) { + array_push($rows, [ + $file['basename'], + formatBytes($file['size']), + Carbon::createFromTimestamp($file['timestamp'], BACKAP_TIMEZONE)->toDateTimeString(), + ]); + } + + $style = new TableStyle(); + + $style->setHorizontalBorderChar('-') + ->setVerticalBorderChar('|') + ->setCrossingChar('+') + -> setCellHeaderFormat('%s'); + + $table = new Table($output); + + $table->setHeaders($headers) + ->setRows($rows); + $table->setStyle($style); + $table->render(); + } + + protected function setCloudProvider($input, $output) + { + $fromCloud = $input->getOption('from-cloud'); + $this->cloudProvider = $input->getOption('from-provider'); + + $this->availableCloudProviders = $this->storage->getCloudAdapters(); + + if ($this->cloudProvider && $fromCloud) { + if (!in_array($this->cloudProvider, $this->availableCloudProviders)) { + $output->displayErrorAndDie("There is no parameter configuration for cloud provider named '" . $this->cloudProvider . "' in the .backap.yaml file"); + } + } + + if(is_null($this->cloudProvider) && $fromCloud) + { + if (count($this->availableCloudProviders) == 0) { + $output->displayErrorAndDie("There are no cloud providers configured on .backap.yaml file"); + } + + $defaultIndex = count($this->availableCloudProviders) - 1; + + $question = new ChoiceQuestion( + 'Wich cloud provider do you want to restore from? (default ' . $this->availableCloudProviders[$defaultIndex] . ')', + $this->availableCloudProviders, + $defaultIndex + ); + + $question->setErrorMessage('Option %s is invalid.' . PHP_EOL); + + $this->cloudProvider = $this->questioner->ask($input, $output, $question); + } + + if (!is_null($this->cloudProvider)) { + $output->display("Viewing files stored on " . $this->cloudProvider . ""); + } else { + $output->display("Viewing files from Local, these files are stored on your own system"); + } + + } +} \ No newline at end of file diff --git a/src/Console/Command/Init.php b/src/Console/Command/Init.php new file mode 100644 index 0000000..f3f99fd --- /dev/null +++ b/src/Console/Command/Init.php @@ -0,0 +1,34 @@ +setName('init') + ->setDescription('Create a .backap.yaml file'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + if (!file_exists(CONFIG_YAML_PATH)) { + copy(CONFIG_YAML_EXAMPLE_PATH, CONFIG_YAML_PATH); + $output->displayInfo(".backap.yaml file created successfully"); + } else { + $output->displayInfo(".backap.yaml file already exists"); + } + } +} \ No newline at end of file diff --git a/src/Console/Command/MysqlDump.php b/src/Console/Command/MysqlDump.php new file mode 100644 index 0000000..4fcc768 --- /dev/null +++ b/src/Console/Command/MysqlDump.php @@ -0,0 +1,158 @@ +validator = new ConfigurationValidator(); + } + + protected function configure() + { + $this + ->setName('mysql:dump') + ->setDescription('Dumps a MySQL database to a file') + ->addOption( + 'connection', + 'c', + InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, + "Especifiy the database connection names", + array() + ) + ->addOption( + 'no-compress', + null, + InputOption::VALUE_NONE, + "Disable file compression regardless if is enabled in .backap.yaml file. This option will be always overwrited by --compress option", + null + ) + ->addOption( + 'compress', + null, + InputOption::VALUE_NONE, + "Enable file compression regardless if is disabled in .backap.yaml file. This option will always overwrite --no-compress option", + null + ) + ->addOption( + 'sync', + 's', + InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, + "Synchronize dump file with cloud providers. This option will be always overwrited by --sync-all option", + null + ) + ->addOption( + 'sync-all', + 'S', + InputOption::VALUE_NONE, + "Synchronize dump file with all cloud providers. This option will always overwrite --sync option", + null + ); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->validator->validate(); + $this->storage = new Storage(); + + $availableDbConnections = $GLOBALS['DB_CONNECTIONS']; + + $connectionNames = $input->getOption('connection'); + + if (empty($connectionNames)) { + array_push($connectionNames, $GLOBALS['DEFAULT_DB_CONNECTION']); + } + + $availableCloudProviders = $this->storage->getCloudAdapters(); + + $this->cloudAdapters = $input->getOption('sync'); + + $syncAll = $input->getOption('sync-all'); + + $this->sync = count($this->cloudAdapters) >= 1 || $syncAll; + + if (!$syncAll) { + foreach ($this->cloudAdapters as $provider) { + if (!in_array($provider, $availableCloudProviders)) { + $output->displayErrorAndDie("There is no parameter configuration for cloud provider named '$provider' in the .backap.yaml file"); + } + } + } else { + $this->cloudAdapters = $availableCloudProviders; + } + + $compress = $input->getOption('compress'); + $noCompress = $input->getOption('no-compress'); + + if ($compress) { + $this->isCompressionEnabled = true; + } elseif ($noCompress) { + $this->isCompressionEnabled = false; + } else { + $this->isCompressionEnabled = ENABLE_COMPRESSION; + } + + foreach ($connectionNames as $connectionName) { + if (!array_key_exists($connectionName, $availableDbConnections)) { + $output->displayErrorAndDie("There is no parameter configuration for connection named '$connectionName' in the .backap.yaml file"); + } + } + + foreach ($connectionNames as $connectionName) { + $this->dumpDatabase($availableDbConnections[$connectionName], $output); + } + } + + protected function dumpDatabase(array $connection, OutputInterface $output) + { + $hostname = escapeshellarg($connection['hostname']); + $port = $connection['port']; + $database = $connection['database']; + $username = escapeshellarg($connection['username']); + $password = $connection['password']; + + $databaseArg = escapeshellarg($database); + $portArg = !empty($port) ? "-P ". escapeshellarg($port) : ""; + $passwordArg = !empty($password) ? "-p" . escapeshellarg($password) : ""; + + $compressionMessage = $this->isCompressionEnabled ? "and compressed" : ""; + + $filename = Carbon::now()->format('YmdHis') . "_" . $database . ".sql" . ($this->isCompressionEnabled ? '.gz' : ''); + + $path = LOCAL_STORAGE_PATH . DIRECTORY_SEPARATOR . $filename; + + $dumpCommand = MYSQLDUMP_PATH . " -C -h $hostname $portArg -u$username $passwordArg --single-transaction --skip-lock-tables --quick $databaseArg"; + + exec($dumpCommand, $dumpResult, $result); + + if ($result == 0) { + $dumpResult = implode(PHP_EOL, $dumpResult); + $dumpResult = $this->isCompressionEnabled ? gzcompress($dumpResult, 9) : $dumpResult; + $this->storage->write($filename, $dumpResult); + $output->display("Database $database dumped $compressionMessage successfully to $path"); + if ($this->sync) { + $this->storage->syncFile($filename, $this->cloudAdapters); + } + } else { + $output->displayError("Database $database cannot be dumped"); + } + } +} \ No newline at end of file diff --git a/src/Console/Command/MysqlRestore.php b/src/Console/Command/MysqlRestore.php new file mode 100644 index 0000000..21610c0 --- /dev/null +++ b/src/Console/Command/MysqlRestore.php @@ -0,0 +1,355 @@ +validator = new ConfigurationValidator(); + $this->backupFiles = []; + $this->backupFileNames = []; + $this->backupFileAlternatives = []; + } + + protected function configure() + { + $this + ->setName('mysql:restore') + ->setDescription('Restores a MySQL database from a file') + ->addOption( + 'connection', + 'c', + InputOption::VALUE_OPTIONAL, + "Especifiy the database connection name", + null + ) + ->addOption( + 'filename', + 'f', + InputOption::VALUE_OPTIONAL, + "Especifiy the database connection name", + null + ) + ->addOption( + 'all-backup-files', + 'A', + InputOption::VALUE_NONE, + "Display all backup files as selectable option", + null + ) + ->addOption( + 'restore-latest-backup', + 'L', + InputOption::VALUE_NONE, + "Use latest backup file to restore database", + null + ) + ->addOption( + 'yes', + 'y', + InputOption::VALUE_NONE, + "Confirms database restoration", + null + ) + ->addOption( + 'from-cloud', + 'C', + InputOption::VALUE_NONE, + "Display a list of cloud providers where to retrieve backup files.", + null + ) + ->addOption( + 'from-provider', + 'p', + InputOption::VALUE_REQUIRED, + "Explicit define the cloud provider where to retrieve backup files", + null + ); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->validator->validate(); + $this->storage = new Storage(); + + $this->questioner = $this->getHelper('question'); + + $this->availableDbConnections = $GLOBALS['DB_CONNECTIONS']; + + $this->connectionName = $input->getOption('connection'); + + if (!is_null($this->connectionName) && !array_key_exists($this->connectionName, $this->availableDbConnections)) { + $output->displayErrorAndDie("There is no parameter configuration for connection named '" . $this->connectionName . "' in the file .backap.yaml"); + } + + $this->setStorageProvider($input, $output); + + $files = $this->storage->disk($this->storageProvider)->files(); + + rsort($files); + + foreach ($files as $file) { + if (ends_with($file['basename'], '.sql') || ends_with($file['basename'], '.sql.gz')) { + array_push($this->backupFiles, $file); + array_push($this->backupFileNames, $file['basename']); + } + } + + if (count($this->backupFiles) == 0) { + $output->displayErrorAndDie("There are no backup files to restore"); + } + + $this->restoreLatestBackup = $input->getOption('restore-latest-backup'); + + if (!$this->restoreLatestBackup) { + $this->filename = $input->getOption('filename'); + + if (!is_null($this->filename) && !in_array($this->filename, $this->backupFiles)) { + $output->displayErrorAndDie("There is no backup file named '" . $this->filename . "'"); + } + } + + $this->setConnection($input, $output); + + $this->setFilename($input, $output); + + $this->confirmRestoration($input, $output); + } + + protected function setStorageProvider($input, $output) + { + $fromCloud = $input->getOption('from-cloud'); + $this->storageProvider = $input->getOption('from-provider'); + + $this->availableCloudProviders = $this->storage->getCloudAdapters(); + + if ($this->storageProvider && $fromCloud) { + if (!in_array($this->storageProvider, $this->availableCloudProviders)) { + $output->displayErrorAndDie("There is no parameter configuration for cloud provider named '" . $this->storageProvider . "' in the .backap.yaml file"); + } + } + + if(is_null($this->storageProvider) && $fromCloud) + { + if (count($this->availableCloudProviders) == 0) { + $output->displayErrorAndDie("There are no cloud providers configured on .backap.yaml file"); + } + + $defaultIndex = count($this->availableCloudProviders) - 1; + + $question = new ChoiceQuestion( + 'Which cloud provider do you want to restore from? (default ' . $this->availableCloudProviders[$defaultIndex] . ')', + $this->availableCloudProviders, + $defaultIndex + ); + + $question->setErrorMessage('Option %s is invalid.' . PHP_EOL); + + $this->storageProvider = $this->questioner->ask($input, $output, $question); + } + + if (!is_null($this->storageProvider)) { + $output->display("Using " . $this->storageProvider . " as cloud provider, data will be restored from there"); + } else { + $this->storageProvider = 'local'; + $output->display("Using Local provider, data will be restored from your own system"); + } + + } + + protected function setConnection($input, $output) + { + $connectionNames = array_keys($this->availableDbConnections); + + if (is_null($this->connectionName) && count($connectionNames) > 1) { + $question = new ChoiceQuestion( + 'Please select a database connection (default)', + $connectionNames, + 0 + ); + $question->setErrorMessage('Option %s is invalid.' . PHP_EOL); + + $this->connectionName = $this->questioner->ask($input, $output, $question); + + $output->display("You have just selected " . $this->connectionName . " as connection"); + + $this->connection = $this->availableDbConnections[$this->connectionName]; + } + + if (is_null($this->connectionName) && count($connectionNames) == 1) { + $this->connectionName = $GLOBALS['DEFAULT_DB_CONNECTION']; + } + + $this->connection = $this->availableDbConnections[$this->connectionName]; + + $output->display("Using " . $this->connectionName . " connection, database " . $this->connection['database'] . " will be restored"); + } + + protected function setFilename($input, $output) + { + if (is_null($this->filename)) { + if ($this->restoreLatestBackup) { + $this->filename = $this->backupFiles[count($this->backupFiles) - 1]; + } else { + if ($input->getOption('all-backup-files')) { + $this->backupFileAlternatives = $this->backupFileNames; + } else { + $selectedFiles = []; + foreach ($this->backupFileNames as $index => $backupFileName) { + if (ends_with($backupFileName, '_' . $this->connection['database'] . '.sql') || ends_with($backupFileName, '_' . $this->connection['database'] . 'sql.gz')) { + array_push($this->backupFileAlternatives, $backupFileName); + array_push($selectedFiles, $this->backupFiles[$index]); + } + } + $this->backupFiles = $selectedFiles; + } + + if (count($this->backupFileAlternatives) == 0) { + $output->displayErrorAndDie("There are no backup files for database '" . $this->connection['database'] . "' to restore"); + } + + $defaultIndex = 0; + + $question = new ChoiceQuestion( + 'Which database backup file do you want to restore on ' . $this->connection['database'] . '? (default ' . $this->backupFileAlternatives[$defaultIndex] . ')', + $this->backupFileAlternatives, + $defaultIndex + ); + + $question->setErrorMessage('Option %s is invalid.' . PHP_EOL); + + // show table + $this->showBackupFilesTableData($this->backupFiles, $output); + + $this->filename = $this->questioner->ask($input, $output, $question); + } + + } + + $output->display("You have just selected " . $this->filename . " to be restored in " . $this->connection['database'] . " database"); + } + + protected function confirmRestoration($input, $output) { + if (!$input->getOption('yes')) { + $question = new ConfirmationQuestion('Continue with database restoration? (default NO)', false, '/^(y|j)/i'); + + if (!$this->questioner->ask($input, $output, $question)) { + $output->displayErrorAndDie('Database restoration cancelled'); + } + } + + $this->restoreDatabase($output); + } + + protected function restoreDatabase($output) + { + $hostname = escapeshellarg($this->connection['hostname']); + $port = $this->connection['port']; + $database = $this->connection['database']; + $username = escapeshellarg($this->connection['username']); + $password = $this->connection['password']; + + $databaseArg = escapeshellarg($database); + $portArg = !empty($port) ? "-P ". escapeshellarg($port) : ""; + $passwordArg = !empty($password) ? "-p" . escapeshellarg($password) : ""; + + $this->localFilename = $this->filename; + + if ($this->storageProvider != 'local') { + if (ends_with($this->filename, '.gz')) { + $this->localFilename = str_replace('.sql.gz', '.cloud.sql.gz', $this->filename); + } else { + $this->localFilename = str_replace('.sql', '.cloud.sql', $this->filename); + } + $this->storage->syncFileFromProvider($this->storageProvider, $this->filename, $this->localFilename); + } + + $this->storage->disk('local'); + + $filename = $this->localFilename; + + if (ends_with($filename, '.gz')) { + $fileContent = gzuncompress($this->storage->read($this->localFilename)); + $filename = str_replace('.sql.gz', '.tmp', $this->localFilename); + $this->storage->write($filename, $fileContent); + $isTempFilename = true; + } + + $restoreCommand = MYSQL_PATH . " -h $hostname $portArg -u$username $passwordArg $databaseArg < " . $this->storage->absFilePath($filename); + + exec($restoreCommand, $restoreResult, $result); + + if ($result == 0) { + $output->display("Database $database restored successfully from " . $this->filename . ""); + } else { + $output->displayError("Database $database cannot be restored"); + } + + if (isset($isTempFilename)) { + $this->storage->delete($filename); + } + if ($this->localFilename != $this->filename) { + $this->storage->delete($this->localFilename); + } + } + + protected function showBackupFilesTableData($files, $output) + { + $headers = ['option', 'name', 'size', 'created at']; + $rows = []; + + foreach ($files as $index => $file) { + array_push($rows, [ + $index, + $file['basename'], + formatBytes($file['size']), + Carbon::createFromTimestamp($file['timestamp'], BACKAP_TIMEZONE)->toDateTimeString(), + ]); + } + + $style = new TableStyle(); + + $style->setHorizontalBorderChar('-') + ->setVerticalBorderChar('|') + ->setCrossingChar('+') + -> setCellHeaderFormat('%s'); + + $table = new Table($output); + + $table->setHeaders($headers) + ->setRows($rows); + $table->setStyle($style); + $table->render(); + } +} \ No newline at end of file diff --git a/src/Console/Command/Sync.php b/src/Console/Command/Sync.php new file mode 100644 index 0000000..3cd0652 --- /dev/null +++ b/src/Console/Command/Sync.php @@ -0,0 +1,154 @@ +validator = new ConfigurationValidator(); + } + + protected function configure() + { + $this + ->setName('sync') + ->setDescription('Synchronize backup files with cloud providers. Pull files from cloud or push file to remote storage providers.') + ->addArgument( + 'action', + InputArgument::REQUIRED, + 'Do you want to perform a pull or a push?' + ) + ->addOption( + 'provider', + 'p', + InputOption::VALUE_REQUIRED, + "Especifiy the cloud provider", + null + ) + ->addOption( + 'yes', + 'y', + InputOption::VALUE_NONE, + "Confirms action", + null + ); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->validator->validate(); + $this->storage = new Storage(); + + $this->questioner = $this->getHelper('question'); + + $this->setAction($input, $output); + + $this->setCloudProvider($input, $output); + + $files = $this->storage->disk($this->isPull ? $this->cloudProvider : 'local')->files(); + + if (count($files) == 0) { + $output->displayErrorAndDie("There are no backup files to " . $this->action); + } + + $this->confirmAction($input, $output); + } + + protected function setAction($input, $output) + { + $this->action = trim($input->getArgument('action')); + + if ($this->action == 'pull') { + $this->isPull = true; + } + elseif ($this->action == 'push') { + $this->isPush = true; + } + else { + $output->displayErrorAndDie("Action MUST be 'push' or 'pull'"); + } + } + + protected function setCloudProvider($input, $output) + { + $this->cloudProvider = $input->getOption('provider'); + + $this->availableCloudProviders = $this->storage->getCloudAdapters(); + + if ($this->cloudProvider) { + if (!in_array($this->cloudProvider, $this->availableCloudProviders)) { + $output->displayErrorAndDie("There is no parameter configuration for cloud provider named '" . $this->cloudProvider . "' in the .backap.yaml file"); + } + } + + if(is_null($this->cloudProvider)) + { + if (count($this->availableCloudProviders) == 0) { + $output->displayErrorAndDie("There are no cloud providers configured on .backap.yaml file"); + } + + $defaultIndex = count($this->availableCloudProviders) - 1; + + $question = new ChoiceQuestion( + 'Which cloud provider do you want to ' . $this->action . ($this->isPush ? ' to' : ' from') . '? (default ' . $this->availableCloudProviders[$defaultIndex] . ')', + $this->availableCloudProviders, + $defaultIndex + ); + + $question->setErrorMessage('Option %s is invalid.' . PHP_EOL); + + $this->cloudProvider = $this->questioner->ask($input, $output, $question); + } + + $output->display(($this->isPull ? 'Pulling' : 'Pushing') . " files " . ($this->isPull ? 'from' : 'to') . " " . $this->cloudProvider . ""); + + } + + protected function confirmAction($input, $output) + { + if (!$input->getOption('yes')) { + $question = new ConfirmationQuestion('Continue with sync ' . $this->action .'? (default NO)', false, '/^(y|j)/i'); + + if (!$this->questioner->ask($input, $output, $question)) { + $output->displayErrorAndDie('Sync cancelled'); + } + } + + $this->performAction($output); + } + + protected function performAction($output) + { + if ($this->isPush) { + $this->storage->syncPush($this->cloudProvider); + } + else { + $this->storage->syncPull($this->cloudProvider); + } + } +} \ No newline at end of file diff --git a/src/Console/Command/Update.php b/src/Console/Command/Update.php new file mode 100644 index 0000000..dcfbaa5 --- /dev/null +++ b/src/Console/Command/Update.php @@ -0,0 +1,34 @@ +setName('update') + ->setDescription('Updates backap.phar to the latest version'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $manager = new Manager(Manifest::loadFile(self::MANIFEST_FILE)); + $manager->update($this->getApplication()->getVersion(), true); + } +} \ No newline at end of file diff --git a/src/Console/ConsoleOutput.php b/src/Console/ConsoleOutput.php new file mode 100644 index 0000000..481215f --- /dev/null +++ b/src/Console/ConsoleOutput.php @@ -0,0 +1,48 @@ + new OutputFormatterStyle('red'), + 'primary' => new OutputFormatterStyle('cyan'), + 'warning' => new OutputFormatterStyle('black', 'yellow'), + ]); + parent::__construct(BaseConsoleOutput::VERBOSITY_NORMAL, null, $formatter); + } + + public function display($message) + { + $this->writeln($message . PHP_EOL); + } + + public function displayAndDie($message) + { + $this->writeln($message . PHP_EOL); + die(); + } + + public function displayInfo($message) + { + $this->writeln("$message" . PHP_EOL); + } + + public function displayError($message) + { + $this->writeln("$message" . PHP_EOL); + } + + public function displayErrorAndDie($message) + { + $this->displayError($message); + die(); + } + +} \ No newline at end of file diff --git a/src/Storage/Storage.php b/src/Storage/Storage.php new file mode 100644 index 0000000..2e43ee9 --- /dev/null +++ b/src/Storage/Storage.php @@ -0,0 +1,241 @@ +output = new ConsoleOutput(); + $this->initLocalAdapter(); + $this->initCloudAdapters(); + $this->manager = new MountManager($this->adapters); + } + + public function disk($adapter = 'local') + { + if (isset($this->adapters[$adapter])) { + $this->selectedAdapter = $adapter; + return $this; + } + $this->output->displayErrorAndDie("The '$adapter' adapter is not available"); + } + + public function getCloudAdapters() + { + return $this->cloudAdapters; + } + + public function hasAdapter($adapter) + { + return in_array($adapter, array_keys($this->adapters)); + } + + public function syncFile($filename, $cloudAdapters = null) + { + $cloudAdapters = is_null($cloudAdapters) ? $this->cloudAdapters : $cloudAdapters; + + foreach ($cloudAdapters as $adapter) { + if (!$this->disk($adapter)->has($filename)) { + $this->manager->copy($this->appendAdapterPrefix($filename), $this->appendAdapterPrefix($filename, $adapter)); + $this->output->display("Database dump $filename synchronized successfully with $adapter"); + } + } + } + + public function syncFileFromProvider($adapter = null, $filename, $localFilename = null) + { + $localFilename = $localFilename ? $localFilename : $filename; + + $this->manager->put($this->appendAdapterPrefix($localFilename, 'local'), $this->manager->read($this->appendAdapterPrefix($filename, $adapter))); + } + + public function syncPull($cloudProvider) + { + $files = $this->disk($cloudProvider)->files(); + + foreach ($files as $file) { + $update = false; + $new = false; + + if ( ! $this->manager->has("local://" .$file['path'])) { + $new = true; + } elseif ($this->manager->getTimestamp("$cloudProvider://" . $file['path']) > $this->manager->getTimestamp("local://" . $file['path'])) { + $update = true; + } else { + $this->output->display("file " . $file['path'] . " already synced"); + } + + if ($update) { + $this->manager->put("local://" . $file['path'], $this->manager->read("$cloudProvider://" . $file['path'])); + $this->output->display("file " . $file['path'] . " updated"); + } + + if ($new) { + $this->manager->copy("$cloudProvider://" . $file['path'], "local://" . $file['path']); + $this->output->display("file " . $file['path'] . " copied"); + } + } + } + + public function syncPush($cloudProviders = null) + { + $cloudProviders = is_null($cloudProviders) ? $this->cloudAdapters : $cloudProviders; + $cloudProviders = is_array($cloudProviders) ? $cloudProviders : [$cloudProviders]; + + $files = $this->disk('local')->files(); + + foreach ($files as $file) { + foreach ($cloudProviders as $provider) { + $update = false; + $new = false; + + if ( ! $this->manager->has("$provider://" .$file['path'])) { + $new = true; + } elseif ($this->manager->getTimestamp("local://" . $file['path']) > $this->manager->getTimestamp("$provider://" . $file['path'])) { + $update = true; + } else { + $this->output->display("file " . $file['path'] . " already synced"); + } + + if ($update) { + $this->manager->put("$provider://" . $file['path'], $this->manager->read("local://" . $file['path'])); + $this->output->display("file " . $file['path'] . " updated"); + } + + if ($new) { + $this->manager->copy("local://" . $file['path'], "$provider://" . $file['path']); + $this->output->display("file " . $file['path'] . " copied"); + } + } + } + } + + public function has($filename) + { + return $this->manager->has($this->appendSelectedAdapterPrefix($filename)); + } + + public function files() + { + $files = $this->manager->listContents($this->appendSelectedAdapterPrefix()); + cleanFiles($files); + return $files; + } + + public function absFilePath($filename) + { + if($this->has($filename)) { + return $this->getRealPathPrefix() . $filename; + } + return null; + } + + public function write($filename, $contents) + { + $this->manager->write($this->appendSelectedAdapterPrefix($filename), $contents); + } + + public function read($filename) + { + $content = $this->manager->read($this->appendSelectedAdapterPrefix($filename)); + return $content; + } + + public function delete($filename) + { + if ($this->has($filename)) { + $content = $this->manager->delete($this->appendSelectedAdapterPrefix($filename)); + } + } + + protected function initLocalAdapter() + { + if ($this->is_absolute_path(LOCAL_STORAGE_PATH) === false) { + $this->output->displayError("Storage path '" . LOCAL_STORAGE_PATH . "' MUST BE an ABSOLUTE PATH"); + die(); + } + if (!is_null($errorPath = $this->is_a_file_in_path(LOCAL_STORAGE_PATH))) { + + $this->output->displayError("'$errorPath' MUST BE a FOLDER"); + die(); + } + $local = new Filesystem(new Local(LOCAL_STORAGE_PATH, 0)); + $this->adapters['local'] = $local; + } + + protected function initCloudAdapters() + { + $cloudProviders = $GLOBALS['CLOUD_PROVIDERS']; + + foreach ($cloudProviders as $provider => $adapters) { + if($provider == 'dropbox') { + foreach ($adapters as $adapterName => $adapterData) { + $this->initDropboxAdapter($adapterName, $adapterData); + RootCertificates::useExternalPaths(); + } + } + } + } + + protected function initDropboxAdapter($adapterName, array $dropboxData) + { + $dropboxClient = new DropboxClient($dropboxData['access_token'], $dropboxData['app_secret']); + $dropboxAdapter = new DropboxAdapter($dropboxClient, $dropboxData['path']); + $dropbox = new Filesystem($dropboxAdapter); + $this->adapters[$adapterName] = $dropbox; + array_push($this->cloudAdapters, $adapterName); + } + + protected function is_absolute_path($path) { + if($path === null || $path === '') throw new Exception("Empty path"); + return $path[0] === DIRECTORY_SEPARATOR || preg_match('~\A[A-Z]:(?![^/\\\\])~i',$path) > 0; + } + + protected function is_a_file_in_path($path) + { + $dirParts = explode(DIRECTORY_SEPARATOR, $path); + $assemblePath = []; + $current = ''; + foreach ($dirParts as $dir) { + array_push($assemblePath, $dir); + $current = implode(DIRECTORY_SEPARATOR, $assemblePath); + if (file_exists($current) && is_file($current)) { + return $current; + } + } + return null; + } + + protected function appendSelectedAdapterPrefix($file = null) + { + return $this->selectedAdapter . "://" . $file; + } + + protected function appendAdapterPrefix($file = null, $adapter = 'local') + { + return $adapter . "://" . $file; + } + + protected function getRealPathPrefix() + { + return $this->manager->getFilesystem($this->selectedAdapter)->getAdapter()->getPathPrefix(); + } +} \ No newline at end of file diff --git a/src/Support/ExcludeDevFilterIterator.php b/src/Support/ExcludeDevFilterIterator.php new file mode 100644 index 0000000..49a5817 --- /dev/null +++ b/src/Support/ExcludeDevFilterIterator.php @@ -0,0 +1,29 @@ +current()->getFilename(), + self::$FILTERS, + true + ); + } + +} \ No newline at end of file diff --git a/src/Support/helpers.php b/src/Support/helpers.php new file mode 100644 index 0000000..eda1061 --- /dev/null +++ b/src/Support/helpers.php @@ -0,0 +1,81 @@ + $file) { + if ( !($file['type'] == 'file' & ($file['extension'] == 'gz' || $file['extension'] == 'sql')) ) { + unset($files[$key]); + } + } + } +} + +if (! function_exists('formatBytes')) { + function formatBytes($bytes, $precision = 2) { + $units = array('B', 'KB', 'MB', 'GB', 'TB'); + $bytes = max($bytes, 0); + $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); + $pow = min($pow, count($units) - 1); + $bytes /= pow(1024, $pow); + return round($bytes, $precision) . ' ' . $units[$pow]; + } +} + +if (! function_exists('isPhar')) { + function isPhar() { + return starts_with(__DIR__, 'phar://'); + } +} \ No newline at end of file diff --git a/src/Validation/ConfigurationValidator.php b/src/Validation/ConfigurationValidator.php new file mode 100644 index 0000000..4c3b9ed --- /dev/null +++ b/src/Validation/ConfigurationValidator.php @@ -0,0 +1,227 @@ +output = new ConsoleOutput(); + $this->mandatoryAttributes = ['default_connection', 'connections']; + $this->optionalAttributes = ['timezone', 'backap_storage_path', 'mysqldump_path', 'mysql_path', 'enable_compression', 'cloud']; + $this->arrayAttributes = ['connections', 'connections.*', 'cloud', 'cloud.*']; + $this->stringAttributes = ['default_connection', 'backap_storage_path', 'mysqldump_path', 'mysql_path']; + $this->booleanAttributes = ['enable_compression']; + } + + public function validate() + { + if ($this->loadConfigYaml()) { + $this->defineConstants(); + $this->defineGlobals(); + } else { + $this->output->displayError("Please create a .backap.yaml file before continue."); + $this->output->displayAndDie("Run init command to create one."); + } + } + + protected function existConfigYaml() + { + return file_exists(CONFIG_YAML_PATH); + } + + protected function loadConfigYaml() + { + if ($this->existConfigYaml()) { + try { + $this->config = Yaml::parse(file_get_contents(CONFIG_YAML_PATH)); + $this->validateConfigAttributes(); + return true; + } catch (ParseException $e) { + printf("Unable to parse the YAML string: %s", $e->getMessage()); + die(); + } + } + return false; + } + + protected function validateConfigAttributes() + { + foreach ($this->mandatoryAttributes as $attr) { + if (!isset($this->config[$attr])) { + $this->output->displayErrorAndDie("[$attr] attribute MUST exists in the '.backap.yaml' file"); + } + if (is_null($this->config[$attr])) { + $this->output->displayErrorAndDie("[$attr] attribute MUST have a value in the '.backap.yaml' file"); + } + } + + foreach ($this->optionalAttributes as $attr) { + if (!isset($this->config[$attr])) { + $this->config[$attr] = in_array($attr, $this->arrayAttributes) ? [] : null; + } + } + + foreach ($this->arrayAttributes as $attrs) { + $attrParts = explode('.', $attrs); + $attrParts = count($attrParts) == 1 ? $attrParts[0] : $attrParts; + + if (is_array($attrParts)) { + if ($attrParts[count($attrParts) - 1] == '*') { + array_pop($attrParts); + $path = implode('.', $attrParts); + foreach (getValueByPath($this->config, $path) as $key => $value) { + if (!is_array($value)) { + $redable = makeRedableConfigArray(array_merge($attrParts, [$key])); + $this->output->displayErrorAndDie("$redable attribute MUST be an ARRAY in the '.backap.yaml' file"); + } + } + } else { + $path = implode('.', $attrParts); + if (!is_array(getValueByPath($this->config, $path))) { + $redable = makeRedableConfigArray($attrParts); + $this->output->displayErrorAndDie("$redable attribute MUST be an ARRAY in the '.backap.yaml' file"); + } + } + } else { + if (!is_array($this->config[$attrParts])) { + $this->output->displayErrorAndDie("$attrParts attribute MUST be an ARRAY in the '.backap.yaml' file"); + } + } + + } + + foreach ($this->stringAttributes as $attrs) { + $attrParts = explode('.', $attrs); + $attrParts = count($attrParts) == 1 ? $attrParts[0] : $attrParts; + + if (is_array($attrParts)) { + if ($attrParts[count($attrParts) - 1] == '*') { + array_pop($attrParts); + $path = implode('.', $attrParts); + foreach (getValueByPath($this->config, $path) as $key => $value) { + if (!is_null($value) && !is_string($value)) { + $redable = makeRedableConfigArray(array_merge($attrParts, [$key])); + $this->output->displayErrorAndDie("$redable attribute MUST be a valid STRING in the '.backap.yaml' file"); + } + } + } else { + $path = implode('.', $attrParts); + $lastValue = getValueByPath($this->config, $path); + if (!is_null($lastValue) && !is_string($lastValue)) { + $redable = makeRedableConfigArray($attrParts); + $this->output->displayErrorAndDie("$redable attribute MUST be a valid STRING in the '.backap.yaml' file"); + } + } + } else { + if (!is_null($this->config[$attrParts]) && !is_string($this->config[$attrParts])) { + $this->output->displayErrorAndDie("[$attrParts] attribute MUST be a valid STRING in the '.backap.yaml' file"); + } + } + + } + + $this->validateDefaultConnectionName(); + } + + protected function validateDefaultConnectionName() + { + if (!in_array($this->config['default_connection'], array_keys($this->config['connections']))) { + $this->output->displayErrorAndDie("[default_connection] attribute MUST be declared on [connections] in the '.backap.yaml' file"); + } + } + + protected function defineConstants() + { + $isStorageDefault = empty($this->config['backap_storage_path']); + + $workingDirStoragePath = WORKING_DIR . DIRECTORY_SEPARATOR . "storage"; + $workingDirDatabasePath = $workingDirStoragePath . DIRECTORY_SEPARATOR . "database"; + + $localStoragePath = $isStorageDefault ? $workingDirDatabasePath : $this->config['backap_storage_path']; + + define("LOCAL_STORAGE_PATH", $localStoragePath); + + define('MYSQLDUMP_PATH', empty($this->config['mysqldump_path']) ? 'mysqldump' : $this->config['mysqldump_path']); + define('MYSQL_PATH', empty($this->config['mysql_path']) ? 'mysql' : $this->config['mysql_path']); + define('ENABLE_COMPRESSION', $this->config['enable_compression'] ? true : false); + define('BACKAP_TIMEZONE', $this->config['timezone'] ? $this->config['timezone'] : 'UTC'); + } + + protected function defineGlobals() + { + $GLOBALS['DB_CONNECTIONS'] = $this->validateDbConnections($this->config['connections']); + $GLOBALS['CLOUD_PROVIDERS'] = $this->validateCloudData($this->config['cloud']); + $GLOBALS['DEFAULT_DB_CONNECTION'] = $this->config['default_connection']; + } + + private function validateDbConnections(array $connections) + { + if (count($connections) < 1) { + $this->output->displayErrorAndDie("There aren't any database connection configured in the '.backap.yaml' file"); + } + + $mandatoryAttributes = ['hostname', 'database', 'username']; + $optionalAttributes = ['port', 'password']; + + foreach ($connections as $connectionName => $connectionParameters) { + foreach (array_merge($mandatoryAttributes, $optionalAttributes) as $parameter) { + if (!array_key_exists($parameter, $connectionParameters)) { + if (in_array($parameter, $mandatoryAttributes)) { + $this->output->displayErrorAndDie(assembleDbEnvVarName([$connectionName, $parameter]) . " MUST BE configured in the '.backap.yaml' file"); + } else { + $connections[$connectionName][$parameter] = ''; + } + } else { + if (in_array($parameter, $mandatoryAttributes) && empty($connectionParameters[$parameter])) { + $this->output->displayErrorAndDie(assembleDbEnvVarName([$connectionName, $parameter]) . " can't be empty in the '.backap.yaml' file"); + } + } + } + } + return $connections; + } + + private function validateCloudData(array $cloud) + { + $cloudAdapters = []; + foreach ($cloud as $adapterName => $adapterParameters) { + if ($adapterParameters['provider'] == 'dropbox') { + $cloudAdapters['dropbox'][$adapterName] = $this->validateDropboxData($adapterParameters); + } + } + return $cloudAdapters; + } + + private function validateDropboxData(array $dropboxData) + { + $mandatoryAttributes = ['access_token', 'app_secret']; + $optionalAttributes = ['path']; + + foreach (array_merge($mandatoryAttributes, $optionalAttributes) as $parameter) { + if (!array_key_exists($parameter, $dropboxData)) { + if (in_array($parameter, $mandatoryAttributes)) { + $this->output->displayErrorAndDie(assembleYamlVarName(['cloud', 'dropbox', $parameter]) . " MUST BE configured in the '.backap.yaml' file"); + } else { + $dropboxData[$parameter] = null; + } + } else { + if (in_array($parameter, $mandatoryAttributes) && empty($dropboxData[$parameter])) { + $this->output->displayErrorAndDie(assembleYamlVarName(['cloud', 'dropbox', $parameter]) . " can't be empty in the '.backap.yaml' file"); + } + } + } + + return $dropboxData; + } +} \ No newline at end of file diff --git a/src/bootstrap.php b/src/bootstrap.php new file mode 100644 index 0000000..2ec8bcc --- /dev/null +++ b/src/bootstrap.php @@ -0,0 +1,12 @@ +