├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml ├── src ├── Schickling │ └── Backup │ │ ├── BackupServiceProvider.php │ │ ├── Commands │ │ ├── BackupCommand.php │ │ ├── BaseCommand.php │ │ └── RestoreCommand.php │ │ ├── Console.php │ │ ├── ConsoleColors.php │ │ ├── DatabaseBuilder.php │ │ └── Databases │ │ ├── DatabaseInterface.php │ │ ├── MySQLDatabase.php │ │ ├── PostgresDatabase.php │ │ └── SqliteDatabase.php └── config │ └── config.php └── tests ├── Commands ├── BackupCommandTest.php ├── RestoreCommandTest.php └── resources │ ├── EmptyFolder │ └── .gitkeep │ └── NonEmptyFolder │ ├── hello.sql │ └── world.sql ├── ConsoleTest.php ├── DatabaseBuilderTest.php └── Databases ├── MySQLDatabaseTest.php ├── PostgresDatabaseTest.php └── SqliteDatabaseTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.phar 3 | composer.lock 4 | .DS_Store -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.3 5 | - 5.4 6 | - 5.5 7 | 8 | before_script: 9 | - composer install --dev 10 | 11 | script: phpunit --coverage-text --coverage-clover ./build/logs/clover.xml 12 | 13 | after_script: php vendor/bin/coveralls -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) {{{year}}} {{{fullname}}} 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | laravel-backup [![Build Status](https://travis-ci.org/schickling/laravel-backup.svg?branch=master)](https://travis-ci.org/schickling/laravel-backup) [![Coverage Status](https://coveralls.io/repos/schickling/laravel-backup/badge.png?branch=master)](https://coveralls.io/r/schickling/laravel-backup?branch=master) [![Total Downloads](https://poser.pugx.org/schickling/backup/downloads.png)](https://packagist.org/packages/schickling/backup) 2 | ============== 3 | 4 | Backup and restore database support for Laravel 4 applications 5 | 6 | ## Installation 7 | 8 | 1. Run the following command: 9 | 10 | ```bash 11 | $ composer require schickling/backup 12 | ``` 13 | 14 | 2. Add `Schickling\Backup\BackupServiceProvider` to your config/app.php 15 | 16 | ## Usage 17 | 18 | #### Backup 19 | Creates a dump file in `app/storage/dumps` 20 | ```sh 21 | $ php artisan db:backup 22 | ``` 23 | 24 | ###### Use specific database 25 | ```sh 26 | $ php artisan db:backup --database=mysql 27 | ``` 28 | 29 | ###### Upload to AWS S3 30 | ```sh 31 | $ php artisan db:backup --upload-s3 your-bucket 32 | ``` 33 | 34 | You can use the `--keep-only-s3` option if you don't want to keep a local copy of the SQL dump. 35 | 36 | Uses the [aws/aws-sdk-php-laravel](https://github.com/aws/aws-sdk-php-laravel) package which needs to be [configured](https://github.com/aws/aws-sdk-php-laravel#configuration). 37 | 38 | #### Restore 39 | Paths are relative to the app/storage/dumps folder. 40 | 41 | ###### Restore a dump 42 | ```sh 43 | $ php artisan db:restore dump.sql 44 | ``` 45 | 46 | ###### List dumps 47 | ```sh 48 | $ php artisan db:restore 49 | ``` 50 | 51 | ## Configuration 52 | Since version `0.5.0` this package follows the recommended standard for configuration. In order to configure this package please run the following command: 53 | 54 | ```sh 55 | $ php artisan config:publish schickling/backup 56 | ``` 57 | 58 | __All settings are optional and have reasonable default values.__ 59 | ```php 60 | 61 | return array( 62 | 63 | // add a backup folder in the app/database/ or your dump folder 64 | 'path' => app_path() . '/database/backup/', 65 | 66 | // add the path to the restore and backup command of mysql 67 | // this exemple is if your are using MAMP server on a mac 68 | // on windows: 'C:\\...\\mysql\\bin\\' 69 | // on linux: '/usr/bin/' 70 | // trailing slash is required 71 | 'mysql' => array( 72 | 'dump_command_path' => '/Applications/MAMP/Library/bin/', 73 | 'restore_command_path' => '/Applications/MAMP/Library/bin/', 74 | ), 75 | 76 | // s3 settings 77 | 's3' => array( 78 | 'path' => 'your/s3/dump/folder' 79 | ) 80 | 81 | // Use GZIP compression 82 | 'compress' => false, 83 | ); 84 | ``` 85 | 86 | ## Dependencies 87 | 88 | #### ...for MySQL 89 | You need to have `mysqldump` installed. It's usually already installed with MySQL itself. 90 | 91 | ## TODO - Upcoming Features 92 | * `db:restore WRONGFILENAME` more detailed error message 93 | * `db:backup FILENAME` set title for dump 94 | * S3 95 | * Upload as default 96 | * default bucket 97 | * More detailed folder checking (permission, existence, ...) 98 | * *Some more ideas? Tell me!* 99 | 100 | 101 | [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/schickling/laravel-backup/trend.png)](https://bitdeli.com/free "Bitdeli Badge") 102 | 103 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "schickling/backup", 3 | "description": "Backup and restore database support for Laravel 4 applications", 4 | "authors": [ 5 | { 6 | "name": "Johannes Schickling", 7 | "email": "schickling.j@gmail.com" 8 | } 9 | ], 10 | "require": { 11 | "php": ">=5.3.0", 12 | "symfony/finder": "2.*", 13 | "aws/aws-sdk-php-laravel": "1.*" 14 | }, 15 | "require-dev": { 16 | "laravel/framework": "4.1.*", 17 | "orchestra/testbench": "2.1.*", 18 | "satooshi/php-coveralls": "0.6.1", 19 | "mockery/mockery": "dev-master" 20 | }, 21 | "autoload": { 22 | "psr-0": { 23 | "Schickling\\Backup": "src/" 24 | } 25 | }, 26 | "minimum-stability": "dev" 27 | } 28 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./tests/ 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Schickling/Backup/BackupServiceProvider.php: -------------------------------------------------------------------------------- 1 | package('schickling/backup'); 15 | } 16 | 17 | /** 18 | * Register the service provider. 19 | * 20 | * @return void 21 | */ 22 | public function register() 23 | { 24 | $databaseBuilder = new DatabaseBuilder(); 25 | 26 | $this->app['db.backup'] = $this->app->share(function($app) use ($databaseBuilder) 27 | { 28 | return new Commands\BackupCommand($databaseBuilder); 29 | }); 30 | 31 | $this->app['db.restore'] = $this->app->share(function($app) use ($databaseBuilder) 32 | { 33 | return new Commands\RestoreCommand($databaseBuilder); 34 | }); 35 | 36 | $this->commands( 37 | 'db.backup', 38 | 'db.restore' 39 | ); 40 | } 41 | } -------------------------------------------------------------------------------- /src/Schickling/Backup/Commands/BackupCommand.php: -------------------------------------------------------------------------------- 1 | getDatabase($this->input->getOption('database')); 19 | $this->checkDumpFolder(); 20 | 21 | if ($this->argument('filename')) 22 | { 23 | // Is it an absolute path? 24 | if (substr($this->argument('filename'), 0, 1) == '/') 25 | { 26 | $this->filePath = $this->argument('filename'); 27 | $this->fileName = basename($this->filePath); 28 | } 29 | // It's relative path? 30 | else 31 | { 32 | $this->filePath = getcwd() . '/' . $this->argument('filename'); 33 | $this->fileName = basename($this->filePath); 34 | } 35 | } 36 | else 37 | { 38 | $this->fileName = date('YmdHis') . '.' .$database->getFileExtension(); 39 | $this->filePath = rtrim($this->getDumpsPath(), '/') . '/' . $this->fileName; 40 | } 41 | 42 | $status = $database->dump($this->filePath); 43 | 44 | if ($status === true) 45 | { 46 | if ($this->isCompressionEnabled()) 47 | { 48 | $this->compress(); 49 | $this->fileName .= ".gz"; 50 | $this->filePath .= ".gz"; 51 | } 52 | if ($this->argument('filename')) 53 | { 54 | $this->line(sprintf($this->colors->getColoredString("\n".'Database backup was successful. Saved to %s'."\n",'green'), $this->filePath)); 55 | } 56 | else 57 | { 58 | $this->line(sprintf($this->colors->getColoredString("\n".'Database backup was successful. %s was saved in the dumps folder.'."\n",'green'), $this->fileName)); 59 | } 60 | 61 | if ($this->option('upload-s3')) 62 | { 63 | $this->uploadS3(); 64 | $this->line($this->colors->getColoredString("\n".'Upload complete.'."\n",'green')); 65 | 66 | if ($this->option('keep-only-s3')) 67 | { 68 | File::delete($this->filePath); 69 | $this->line($this->colors->getColoredString("\n".'Removed dump as it\'s now stored on S3.'."\n",'green')); 70 | } 71 | } 72 | } 73 | else 74 | { 75 | $this->line(sprintf($this->colors->getColoredString("\n".'Database backup failed. %s'."\n",'red'), $status)); 76 | } 77 | } 78 | 79 | /** 80 | * Perform Gzip compression on file 81 | * 82 | * @return boolean Status of command 83 | */ 84 | protected function compress() 85 | { 86 | $command = sprintf('gzip -9 %s', $this->filePath); 87 | return $this->console->run($command); 88 | } 89 | 90 | /** 91 | * Get the console command arguments. 92 | * 93 | * @return array 94 | */ 95 | protected function getArguments() 96 | { 97 | return array( 98 | array('filename', InputArgument::OPTIONAL, 'Filename or -path for the dump.'), 99 | ); 100 | } 101 | 102 | protected function getOptions() 103 | { 104 | return array( 105 | array('database', null, InputOption::VALUE_OPTIONAL, 'The database connection to backup'), 106 | array('upload-s3', 'u', InputOption::VALUE_REQUIRED, 'Upload the dump to your S3 bucket'), 107 | array('keep-only-s3', true, InputOption::VALUE_NONE, 'Delete the local dump after upload to S3 bucket') 108 | ); 109 | } 110 | 111 | protected function checkDumpFolder() 112 | { 113 | $dumpsPath = $this->getDumpsPath(); 114 | 115 | if ( ! is_dir($dumpsPath)) 116 | { 117 | mkdir($dumpsPath); 118 | } 119 | } 120 | 121 | protected function uploadS3() 122 | { 123 | $bucket = $this->option('upload-s3'); 124 | $s3 = AWS::get('s3'); 125 | $s3->putObject(array( 126 | 'Bucket' => $bucket, 127 | 'Key' => $this->getS3DumpsPath() . '/' . $this->fileName, 128 | 'SourceFile' => $this->filePath, 129 | )); 130 | } 131 | 132 | protected function getS3DumpsPath() 133 | { 134 | $default = 'dumps'; 135 | 136 | return Config::get('backup::s3.path', $default);; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/Schickling/Backup/Commands/BaseCommand.php: -------------------------------------------------------------------------------- 1 | databaseBuilder = $databaseBuilder; 20 | $this->colors = new ConsoleColors(); 21 | $this->console = new Console(); 22 | } 23 | 24 | public function getDatabase($database) 25 | { 26 | $database = $database ? : Config::get('database.default'); 27 | $realConfig = Config::get('database.connections.' . $database); 28 | 29 | return $this->databaseBuilder->getDatabase($realConfig); 30 | } 31 | 32 | protected function getDumpsPath() 33 | { 34 | return Config::get('backup::path'); 35 | } 36 | 37 | public function enableCompression() 38 | { 39 | return Config::set('backup::compress', true); 40 | } 41 | 42 | public function disableCompression() 43 | { 44 | return Config::set('backup::compress', false); 45 | } 46 | 47 | public function isCompressionEnabled() 48 | { 49 | return Config::get('backup::compress'); 50 | } 51 | 52 | public function isCompressed($fileName) 53 | { 54 | return pathinfo($fileName, PATHINFO_EXTENSION) === "gz"; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Schickling/Backup/Commands/RestoreCommand.php: -------------------------------------------------------------------------------- 1 | database = $this->getDatabase($this->input->getOption('database')); 17 | 18 | $fileName = $this->argument('dump'); 19 | 20 | if ($fileName) 21 | { 22 | $this->restoreDump($fileName); 23 | } 24 | else 25 | { 26 | $this->listAllDumps(); 27 | } 28 | } 29 | 30 | protected function restoreDump($fileName) 31 | { 32 | $sourceFile = $this->getDumpsPath() . $fileName; 33 | 34 | if ($this->isCompressed($sourceFile)) 35 | $sourceFile = $this->uncompress($sourceFile); 36 | 37 | $status = $this->database->restore($this->getUncompressedFileName($sourceFile)); 38 | 39 | if ($this->isCompressed($sourceFile)) 40 | $this->uncompressCleanup($this->getUncompressedFileName($sourceFile)); 41 | 42 | if ($status === true) 43 | { 44 | $this->line(sprintf($this->colors->getColoredString("\n".'%s was successfully restored.'."\n",'green'), $fileName)); 45 | } 46 | else 47 | { 48 | $this->line($this->colors->getColoredString("\n".'Database restore failed.'."\n",'red')); 49 | } 50 | } 51 | 52 | protected function listAllDumps() 53 | { 54 | $finder = new Finder(); 55 | $finder->files()->in($this->getDumpsPath()); 56 | 57 | if ($finder->count() > 0) 58 | { 59 | $this->line($this->colors->getColoredString("\n".'Please select one of the following dumps:'."\n",'white')); 60 | 61 | $finder->sortByName(); 62 | $count = count($finder); 63 | $i=0; 64 | foreach ($finder as $dump) 65 | { 66 | $i++; 67 | if($i!=$count) 68 | { 69 | $this->line($this->colors->getColoredString($dump->getFilename(),'brown')); 70 | } 71 | else 72 | { 73 | $this->line($this->colors->getColoredString($dump->getFilename()."\n",'brown')); 74 | } 75 | } 76 | } 77 | else 78 | { 79 | $this->line($this->colors->getColoredString("\n".'You haven\'t saved any dumps.'."\n",'brown')); 80 | } 81 | } 82 | 83 | /** 84 | * Uncompress a GZip compressed file 85 | * 86 | * @param string $fileName Relative or absolute path to file 87 | * @return string Name of uncompressed file (without .gz extension) 88 | */ 89 | protected function uncompress($fileName) 90 | { 91 | $fileNameUncompressed = $this->getUncompressedFileName($fileName); 92 | $command = sprintf('gzip -dc %s > %s', $fileName, $fileNameUncompressed); 93 | if ($this->console->run($command) !== true) 94 | $this->line($this->colors->getColoredString("\n".'Uncompress of gzipped file failed.'."\n",'red')); 95 | 96 | return $fileNameUncompressed; 97 | } 98 | 99 | /** 100 | * Remove uncompressed files 101 | * 102 | * Files are temporarily uncompressed for usage in restore. We do not need these copies 103 | * permanently. 104 | * 105 | * @param string $fileName Relative or absolute path to file 106 | * @return boolean Success or failure of cleanup 107 | */ 108 | protected function cleanup($fileName) 109 | { 110 | $status = true; 111 | $fileNameUncompressed = $this->getUncompressedFileName($fileName); 112 | if ($fileName !== $fileNameUncompressed) 113 | $status = File::delete($fileName); 114 | 115 | return $status; 116 | } 117 | 118 | /** 119 | * Retrieve filename without Gzip extension 120 | * 121 | * @param string $fileName Relative or absolute path to file 122 | * @return string Filename without .gz extension 123 | */ 124 | protected function getUncompressedFileName($fileName) 125 | { 126 | return preg_replace('"\.gz$"', '', $fileName); 127 | } 128 | 129 | protected function getArguments() 130 | { 131 | return array( 132 | array('dump', InputArgument::OPTIONAL, 'Filename of the dump') 133 | ); 134 | } 135 | 136 | protected function getOptions() 137 | { 138 | return array( 139 | array('database', null, InputOption::VALUE_OPTIONAL, 'The database connection to restore to'), 140 | ); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/Schickling/Backup/Console.php: -------------------------------------------------------------------------------- 1 | setTimeout(999999999); 11 | $process->run(); 12 | 13 | if ($process->isSuccessful()) 14 | { 15 | return true; 16 | } 17 | else 18 | { 19 | return $process->getErrorOutput(); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Schickling/Backup/ConsoleColors.php: -------------------------------------------------------------------------------- 1 | foreground_colors['black'] = '0;30'; 12 | $this->foreground_colors['dark_gray'] = '1;30'; 13 | $this->foreground_colors['blue'] = '0;34'; 14 | $this->foreground_colors['light_blue'] = '1;34'; 15 | $this->foreground_colors['green'] = '0;32'; 16 | $this->foreground_colors['light_green'] = '1;32'; 17 | $this->foreground_colors['cyan'] = '0;36'; 18 | $this->foreground_colors['light_cyan'] = '1;36'; 19 | $this->foreground_colors['red'] = '0;31'; 20 | $this->foreground_colors['light_red'] = '1;31'; 21 | $this->foreground_colors['purple'] = '0;35'; 22 | $this->foreground_colors['light_purple'] = '1;35'; 23 | $this->foreground_colors['brown'] = '0;33'; 24 | $this->foreground_colors['yellow'] = '1;33'; 25 | $this->foreground_colors['light_gray'] = '0;37'; 26 | $this->foreground_colors['white'] = '1;37'; 27 | 28 | $this->background_colors['black'] = '40'; 29 | $this->background_colors['red'] = '41'; 30 | $this->background_colors['green'] = '42'; 31 | $this->background_colors['yellow'] = '43'; 32 | $this->background_colors['blue'] = '44'; 33 | $this->background_colors['magenta'] = '45'; 34 | $this->background_colors['cyan'] = '46'; 35 | $this->background_colors['light_gray'] = '47'; 36 | } 37 | 38 | // Returns colored string 39 | public function getColoredString($string, $foreground_color = null, $background_color = null) 40 | { 41 | $colored_string = ""; 42 | 43 | // Check if given foreground color found 44 | if (isset($this->foreground_colors[$foreground_color])) { 45 | $colored_string .= "\033[" . $this->foreground_colors[$foreground_color] . "m"; 46 | } 47 | 48 | // Check if given background color found 49 | if (isset($this->background_colors[$background_color])) { 50 | $colored_string .= "\033[" . $this->background_colors[$background_color] . "m"; 51 | } 52 | 53 | // Add string and end coloring 54 | $colored_string .= $string . "\033[0m"; 55 | 56 | return $colored_string; 57 | } 58 | 59 | // Returns all foreground color names 60 | public function getForegroundColors() 61 | { 62 | return array_keys($this->foreground_colors); 63 | } 64 | 65 | // Returns all background color names 66 | public function getBackgroundColors() 67 | { 68 | return array_keys($this->background_colors); 69 | } 70 | } -------------------------------------------------------------------------------- /src/Schickling/Backup/DatabaseBuilder.php: -------------------------------------------------------------------------------- 1 | console = new Console(); 11 | } 12 | 13 | public function getDatabase(array $realConfig) 14 | { 15 | switch ($realConfig['driver']) 16 | { 17 | case 'mysql': 18 | $this->buildMySQL($realConfig); 19 | break; 20 | case 'sqlite': 21 | $this->buildSqlite($realConfig); 22 | break; 23 | case 'pgsql': 24 | $this->buildPostgres($realConfig); 25 | break; 26 | default: 27 | throw new \Exception('Database driver not supported yet'); 28 | break; 29 | } 30 | 31 | return $this->database; 32 | } 33 | 34 | protected function buildMySQL(array $config) 35 | { 36 | $port = isset($config['port']) ? $config['port'] : 3306; 37 | $this->database = new Databases\MySQLDatabase( 38 | $this->console, 39 | $config['database'], 40 | $config['username'], 41 | $config['password'], 42 | $config['host'], 43 | $port 44 | ); 45 | } 46 | 47 | protected function buildSqlite(array $config) 48 | { 49 | $this->database = new Databases\SqliteDatabase( 50 | $this->console, 51 | $config['database'] 52 | ); 53 | } 54 | 55 | protected function buildPostgres(array $config) 56 | { 57 | $this->database = new Databases\PostgresDatabase( 58 | $this->console, 59 | $config['database'], 60 | $config['username'], 61 | $config['password'], 62 | $config['host'] 63 | ); 64 | } 65 | } -------------------------------------------------------------------------------- /src/Schickling/Backup/Databases/DatabaseInterface.php: -------------------------------------------------------------------------------- 1 | console = $console; 18 | $this->database = $database; 19 | $this->user = $user; 20 | $this->password = $password; 21 | $this->host = $host; 22 | $this->port = $port; 23 | } 24 | 25 | public function dump($destinationFile) 26 | { 27 | $command = sprintf('%smysqldump --user=%s --password=%s --host=%s --port=%s %s > %s', 28 | $this->getDumpCommandPath(), 29 | escapeshellarg($this->user), 30 | escapeshellarg($this->password), 31 | escapeshellarg($this->host), 32 | escapeshellarg($this->port), 33 | escapeshellarg($this->database), 34 | escapeshellarg($destinationFile) 35 | ); 36 | 37 | return $this->console->run($command); 38 | } 39 | 40 | public function restore($sourceFile) 41 | { 42 | $command = sprintf('%smysql --user=%s --password=%s --host=%s --port=%s %s < %s', 43 | $this->getRestoreCommandPath(), 44 | escapeshellarg($this->user), 45 | escapeshellarg($this->password), 46 | escapeshellarg($this->host), 47 | escapeshellarg($this->port), 48 | escapeshellarg($this->database), 49 | escapeshellarg($sourceFile) 50 | ); 51 | 52 | return $this->console->run($command); 53 | } 54 | 55 | public function getFileExtension() 56 | { 57 | return 'sql'; 58 | } 59 | 60 | protected function getDumpCommandPath() 61 | { 62 | return Config::get('backup::mysql.dump_command_path');; 63 | } 64 | 65 | protected function getRestoreCommandPath() 66 | { 67 | return Config::get('backup::mysql.restore_command_path');; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Schickling/Backup/Databases/PostgresDatabase.php: -------------------------------------------------------------------------------- 1 | console = $console; 16 | $this->database = $database; 17 | $this->user = $user; 18 | $this->password = $password; 19 | $this->host = $host; 20 | } 21 | 22 | public function dump($destinationFile) 23 | { 24 | $command = sprintf('PGPASSWORD=%s pg_dump -Fc --no-acl --no-owner -h %s -U %s %s > %s', 25 | escapeshellarg($this->password), 26 | escapeshellarg($this->host), 27 | escapeshellarg($this->user), 28 | escapeshellarg($this->database), 29 | escapeshellarg($destinationFile) 30 | ); 31 | 32 | return $this->console->run($command); 33 | } 34 | 35 | public function restore($sourceFile) 36 | { 37 | $command = sprintf('PGPASSWORD=%s pg_restore --verbose --clean --no-acl --no-owner -h %s -U %s -d %s %s', 38 | escapeshellarg($this->password), 39 | escapeshellarg($this->host), 40 | escapeshellarg($this->user), 41 | escapeshellarg($this->database), 42 | escapeshellarg($sourceFile) 43 | ); 44 | 45 | return $this->console->run($command); 46 | } 47 | 48 | public function getFileExtension() 49 | { 50 | return 'dump'; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Schickling/Backup/Databases/SqliteDatabase.php: -------------------------------------------------------------------------------- 1 | console = $console; 13 | $this->databaseFile = $databaseFile; 14 | } 15 | 16 | public function dump($destinationFile) 17 | { 18 | $command = sprintf('cp %s %s', 19 | escapeshellarg($this->databaseFile), 20 | escapeshellarg($destinationFile) 21 | ); 22 | 23 | return $this->console->run($command); 24 | } 25 | 26 | public function restore($sourceFile) 27 | { 28 | $command = sprintf('cp -f %s %s', 29 | escapeshellarg($sourceFile), 30 | escapeshellarg($this->databaseFile) 31 | ); 32 | 33 | return $this->console->run($command); 34 | } 35 | 36 | public function getFileExtension() 37 | { 38 | return 'sqlite'; 39 | } 40 | } -------------------------------------------------------------------------------- /src/config/config.php: -------------------------------------------------------------------------------- 1 | storage_path() . '/dumps/', 4 | 5 | 'mysql' => array( 6 | 'dump_command_path' => '', 7 | 'restore_command_path' => '', 8 | ), 9 | 10 | 's3' => array( 11 | 'path' => '' 12 | ), 13 | 14 | 'compress' => false, 15 | ); 16 | 17 | -------------------------------------------------------------------------------- /tests/Commands/BackupCommandTest.php: -------------------------------------------------------------------------------- 1 | app->config->set('backup::compress', false); 18 | 19 | $this->databaseMock = m::mock('Schickling\Backup\Databases\DatabaseInterface'); 20 | $this->databaseBuilderMock = m::mock('Schickling\Backup\DatabaseBuilder'); 21 | $this->databaseBuilderMock->shouldReceive('getDatabase') 22 | ->once() 23 | ->andReturn($this->databaseMock); 24 | 25 | $command = new BackupCommand($this->databaseBuilderMock); 26 | 27 | $this->tester = new CommandTester($command); 28 | } 29 | 30 | public function tearDown() 31 | { 32 | m::close(); 33 | } 34 | 35 | protected function getPackageProviders() 36 | { 37 | return array( 38 | 'Schickling\Backup\BackupServiceProvider', 39 | 'Aws\Laravel\AwsServiceProvider', 40 | ); 41 | } 42 | 43 | protected function getPackageAliases() 44 | { 45 | return array( 46 | 'AWS' => 'Aws\Laravel\AwsFacade', 47 | ); 48 | } 49 | 50 | public function testSuccessfulUncompressedBackup() 51 | { 52 | $this->databaseMock->shouldReceive('getFileExtension') 53 | ->once() 54 | ->andReturn('sql'); 55 | 56 | $this->databaseMock->shouldReceive('dump') 57 | ->once() 58 | ->andReturn(true); 59 | 60 | $this->tester->execute(array()); 61 | 62 | $this->assertRegExp("/^(\\033\[[0-9;]*m)*(\\n)*Database backup was successful. [0-9]{14}.sql was saved in the dumps folder.(\\n)*(\\033\[0m)*$/", $this->tester->getDisplay()); 63 | } 64 | 65 | public function testSuccessfulCompressedBackup() 66 | { 67 | $this->databaseMock->shouldReceive('getFileExtension') 68 | ->once() 69 | ->andReturn('sql'); 70 | 71 | $this->databaseMock->shouldReceive('dump') 72 | ->once() 73 | ->andReturn(true); 74 | 75 | $this->app->config->set('backup::compress', true); 76 | $this->tester->execute(array()); 77 | 78 | $this->assertRegExp("/^(\\033\[[0-9;]*m)*(\\n)*Database backup was successful. [0-9]{14}.sql.gz was saved in the dumps folder.(\\n)*(\\033\[0m)*$/", $this->tester->getDisplay()); 79 | } 80 | 81 | public function testFailingBackup() 82 | { 83 | $this->databaseMock->shouldReceive('getFileExtension') 84 | ->once() 85 | ->andReturn('sql'); 86 | 87 | $this->databaseMock->shouldReceive('dump') 88 | ->once() 89 | ->andReturn('Error message'); 90 | 91 | $this->tester->execute(array()); 92 | 93 | $this->assertRegExp("/^(\\033\[[0-9;]*m)*(\\n)*Database backup failed. Error message(\\n)*(\\033\[0m)*$/", $this->tester->getDisplay()); 94 | } 95 | 96 | public function testUploadS3() 97 | { 98 | $s3Mock = m::mock(); 99 | $s3Mock->shouldReceive('putObject') 100 | ->andReturn(true); 101 | 102 | AWS::shouldReceive('get') 103 | ->once() 104 | ->with('s3') 105 | ->andReturn($s3Mock); 106 | 107 | $this->databaseMock->shouldReceive('getFileExtension') 108 | ->once() 109 | ->andReturn('sql'); 110 | 111 | $this->databaseMock->shouldReceive('dump') 112 | ->once() 113 | ->andReturn(true); 114 | 115 | $this->tester->execute(array( 116 | '--upload-s3' => 'bucket-title' 117 | )); 118 | } 119 | 120 | public function testKeepOnlyS3() 121 | { 122 | $s3Mock = m::mock(); 123 | $s3Mock->shouldReceive('putObject') 124 | ->andReturn(true); 125 | 126 | AWS::shouldReceive('get') 127 | ->once() 128 | ->with('s3') 129 | ->andReturn($s3Mock); 130 | 131 | File::shouldReceive('delete') 132 | ->once() 133 | ->andReturn(true); 134 | 135 | $this->databaseMock->shouldReceive('getFileExtension') 136 | ->once() 137 | ->andReturn('sql'); 138 | 139 | $this->databaseMock->shouldReceive('dump') 140 | ->once() 141 | ->andReturn(true); 142 | 143 | $this->tester->execute(array( 144 | '--upload-s3' => 'bucket-title', 145 | '--keep-only-s3' => true 146 | )); 147 | } 148 | 149 | public function testAbsolutePathAsFilename() 150 | { 151 | 152 | $this->databaseMock->shouldReceive('getFileExtension') 153 | ->never(); 154 | 155 | $this->databaseMock->shouldReceive('dump') 156 | ->once() 157 | ->andReturn(true); 158 | 159 | $filename = '/home/dummy/mydump.sql'; 160 | 161 | $this->tester->execute(array( 162 | 'filename' => $filename 163 | )); 164 | 165 | $regex = "/^(\\033\[[0-9;]*m)*(\\n)*Database backup was successful. Saved to \/home\/dummy\/mydump.sql(\\n)*(\\033\[0m)*$/"; 166 | $this->assertRegExp($regex, $this->tester->getDisplay()); 167 | } 168 | 169 | public function testRelativePathAsFilename() 170 | { 171 | 172 | $this->databaseMock->shouldReceive('getFileExtension') 173 | ->never(); 174 | 175 | $this->databaseMock->shouldReceive('dump') 176 | ->once() 177 | ->andReturn(true); 178 | 179 | $filename = 'dummy/mydump.sql'; 180 | 181 | $this->tester->execute(array( 182 | 'filename' => $filename 183 | )); 184 | 185 | $path = str_replace('/', '\/', getcwd()); 186 | $regex = "/^(\\033\[[0-9;]*m)*(\\n)*Database backup was successful. Saved to " . $path . "\/dummy\/mydump.sql(\\n)*(\\033\[0m)*$/"; 187 | $this->assertRegExp($regex, $this->tester->getDisplay()); 188 | } 189 | } -------------------------------------------------------------------------------- /tests/Commands/RestoreCommandTest.php: -------------------------------------------------------------------------------- 1 | databaseMock = m::mock('Schickling\Backup\Databases\DatabaseInterface'); 18 | $this->databaseBuilderMock = m::mock('Schickling\Backup\DatabaseBuilder'); 19 | $this->databaseBuilderMock->shouldReceive('getDatabase') 20 | ->once() 21 | ->andReturn($this->databaseMock); 22 | 23 | $command = new RestoreCommand($this->databaseBuilderMock); 24 | 25 | $this->tester = new CommandTester($command); 26 | } 27 | 28 | public function tearDown() 29 | { 30 | m::close(); 31 | } 32 | 33 | protected function getPackageProviders() 34 | { 35 | return array( 36 | 'Schickling\Backup\BackupServiceProvider', 37 | ); 38 | } 39 | 40 | public function testSuccessfulRestore() 41 | { 42 | $testDumpFile = storage_path() . '/dumps/testDump.sql'; 43 | 44 | $this->databaseMock->shouldReceive('restore') 45 | ->with($testDumpFile) 46 | ->once() 47 | ->andReturn(true); 48 | 49 | $this->tester->execute(array( 50 | 'dump' => 'testDump.sql' 51 | )); 52 | 53 | $this->assertRegExp("/^(\\033\[[0-9;]*m)*(\\n)*testDump.sql was successfully restored.(\\n)*(\\033\[0m)*$/", $this->tester->getDisplay()); 54 | } 55 | 56 | public function testFailingRestore() 57 | { 58 | $testDumpFile = storage_path() . '/dumps/testDump.sql'; 59 | 60 | $this->databaseMock->shouldReceive('restore') 61 | ->with($testDumpFile) 62 | ->once() 63 | ->andReturn(false); 64 | 65 | $this->tester->execute(array( 66 | 'dump' => 'testDump.sql' 67 | )); 68 | 69 | $this->assertRegExp("/^(\\033\[[0-9;]*m)*(\\n)*Database restore failed.(\\n)*(\\033\[0m)*$/", $this->tester->getDisplay()); 70 | } 71 | 72 | public function testDumpListForEmptyFolder() 73 | { 74 | $this->app->config->set('database.backup.path', __DIR__ . '/resources/EmptyFolder'); 75 | 76 | $this->tester->execute(array()); 77 | 78 | $this->assertRegExp("/^(\\033\[[0-9;]*m)*(\\n)*You haven't saved any dumps.(\\n)*(\\033\[0m)*$/", $this->tester->getDisplay()); 79 | } 80 | 81 | public function testDumpListForNonEmptyFolder() 82 | { 83 | $this->app->config->set('database.backup.path', __DIR__ . '/resources/NonEmptyFolder'); 84 | 85 | $this->tester->execute(array()); 86 | // Need to find the good regex 87 | //$this->assertRegExp("/^(\\033\[[0-9;]*m)*(\\n)*Please select one of the following dumps:(\\n)*(\\033\[0m)*(\\033\[[0-9;]*m)*(\\n)*hello.sql(\\n)*(\\033\[0m)*(\\033\[[0-9;]*m)*(\\n)*world.sql(\\n)*(\\033\[0m)*$/", $this->tester->getDisplay()); 88 | } 89 | } -------------------------------------------------------------------------------- /tests/Commands/resources/EmptyFolder/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schickling/laravel-backup/5243dc1d9b11390e1eb58202fd07dc3829b1bbe0/tests/Commands/resources/EmptyFolder/.gitkeep -------------------------------------------------------------------------------- /tests/Commands/resources/NonEmptyFolder/hello.sql: -------------------------------------------------------------------------------- 1 | -- MySQL dump 10.13 Distrib 5.1.51, for pc-linux-gnu (i686) 2 | -- 3 | -- Host: 127.0.0.1 Database: world 4 | -- ------------------------------------------------------ 5 | -- Server version 5.1.51-debug-log 6 | 7 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; 8 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; 9 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; 10 | /*!40101 SET NAMES latin1 */; 11 | /*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; 12 | /*!40103 SET TIME_ZONE='+00:00' */; 13 | /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; 14 | /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; 15 | /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; 16 | /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; 17 | 18 | -- 19 | -- Table structure for table `City` 20 | -- 21 | 22 | DROP TABLE IF EXISTS `City`; 23 | /*!40101 SET @saved_cs_client = @@character_set_client */; 24 | /*!40101 SET character_set_client = utf8 */; 25 | CREATE TABLE `City` ( 26 | `ID` int(11) NOT NULL AUTO_INCREMENT, 27 | `Name` char(35) NOT NULL DEFAULT '', 28 | `CountryCode` char(3) NOT NULL DEFAULT '', 29 | `District` char(20) NOT NULL DEFAULT '', 30 | `Population` int(11) NOT NULL DEFAULT '0', 31 | PRIMARY KEY (`ID`) 32 | ) ENGINE=MyISAM AUTO_INCREMENT=4080 DEFAULT CHARSET=latin1; 33 | /*!40101 SET character_set_client = @saved_cs_client */; 34 | -------------------------------------------------------------------------------- /tests/Commands/resources/NonEmptyFolder/world.sql: -------------------------------------------------------------------------------- 1 | -- MySQL dump 10.13 Distrib 5.1.51, for pc-linux-gnu (i686) 2 | -- 3 | -- Host: 127.0.0.1 Database: world 4 | -- ------------------------------------------------------ 5 | -- Server version 5.1.51-debug-log 6 | 7 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; 8 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; 9 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; 10 | /*!40101 SET NAMES latin1 */; 11 | /*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; 12 | /*!40103 SET TIME_ZONE='+00:00' */; 13 | /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; 14 | /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; 15 | /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; 16 | /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; 17 | 18 | -- 19 | -- Table structure for table `City` 20 | -- 21 | 22 | DROP TABLE IF EXISTS `City`; 23 | /*!40101 SET @saved_cs_client = @@character_set_client */; 24 | /*!40101 SET character_set_client = utf8 */; 25 | CREATE TABLE `City` ( 26 | `ID` int(11) NOT NULL AUTO_INCREMENT, 27 | `Name` char(35) NOT NULL DEFAULT '', 28 | `CountryCode` char(3) NOT NULL DEFAULT '', 29 | `District` char(20) NOT NULL DEFAULT '', 30 | `Population` int(11) NOT NULL DEFAULT '0', 31 | PRIMARY KEY (`ID`) 32 | ) ENGINE=MyISAM AUTO_INCREMENT=4080 DEFAULT CHARSET=latin1; 33 | /*!40101 SET character_set_client = @saved_cs_client */; 34 | -------------------------------------------------------------------------------- /tests/ConsoleTest.php: -------------------------------------------------------------------------------- 1 | console = new Console(); 13 | } 14 | 15 | public function testSuccess() 16 | { 17 | $this->assertTrue($this->console->run('true')); 18 | } 19 | 20 | public function testFailure() 21 | { 22 | $this->assertTrue($this->console->run('false') !== true); 23 | } 24 | 25 | } -------------------------------------------------------------------------------- /tests/DatabaseBuilderTest.php: -------------------------------------------------------------------------------- 1 | 'mysql', 12 | 'host' => 'localhost', 13 | 'database' => 'database', 14 | 'username' => 'root', 15 | 'password' => '', 16 | 'port' => '3307', 17 | ); 18 | 19 | $databaseBuilder = new DatabaseBuilder(); 20 | $database = $databaseBuilder->getDatabase($config); 21 | 22 | $this->assertInstanceOf('Schickling\Backup\Databases\MySQLDatabase', $database); 23 | } 24 | 25 | public function testSqlite() 26 | { 27 | $config = array( 28 | 'driver' => 'sqlite', 29 | 'database' => __DIR__.'/../database/production.sqlite', 30 | ); 31 | 32 | $databaseBuilder = new DatabaseBuilder(); 33 | $database = $databaseBuilder->getDatabase($config); 34 | 35 | $this->assertInstanceOf('Schickling\Backup\Databases\SqliteDatabase', $database); 36 | } 37 | 38 | public function testPostgres() { 39 | $config = array( 40 | 'driver' => 'pgsql', 41 | 'host' => 'localhost', 42 | 'database' => 'database', 43 | 'username' => 'root', 44 | 'password' => 'paso', 45 | ); 46 | 47 | $databaseBuilder = new DatabaseBuilder(); 48 | $database = $databaseBuilder->getDatabase($config); 49 | 50 | $this->assertInstanceOf('Schickling\Backup\Databases\PostgresDatabase', $database); 51 | } 52 | 53 | public function testUnsupported() 54 | { 55 | $config = array( 56 | 'driver' => 'unsupported', 57 | ); 58 | 59 | $this->setExpectedException('Exception'); 60 | 61 | $databaseBuilder = new DatabaseBuilder(); 62 | $database = $databaseBuilder->getDatabase($config); 63 | } 64 | } -------------------------------------------------------------------------------- /tests/Databases/MySQLDatabaseTest.php: -------------------------------------------------------------------------------- 1 | console = m::mock('Schickling\Backup\Console'); 15 | $this->database = new MySQLDatabase($this->console, 'testDatabase', 'testUser', 'password', 'localhost', '3306'); 16 | } 17 | 18 | public function tearDown() 19 | { 20 | m::close(); 21 | } 22 | 23 | public function testDump() 24 | { 25 | $this->console->shouldReceive('run') 26 | ->with("mysqldump --user='testUser' --password='password' --host='localhost' --port='3306' 'testDatabase' > 'testfile.sql'") 27 | ->once() 28 | ->andReturn(true); 29 | 30 | $this->assertTrue($this->database->dump('testfile.sql')); 31 | } 32 | 33 | public function testDumpFails() 34 | { 35 | $this->console->shouldReceive('run') 36 | ->with("mysqldump --user='testUser' --password='password' --host='localhost' --port='3306' 'testDatabase' > 'testfile.sql'") 37 | ->once() 38 | ->andReturn(false); 39 | 40 | $this->assertFalse($this->database->dump('testfile.sql')); 41 | } 42 | 43 | public function testRestore() 44 | { 45 | $this->console->shouldReceive('run') 46 | ->with("mysql --user='testUser' --password='password' --host='localhost' --port='3306' 'testDatabase' < 'testfile.sql'") 47 | ->once() 48 | ->andReturn(true); 49 | 50 | $this->assertTrue($this->database->restore('testfile.sql')); 51 | } 52 | 53 | public function testRestoreFails() 54 | { 55 | $this->console->shouldReceive('run') 56 | ->with("mysql --user='testUser' --password='password' --host='localhost' --port='3306' 'testDatabase' < 'testfile.sql'") 57 | ->once() 58 | ->andReturn(false); 59 | 60 | $this->assertFalse($this->database->restore('testfile.sql')); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /tests/Databases/PostgresDatabaseTest.php: -------------------------------------------------------------------------------- 1 | console = m::mock('Schickling\Backup\Console'); 15 | $this->database = new PostgresDatabase($this->console, 'testDatabase', 'testUser', 'password', 'localhost'); 16 | } 17 | 18 | public function tearDown() 19 | { 20 | m::close(); 21 | } 22 | 23 | public function testDump() 24 | { 25 | $this->console->shouldReceive('run') 26 | ->with("PGPASSWORD='password' pg_dump -Fc --no-acl --no-owner -h 'localhost' -U 'testUser' 'testDatabase' > 'testfile.dump'") 27 | ->once() 28 | ->andReturn(true); 29 | 30 | $this->assertTrue($this->database->dump('testfile.dump')); 31 | } 32 | 33 | public function testDumpFails() 34 | { 35 | $this->console->shouldReceive('run') 36 | ->with("PGPASSWORD='password' pg_dump -Fc --no-acl --no-owner -h 'localhost' -U 'testUser' 'testDatabase' > 'testfile.dump'") 37 | ->once() 38 | ->andReturn(false); 39 | 40 | $this->assertFalse($this->database->dump('testfile.dump')); 41 | } 42 | 43 | public function testRestore() 44 | { 45 | $this->console->shouldReceive('run') 46 | ->with("PGPASSWORD='password' pg_restore --verbose --clean --no-acl --no-owner -h 'localhost' -U 'testUser' -d 'testDatabase' 'testfile.dump'") 47 | ->once() 48 | ->andReturn(true); 49 | 50 | $this->assertTrue($this->database->restore('testfile.dump')); 51 | } 52 | 53 | public function testRestoreFails() 54 | { 55 | $this->console->shouldReceive('run') 56 | ->with("PGPASSWORD='password' pg_restore --verbose --clean --no-acl --no-owner -h 'localhost' -U 'testUser' -d 'testDatabase' 'testfile.dump'") 57 | ->once() 58 | ->andReturn(false); 59 | 60 | $this->assertFalse($this->database->restore('testfile.dump')); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /tests/Databases/SqliteDatabaseTest.php: -------------------------------------------------------------------------------- 1 | console = m::mock('Schickling\Backup\Console'); 15 | $this->database = new SqliteDatabase($this->console, 'testDatabase.sqlite'); 16 | } 17 | 18 | public function tearDown() 19 | { 20 | m::close(); 21 | } 22 | 23 | public function testDump() 24 | { 25 | $this->console->shouldReceive('run') 26 | ->with("cp 'testDatabase.sqlite' 'testfile.sqlite'") 27 | ->once() 28 | ->andReturn(true); 29 | 30 | $this->assertTrue($this->database->dump('testfile.sqlite')); 31 | } 32 | 33 | public function testDumpFails() 34 | { 35 | $this->console->shouldReceive('run') 36 | ->with("cp 'testDatabase.sqlite' 'testfile.sqlite'") 37 | ->once() 38 | ->andReturn(false); 39 | 40 | $this->assertFalse($this->database->dump('testfile.sqlite')); 41 | } 42 | 43 | public function testRestore() 44 | { 45 | $this->console->shouldReceive('run') 46 | ->with("cp -f 'testfile.sqlite' 'testDatabase.sqlite'") 47 | ->once() 48 | ->andReturn(true); 49 | 50 | $this->assertTrue($this->database->restore('testfile.sqlite')); 51 | } 52 | 53 | public function testRestoreFails() 54 | { 55 | $this->console->shouldReceive('run') 56 | ->with("cp -f 'testfile.sqlite' 'testDatabase.sqlite'") 57 | ->once() 58 | ->andReturn(false); 59 | 60 | $this->assertFalse($this->database->restore('testfile.sqlite')); 61 | } 62 | 63 | } --------------------------------------------------------------------------------