├── tests ├── assets │ ├── terms.txt │ └── css │ │ ├── app.css │ │ └── components │ │ └── text.css ├── working │ └── .gitignore ├── Remotes │ ├── FtpTest.php │ └── DropboxTest.php ├── Processors │ ├── ArchiveTest.php │ ├── FileTest.php │ ├── CleanupTest.php │ └── DirectoryTest.php ├── Jobs │ ├── FileTest.php │ ├── DirectoryTest.php │ ├── MySQLDatabaseTest.php │ └── PostgreSQLDatabaseTest.php ├── BaseCase.php └── BackupTest.php ├── example ├── files │ ├── css │ │ └── app.css │ └── text.txt ├── .env.example ├── dropbox.php └── ftp.php ├── src └── Backup │ ├── Contracts │ ├── Job.php │ ├── File.php │ ├── Directory.php │ └── Processor.php │ ├── Remotes │ ├── Remote.php │ ├── Dropbox.php │ └── Ftp.php │ ├── Jobs │ ├── MySQLDatabase.php │ ├── PostgreSQLDatabase.php │ ├── File.php │ ├── Job.php │ ├── Directory.php │ ├── Filesystem.php │ └── Database.php │ ├── Processors │ ├── File.php │ ├── Filesystem.php │ ├── Cleanup.php │ ├── Distributor.php │ ├── Archive.php │ ├── Directory.php │ └── Database.php │ └── Backup.php ├── .gitignore ├── .travis.yml ├── phpunit.xml ├── composer.json ├── LICENSE └── README.md /tests/assets/terms.txt: -------------------------------------------------------------------------------- 1 | Terms and Conditions -------------------------------------------------------------------------------- /tests/working/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /example/files/css/app.css: -------------------------------------------------------------------------------- 1 | * { 2 | color: #333; 3 | } -------------------------------------------------------------------------------- /tests/assets/css/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: #333; 3 | background-color: #f9f9f9; 4 | } -------------------------------------------------------------------------------- /tests/assets/css/components/text.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: Arial, Verdana, sans-serif; 3 | } -------------------------------------------------------------------------------- /src/Backup/Contracts/Job.php: -------------------------------------------------------------------------------- 1 | ftpInstance(); 15 | $this->assertInstanceOf(FilesystemOperator::class, $ftp->remote); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/Remotes/DropboxTest.php: -------------------------------------------------------------------------------- 1 | dropboxInstance(); 15 | $this->assertInstanceOf(FilesystemOperator::class, $dropbox->remote); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Backup/Remotes/Dropbox.php: -------------------------------------------------------------------------------- 1 | remote = new Filesystem(new DropboxAdapter($client)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Backup/Jobs/File.php: -------------------------------------------------------------------------------- 1 | fullPath = $fullPath; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./tests/ 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/Backup/Jobs/Job.php: -------------------------------------------------------------------------------- 1 | job = $job; 29 | $this->namespace = $namespace; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Backup/Processors/File.php: -------------------------------------------------------------------------------- 1 | backup->addToCollection( 18 | [ 19 | 'name' => $file->asset(), 20 | 'path' => $file->getFullPath(), 21 | ], 22 | $namespace 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sebastiansulinski/php-backup", 3 | "description": "Simple package for backing up databases, files and directories to Dropbox and Ftp.", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Sebastian Sulinski", 8 | "email": "info@ssdtutorials.com" 9 | } 10 | ], 11 | "minimum-stability": "stable", 12 | "require": { 13 | "php": "^8.2", 14 | "league/flysystem": "^3.0", 15 | "spatie/flysystem-dropbox": "^3.0", 16 | "nesbot/carbon": "^3.8", 17 | "backup-manager/backup-manager": "^3.1", 18 | "illuminate/filesystem": "^11.9", 19 | "league/flysystem-ftp": "^3.0" 20 | }, 21 | "require-dev": { 22 | "phpunit/phpunit": "^11.0.1", 23 | "ext-zip": "*", 24 | "laravel/pint": "^1.18" 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "SSD\\": "src/" 29 | } 30 | }, 31 | "autoload-dev": { 32 | "psr-4": { 33 | "SSDTest\\": "tests/" 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/Processors/ArchiveTest.php: -------------------------------------------------------------------------------- 1 | dropboxInstance(), 20 | $this->working() 21 | ); 22 | $backup->addJob(new Job( 23 | new File( 24 | $this->termsFile(), 25 | $this->assets() 26 | ) 27 | )); 28 | $backup->prepare(); 29 | $backup->processFiles(); 30 | 31 | $archive = new Archive($backup, new ZipArchive); 32 | $archive->execute(); 33 | 34 | $this->addFileToRemove($backup->archivePath()); 35 | 36 | $this->assertFileExists($backup->archivePath()); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Backup/Remotes/Ftp.php: -------------------------------------------------------------------------------- 1 | 21, 16 | 'root' => '', 17 | 'passive' => true, 18 | 'ssl' => false, 19 | 'timeout' => 30, 20 | ]; 21 | 22 | /** 23 | * Ftp constructor. 24 | */ 25 | public function __construct(string $host, string $username, string $password, array $other = []) 26 | { 27 | $this->config['host'] = $host; 28 | $this->config['username'] = $username; 29 | $this->config['password'] = $password; 30 | 31 | if (! empty($other)) { 32 | $this->config = array_replace($this->config, $other); 33 | } 34 | 35 | $this->remote = new Filesystem( 36 | new FtpAdapter(FtpConnectionOptions::fromArray($this->config)) 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Backup/Processors/Filesystem.php: -------------------------------------------------------------------------------- 1 | backup = $backup; 30 | $this->jobs = $jobs; 31 | } 32 | 33 | /** 34 | * Collect items. 35 | */ 36 | public function execute(): void 37 | { 38 | foreach ($this->jobs as $job) { 39 | $this->add($job->job, $job->namespace); 40 | } 41 | } 42 | 43 | /** 44 | * Add to the collection. 45 | * 46 | * @param string $namespace 47 | */ 48 | abstract protected function add(FilesystemJob $resource, $namespace = ''): void; 49 | } 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Sebastian Sulinski 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 | 23 | -------------------------------------------------------------------------------- /example/files/text.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibus. Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra nulla ut metus varius laoreet. Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi. Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero, sit amet adipiscing sem neque sed ipsum. Nam quam nunc, blandit vel, luctus pulvinar, hendrerit id, lorem. Maecenas nec odio et ante tincidunt tempus. Donec vitae sapien ut libero venenatis faucibus. Nullam quis ante. Etiam sit amet orci eget eros faucibus tincidunt. Duis leo. Sed fringilla mauris sit amet nibh. Donec sodales sagittis magna. Sed consequat, leo eget bibendum sodales, augue velit cursus nunc, -------------------------------------------------------------------------------- /tests/Processors/FileTest.php: -------------------------------------------------------------------------------- 1 | dropboxInstance(), 18 | $this->working() 19 | ); 20 | $backup->addJob(new Job( 21 | new FileJob( 22 | $this->cssFile(), 23 | $this->assets() 24 | ), 25 | 'files' 26 | )); 27 | $backup->addJob(new Job( 28 | new FileJob( 29 | $this->termsFile(), 30 | $this->assets() 31 | ), 32 | 'files' 33 | )); 34 | $backup->prepare(); 35 | $backup->processFiles(); 36 | 37 | $this->assertCount( 38 | 1, 39 | $backup->getCollection(), 40 | 'Files are not in the collection' 41 | ); 42 | 43 | $this->assertCount( 44 | 2, 45 | $backup->getCollection()['files'], 46 | 'There are more or less than 2 items in the "files" item of the collection' 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/Processors/CleanupTest.php: -------------------------------------------------------------------------------- 1 | dropboxInstance(), 21 | $this->working() 22 | ); 23 | $backup->addJob(new Job( 24 | new File( 25 | $this->termsFile(), 26 | $this->assets() 27 | ) 28 | )); 29 | $backup->prepare(); 30 | $backup->processFiles(); 31 | 32 | $archive = new Archive($backup, new ZipArchive); 33 | $archive->execute(); 34 | 35 | $this->assertCount( 36 | 1, 37 | $backup->getCollection(), 38 | 'Collection does not contain 1 item' 39 | ); 40 | 41 | $this->assertFileExists($backup->archivePath()); 42 | 43 | $cleanup = new Cleanup($backup); 44 | $cleanup->execute(); 45 | 46 | $this->assertEmpty( 47 | $backup->getCollection(), 48 | 'Collection is not empty' 49 | ); 50 | 51 | $this->assertFileDoesNotExist($backup->archivePath()); 52 | 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Backup/Jobs/Directory.php: -------------------------------------------------------------------------------- 1 | setExclude($exclude); 26 | } 27 | 28 | /** 29 | * Set full path. 30 | */ 31 | public function setFullPath(string $fullPath): void 32 | { 33 | if (! is_dir($fullPath)) { 34 | throw new InvalidArgumentException(sprintf( 35 | '%s is not a valid directory.', $fullPath 36 | )); 37 | } 38 | 39 | $this->fullPath = $fullPath; 40 | } 41 | 42 | /** 43 | * Set directories to be excluded. 44 | */ 45 | private function setExclude(array $exclude = []): void 46 | { 47 | $this->exclude = ! empty($exclude) 48 | ? array_map([$this, 'trimExcludePaths'], $exclude) 49 | : []; 50 | } 51 | 52 | /** 53 | * Trim the directory separator. 54 | */ 55 | private function trimExcludePaths(string $item): string 56 | { 57 | return ltrim($item, DIRECTORY_SEPARATOR); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Backup/Processors/Cleanup.php: -------------------------------------------------------------------------------- 1 | backup = $backup; 23 | } 24 | 25 | /** 26 | * Execute cleanup. 27 | */ 28 | public function execute(): void 29 | { 30 | foreach ($this->backup->getRemoval() as $file) { 31 | 32 | if (! is_file($file)) { 33 | continue; 34 | } 35 | 36 | unlink($file); 37 | } 38 | 39 | $this->backup->resetCollection(); 40 | } 41 | 42 | /** 43 | * Remove old backup files. 44 | */ 45 | public function clearOutdated(): void 46 | { 47 | $files = $this->backup->manager->listContents( 48 | 'remote://'.$this->backup->getRemoteDirectory(), 49 | true 50 | ); 51 | 52 | $count = count($files); 53 | 54 | if (empty($files) || $count <= $this->backup->getNumberOfBackups()) { 55 | return; 56 | } 57 | 58 | $remove = ($count - $this->backup->getNumberOfBackups()); 59 | 60 | asort($files); 61 | 62 | foreach ($files as $key => $file) { 63 | 64 | if (($key + 1) > $remove) { 65 | return; 66 | } 67 | 68 | $this->backup->manager->delete( 69 | 'remote://'.$this->backup->getRemoteDirectory().'/'.$file['basename'] 70 | ); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Backup/Processors/Distributor.php: -------------------------------------------------------------------------------- 1 | backup = $backup; 26 | 27 | $local = new LeagueFilesystem( 28 | new Local( 29 | $this->backup->getLocalWorkingDirectory(), 30 | LOCK_EX, 31 | Local::SKIP_LINKS 32 | ), 33 | [ 34 | 'visibility' => AdapterInterface::VISIBILITY_PUBLIC, 35 | ] 36 | ); 37 | 38 | $this->backup->manager = new MountManager([ 39 | 'local' => $local, 40 | 'remote' => $this->backup->remote->remote, 41 | ]); 42 | } 43 | 44 | /** 45 | * Execute job. 46 | */ 47 | public function execute(): void 48 | { 49 | $remoteDirectory = $this->backup->getRemoteDirectory(); 50 | $archiveFile = $this->backup->getArchiveName(); 51 | 52 | if (! $this->backup->manager->has("remote://{$remoteDirectory}")) { 53 | 54 | $this->backup->manager->createDir("remote://{$remoteDirectory}"); 55 | 56 | } 57 | 58 | $this->backup->manager->move( 59 | 'local://'.$archiveFile, 60 | 'remote://'.$this->backup->getRemoteDirectory().'/'.$archiveFile 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Backup/Jobs/Filesystem.php: -------------------------------------------------------------------------------- 1 | setFullPath($fullPath); 26 | 27 | ! is_null($rootPath) 28 | ? $this->setRootPath($rootPath) 29 | : $this->rootPath = null; 30 | } 31 | 32 | /** 33 | * Extract and return asset path starting from root directory. 34 | */ 35 | public function asset(): string 36 | { 37 | if (is_null($this->rootPath)) { 38 | 39 | $partials = explode('/', $this->fullPath); 40 | 41 | return array_pop($partials); 42 | 43 | } 44 | 45 | return substr($this->fullPath, strlen(rtrim($this->rootPath, '/')) + 1); 46 | } 47 | 48 | /** 49 | * Set full path. 50 | */ 51 | abstract public function setFullPath(string $fullPath): void; 52 | 53 | /** 54 | * Get full path. 55 | */ 56 | public function getFullPath(): string 57 | { 58 | return $this->fullPath; 59 | } 60 | 61 | /** 62 | * Set root path. 63 | */ 64 | public function setRootPath(string $rootPath): void 65 | { 66 | if (! is_dir($rootPath)) { 67 | throw new InvalidArgumentException(sprintf( 68 | '%s is not a valid directory.', $rootPath 69 | )); 70 | } 71 | 72 | $this->rootPath = $rootPath; 73 | } 74 | 75 | /** 76 | * Get root path. 77 | */ 78 | public function getRootPath(): string 79 | { 80 | return $this->rootPath ?? ''; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /tests/Processors/DirectoryTest.php: -------------------------------------------------------------------------------- 1 | dropboxInstance(), 18 | $this->working() 19 | ); 20 | $backup->addJob(new Job( 21 | new DirectoryJob( 22 | $this->cssDirectory(), 23 | $this->assets() 24 | ), 25 | 'directories' 26 | )); 27 | $backup->prepare(); 28 | $backup->processDirectories(); 29 | 30 | $this->assertCount( 31 | 1, 32 | $backup->getCollection(), 33 | 'Directory is not in the collection' 34 | ); 35 | 36 | $this->assertCount( 37 | 3, 38 | $backup->getCollection()['directories'], 39 | 'There are more or less than 3 items in the "directories" item of the collection' 40 | ); 41 | } 42 | 43 | #[Test] 44 | public function adds_directory_to_collection_with_exclusion(): void 45 | { 46 | $backup = new Backup( 47 | $this->dropboxInstance(), 48 | $this->working() 49 | ); 50 | $backup->addJob(new Job( 51 | new DirectoryJob( 52 | $this->cssDirectory(), 53 | $this->assets(), 54 | [ 55 | $this->cssComponentsDirectory(), 56 | ] 57 | ), 58 | 'directories' 59 | )); 60 | $backup->prepare(); 61 | $backup->processDirectories(); 62 | 63 | $this->assertCount( 64 | 1, 65 | $backup->getCollection()['directories'], 66 | 'There is more then 1 item in the "directories" collection' 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /example/dropbox.php: -------------------------------------------------------------------------------- 1 | load(); 20 | $dotenv->required([ 21 | 'DROPBOX_OAUTH', 22 | 'DOMAIN_NAME', 23 | 'DB_HOST', 24 | 'DB_PORT', 25 | 'DB_NAME', 26 | 'DB_USER', 27 | 'DB_PASS', 28 | ]); 29 | 30 | // working directory 31 | $workingDirectory = __DIR__.'/tmp'; 32 | 33 | $remote = new Dropbox( 34 | getenv('DROPBOX_OAUTH') 35 | ); 36 | 37 | $backup = new Backup( 38 | $remote, 39 | $workingDirectory 40 | ); 41 | 42 | // directory to which backup should be saved on the remote server 43 | $backup->setRemoteDirectory(getenv('DOMAIN_NAME')); 44 | 45 | // keep only 7 backups then overwrite the oldest one 46 | $backup->setNumberOfBackups(7); 47 | 48 | // add MySQL database to the backup 49 | $backup->addJob(new Job( 50 | new MySQLDatabase([ 51 | 'host' => getenv('DB_HOST'), 52 | 'name' => getenv('DB_NAME'), 53 | 'user' => getenv('DB_USER'), 54 | 'password' => getenv('DB_PASS'), 55 | ]), 56 | 'database' 57 | )); 58 | 59 | // add single file ot the backup 60 | $backup->addJob(new Job( 61 | new File( 62 | __DIR__.'/files/text.txt', 63 | __DIR__ 64 | ) 65 | )); 66 | 67 | // add the entire directory to the backup 68 | $backup->addJob(new Job( 69 | new Directory( 70 | __DIR__.'/files', 71 | __DIR__, 72 | [ 73 | 'files/css', 74 | ] 75 | ) 76 | )); 77 | 78 | // run backup 79 | $backup->run(); 80 | 81 | } catch (Exception $e) { 82 | 83 | $filesystem = new Filesystem; 84 | 85 | $filesystem->cleanDirectory($workingDirectory); 86 | 87 | $filesystem->prepend( 88 | $workingDirectory.DIRECTORY_SEPARATOR.'error_log', 89 | $e->getMessage().PHP_EOL 90 | ); 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/Backup/Processors/Archive.php: -------------------------------------------------------------------------------- 1 | backup = $backup; 31 | $this->archive = $archive; 32 | } 33 | 34 | /** 35 | * Convert collection to the archive. 36 | */ 37 | public function execute(): void 38 | { 39 | if ( 40 | $this->archive->open( 41 | $this->backup->archivePath(), 42 | ZipArchive::CREATE | ZipArchive::OVERWRITE 43 | ) === true 44 | ) { 45 | 46 | $this->processCollection(); 47 | 48 | } 49 | 50 | $this->archive->close(); 51 | 52 | $this->backup->addToRemoval($this->backup->archivePath()); 53 | } 54 | 55 | /** 56 | * Add files from the collection to the archive. 57 | */ 58 | private function processCollection(): void 59 | { 60 | foreach ($this->backup->getCollection() as $namespace => $items) { 61 | $this->processItemGroup($items, $namespace); 62 | } 63 | } 64 | 65 | /** 66 | * Process item group. 67 | */ 68 | private function processItemGroup(array $items, string $namespace): void 69 | { 70 | foreach ($items as $item) { 71 | 72 | if (is_dir($item['path'])) { 73 | 74 | $this->archive->addEmptyDir($this->namespacedName($item, $namespace)); 75 | 76 | } else { 77 | 78 | $this->archive->addFile( 79 | $item['path'], 80 | $this->namespacedName($item, $namespace) 81 | ); 82 | 83 | } 84 | } 85 | } 86 | 87 | /** 88 | * Prepend namespace to the path. 89 | */ 90 | private function namespacedName(array $item, string $namespace): string 91 | { 92 | return $namespace.'/'.$item['name']; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Backup/Processors/Directory.php: -------------------------------------------------------------------------------- 1 | getRootPath(), 24 | writeFlags: LOCK_EX, 25 | linkHandling: LocalFilesystemAdapter::SKIP_LINKS 26 | ), 27 | config: [ 28 | 'visibility' => Visibility::PUBLIC, 29 | ] 30 | ); 31 | 32 | $collection = $filesystem->listContents($resource->asset(), true); 33 | 34 | $this->addToCollection($resource, $collection, $namespace); 35 | } 36 | 37 | /** 38 | * Add item to collection. 39 | */ 40 | private function addToCollection(FilesystemJob $directory, DirectoryListing $collection, string $namespace = ''): void 41 | { 42 | foreach ($collection as $item) { 43 | 44 | $fullPath = $directory->getRootPath().DIRECTORY_SEPARATOR.$item['path']; 45 | 46 | $trimmedPath = ltrim($item['path'], DIRECTORY_SEPARATOR); 47 | $trimmedFullPath = ltrim($fullPath, DIRECTORY_SEPARATOR); 48 | 49 | if ($this->isExcluded($directory, $trimmedPath, $trimmedFullPath)) { 50 | continue; 51 | } 52 | 53 | $this->backup->addToCollection( 54 | [ 55 | 'name' => $item['path'], 56 | 'path' => $fullPath, 57 | ], 58 | $namespace 59 | ); 60 | } 61 | } 62 | 63 | /** 64 | * Check if a given path is excluded. 65 | */ 66 | private function isExcluded(FilesystemJob $directory, string $path, string $fullPath): bool 67 | { 68 | foreach ($directory->exclude as $excluded) { 69 | 70 | if (str_starts_with($fullPath, $excluded) || str_starts_with($path, $excluded)) { 71 | return true; 72 | } 73 | } 74 | 75 | return false; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /example/ftp.php: -------------------------------------------------------------------------------- 1 | load() 20 | ->required([ 21 | 'FTP_HOST', 22 | 'FTP_USER', 23 | 'FTP_PASS', 24 | 'DOMAIN_NAME', 25 | 'DB_HOST', 26 | 'DB_PORT', 27 | 'DB_NAME', 28 | 'DB_USER', 29 | 'DB_PASS', 30 | ]); 31 | 32 | // working directory 33 | $workingDirectory = __DIR__.'/tmp'; 34 | 35 | $remote = new Ftp( 36 | getenv('FTP_HOST'), 37 | getenv('FTP_USER'), 38 | getenv('FTP_PASS'), 39 | [ 40 | 'root' => 'public_html', 41 | ] 42 | ); 43 | 44 | $backup = new Backup( 45 | $remote, 46 | $workingDirectory 47 | ); 48 | 49 | // directory to which backup should be saved on the remote server 50 | $backup->setRemoteDirectory(getenv('DOMAIN_NAME')); 51 | 52 | // keep only 7 backups then overwrite the oldest one 53 | $backup->setNumberOfBackups(7); 54 | 55 | // add MySQL database to the backup 56 | $backup->addJob(new Job( 57 | new MySQLDatabase([ 58 | 'host' => getenv('DB_HOST'), 59 | 'name' => getenv('DB_NAME'), 60 | 'user' => getenv('DB_USER'), 61 | 'password' => getenv('DB_PASS'), 62 | ]), 63 | 'database' 64 | )); 65 | 66 | // add single file ot the backup 67 | $backup->addJob(new Job( 68 | new File( 69 | __DIR__.'/files/text.txt', 70 | __DIR__ 71 | ) 72 | )); 73 | 74 | // add the entire directory to the backup 75 | $backup->addJob(new Job( 76 | new Directory( 77 | __DIR__.'/files', 78 | __DIR__, 79 | [ 80 | 'files/css', 81 | ] 82 | ) 83 | )); 84 | 85 | // run backup 86 | $backup->run(); 87 | 88 | } catch (Exception $e) { 89 | 90 | $filesystem = new Filesystem; 91 | 92 | $filesystem->cleanDirectory($workingDirectory); 93 | 94 | $filesystem->prepend( 95 | $workingDirectory.DIRECTORY_SEPARATOR.'error_log', 96 | $e->getMessage().PHP_EOL 97 | ); 98 | 99 | } 100 | -------------------------------------------------------------------------------- /tests/Jobs/FileTest.php: -------------------------------------------------------------------------------- 1 | termsFile()); 15 | 16 | $this->assertEquals('terms.txt', $file->asset()); 17 | } 18 | 19 | #[Test] 20 | public function returns_file_name_from_full_path(): void 21 | { 22 | $file = new File( 23 | $this->termsFile(), 24 | $this->assets() 25 | ); 26 | 27 | $this->assertEquals('terms.txt', $file->asset()); 28 | } 29 | 30 | #[Test] 31 | public function returns_file_name_without_its_directory(): void 32 | { 33 | $file = new File($this->cssFile()); 34 | 35 | $this->assertEquals('app.css', $file->asset()); 36 | } 37 | 38 | #[Test] 39 | public function returns_file_name_with_its_directory(): void 40 | { 41 | $file = new File( 42 | $this->cssFile(), 43 | $this->assets() 44 | ); 45 | 46 | $this->assertEquals('css/app.css', $file->asset()); 47 | } 48 | 49 | #[Test] 50 | public function returns_component_file_name_with_its_directories(): void 51 | { 52 | $file = new File( 53 | $this->cssComponentsFile(), 54 | $this->assets() 55 | ); 56 | 57 | $this->assertEquals('css/components/text.css', $file->asset()); 58 | } 59 | 60 | #[Test] 61 | public function returns_component_file_name_with_only_component_directory(): void 62 | { 63 | $file = new File( 64 | $this->cssComponentsFile(), 65 | $this->cssDirectory() 66 | ); 67 | 68 | $this->assertEquals('components/text.css', $file->asset()); 69 | } 70 | 71 | #[Test] 72 | public function returns_full_path(): void 73 | { 74 | $file = new File($this->cssFile()); 75 | 76 | $this->assertEquals($this->cssFile(), $file->getFullPath()); 77 | } 78 | 79 | #[Test] 80 | public function returns_null_for_root_path_without_second_constructor_argument(): void 81 | { 82 | $file = new File($this->cssFile()); 83 | 84 | $this->assertEmpty($file->getRootPath()); 85 | } 86 | 87 | #[Test] 88 | public function returns_root_path_with_second_constructor_argument(): void 89 | { 90 | $file = new File( 91 | $this->cssFile(), 92 | $this->assets() 93 | ); 94 | 95 | $this->assertEquals($this->assets(), $file->getRootPath()); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /tests/Jobs/DirectoryTest.php: -------------------------------------------------------------------------------- 1 | cssDirectory()); 15 | 16 | $this->assertEquals('css', $directory->asset()); 17 | } 18 | 19 | #[Test] 20 | public function returns_component_directory_name_with_parent_directory_without_second_argument(): void 21 | { 22 | $directory = new Directory($this->cssComponentsDirectory()); 23 | 24 | $this->assertEquals('components', $directory->asset()); 25 | } 26 | 27 | #[Test] 28 | public function returns_css_directory_name_from_full_path(): void 29 | { 30 | $directory = new Directory( 31 | $this->cssDirectory(), 32 | $this->assets() 33 | ); 34 | 35 | $this->assertEquals('css', $directory->asset()); 36 | } 37 | 38 | #[Test] 39 | public function returns_component_directory_name_with_parent_directory(): void 40 | { 41 | $directory = new Directory( 42 | $this->cssComponentsDirectory(), 43 | $this->assets() 44 | ); 45 | 46 | $this->assertEquals('css/components', $directory->asset()); 47 | } 48 | 49 | #[Test] 50 | public function returns_component_directory_only(): void 51 | { 52 | $directory = new Directory( 53 | $this->cssComponentsDirectory(), 54 | $this->cssDirectory() 55 | ); 56 | 57 | $this->assertEquals('components', $directory->asset()); 58 | } 59 | 60 | #[Test] 61 | public function returns_null_for_same_full_and_root_path(): void 62 | { 63 | $directory = new Directory( 64 | $this->cssComponentsDirectory(), 65 | $this->cssComponentsDirectory() 66 | ); 67 | 68 | $this->assertEmpty($directory->asset()); 69 | } 70 | 71 | #[Test] 72 | public function empty_exclusions(): void 73 | { 74 | $directory = new Directory( 75 | $this->cssComponentsDirectory(), 76 | $this->cssComponentsDirectory() 77 | ); 78 | 79 | $this->assertEmpty($directory->exclude); 80 | } 81 | 82 | #[Test] 83 | public function exclusions_not_empty(): void 84 | { 85 | $directory = new Directory( 86 | $this->cssDirectory(), 87 | $this->cssDirectory(), 88 | [ 89 | $this->cssComponentsDirectory(), 90 | ] 91 | ); 92 | 93 | $this->assertCount(1, $directory->exclude); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /tests/BaseCase.php: -------------------------------------------------------------------------------- 1 | assets().DIRECTORY_SEPARATOR.'terms.txt'; 43 | } 44 | 45 | /** 46 | * Absolute path to the 'css' directory. 47 | */ 48 | protected function cssDirectory(): string 49 | { 50 | return $this->assets().DIRECTORY_SEPARATOR.'css'; 51 | } 52 | 53 | /** 54 | * Absolute path to the 'app.css' file inside 'css' directory. 55 | */ 56 | protected function cssFile(): string 57 | { 58 | return $this->cssDirectory().DIRECTORY_SEPARATOR.'app.css'; 59 | } 60 | 61 | /** 62 | * Absolute path to the 'css' directory. 63 | */ 64 | protected function cssComponentsDirectory(): string 65 | { 66 | return $this->cssDirectory().DIRECTORY_SEPARATOR.'components'; 67 | } 68 | 69 | /** 70 | * Absolute path to the 'app.css' file inside 'css' directory. 71 | */ 72 | protected function cssComponentsFile(): string 73 | { 74 | return $this->cssComponentsDirectory().DIRECTORY_SEPARATOR.'text.css'; 75 | } 76 | 77 | /** 78 | * Archive path with file name. 79 | */ 80 | protected function archivePath(string $name): string 81 | { 82 | return $this->working().DIRECTORY_SEPARATOR.$name; 83 | } 84 | 85 | /** 86 | * Add file to the removal array. 87 | */ 88 | protected function addFileToRemove(string $file): void 89 | { 90 | $this->removeFiles[] = $file; 91 | } 92 | 93 | /** 94 | * Remove any generated files. 95 | */ 96 | protected function tearDown(): void 97 | { 98 | if (empty($this->removeFiles)) { 99 | return; 100 | } 101 | 102 | foreach ($this->removeFiles as $file) { 103 | if (! is_file($file)) { 104 | continue; 105 | } 106 | unlink($file); 107 | } 108 | } 109 | 110 | /** 111 | * Get Dropbox object instance. 112 | */ 113 | protected function dropboxInstance(): Dropbox 114 | { 115 | return new Dropbox('abc'); 116 | } 117 | 118 | /** 119 | * Get Ftp object instance. 120 | */ 121 | protected function ftpInstance(): Ftp 122 | { 123 | return new Ftp('abc', 'def', 'abc'); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Backup/Processors/Database.php: -------------------------------------------------------------------------------- 1 | backup = $backup; 44 | $this->jobs = $jobs; 45 | $this->workingDir = $workingDir; 46 | } 47 | 48 | /** 49 | * Execute backup. 50 | */ 51 | public function execute(): void 52 | { 53 | foreach ($this->jobs as $job) { 54 | $this->instance($job->job, $job->namespace); 55 | } 56 | } 57 | 58 | /** 59 | * Process single backup job. 60 | */ 61 | private function instance(BaseDatabase $database, string $namespace): void 62 | { 63 | $file = $this->workingDir.'/'.$database->fileName(); 64 | 65 | if (is_file($file)) { 66 | unlink($file); 67 | } 68 | 69 | $filesystems = new Filesystems\FilesystemProvider($this->fileSystemConfig()); 70 | $filesystems->add(new Filesystems\LocalFilesystem); 71 | 72 | $databases = new Databases\DatabaseProvider($this->databaseConfig($database)); 73 | $databases->add(new Databases\MysqlDatabase); 74 | $databases->add(new Databases\PostgresqlDatabase); 75 | 76 | $compressors = new Compressors\CompressorProvider; 77 | $compressors->add(new Compressors\NullCompressor); 78 | 79 | $manager = new Manager($filesystems, $databases, $compressors); 80 | 81 | $manager->makeBackup()->run( 82 | 'config', 83 | [ 84 | new Destination('local', $database->fileName()), 85 | ], 86 | 'null' 87 | ); 88 | 89 | $this->backup->addToCollection( 90 | [ 91 | 'name' => $database->fileName(), 92 | 'path' => $file, 93 | ], 94 | $namespace 95 | ); 96 | 97 | $this->backup->addToRemoval($file); 98 | } 99 | 100 | /** 101 | * Filesystem config. 102 | */ 103 | private function fileSystemConfig(): Config 104 | { 105 | return new Config([ 106 | 'local' => [ 107 | 'type' => 'Local', 108 | 'root' => $this->workingDir, 109 | ], 110 | ]); 111 | } 112 | 113 | /** 114 | * Configuration for a given database type. 115 | */ 116 | private function databaseConfig(BaseDatabase $database): Config 117 | { 118 | return new Config([ 119 | 'config' => [ 120 | 'type' => $database->type(), 121 | 'host' => $database->host, 122 | 'port' => $database->port, 123 | 'user' => $database->user, 124 | 'pass' => $database->password, 125 | 'database' => $database->name, 126 | ], 127 | ]); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Backup/Jobs/Database.php: -------------------------------------------------------------------------------- 1 | setProperties($props); 64 | } 65 | 66 | /** 67 | * Set properties. 68 | */ 69 | public function setProperties(array $props): void 70 | { 71 | $reflection = new ReflectionClass($this); 72 | 73 | foreach ($props as $key => $value) { 74 | 75 | if (! $reflection->hasProperty($key)) { 76 | throw new InvalidArgumentException("Property {$key} is invalid."); 77 | } 78 | 79 | $this->{$key} = $value; 80 | } 81 | } 82 | 83 | /** 84 | * Database type. 85 | */ 86 | abstract public function type(): string; 87 | 88 | /** 89 | * Set database host. 90 | */ 91 | public function setHost(string $host): self 92 | { 93 | $this->host = $host; 94 | 95 | return $this; 96 | } 97 | 98 | /** 99 | * Set database name. 100 | */ 101 | public function setName(string $name): self 102 | { 103 | $this->name = $name; 104 | 105 | return $this; 106 | } 107 | 108 | /** 109 | * Set database username. 110 | */ 111 | public function setUser(string $user): self 112 | { 113 | $this->user = $user; 114 | 115 | return $this; 116 | } 117 | 118 | /** 119 | * Set database password. 120 | */ 121 | public function setPassword(string $password): self 122 | { 123 | $this->password = $password; 124 | 125 | return $this; 126 | } 127 | 128 | /** 129 | * Set database port. 130 | */ 131 | public function setPort(int $port): self 132 | { 133 | $this->port = $port; 134 | 135 | return $this; 136 | } 137 | 138 | /** 139 | * Set export file name. 140 | */ 141 | public function setFileName(string $name): self 142 | { 143 | $this->fileName = $name; 144 | 145 | return $this; 146 | } 147 | 148 | /** 149 | * Get export file name. 150 | */ 151 | public function fileName(): string 152 | { 153 | if (is_null($this->fileName)) { 154 | $this->fileName = $this->name.'_'.Carbon::now()->format('Y-m-d_H-i-s'); 155 | } 156 | 157 | return rtrim($this->fileName, '.sql').'.sql'; 158 | } 159 | 160 | /** 161 | * Check if all properties have values. 162 | */ 163 | public function isValid(): bool 164 | { 165 | return count($this->filterProperties()) === 5; 166 | } 167 | 168 | /** 169 | * Filter non empty properties. 170 | */ 171 | private function filterProperties(): array 172 | { 173 | return array_filter( 174 | [ 175 | $this->host, 176 | $this->name, 177 | $this->user, 178 | $this->password, 179 | $this->port, 180 | ], 181 | function ($item) { 182 | return ! empty($item); 183 | } 184 | ); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /tests/BackupTest.php: -------------------------------------------------------------------------------- 1 | dropboxInstance(), $this->working()); 20 | 21 | $this->assertInstanceOf(Dropbox::class, $backup->remote); 22 | 23 | } 24 | 25 | #[Test] 26 | public function returns_correct_local_directory(): void 27 | { 28 | $backup = new Backup($this->dropboxInstance(), $this->working()); 29 | 30 | $this->assertEquals($this->working(), $backup->getLocalWorkingDirectory()); 31 | 32 | } 33 | 34 | #[Test] 35 | public function sets_and_returns_correct_remote_directory(): void 36 | { 37 | $backup = new Backup($this->dropboxInstance(), $this->working()); 38 | $backup->setRemoteDirectory('test'); 39 | 40 | $this->assertEquals('test', $backup->getRemoteDirectory()); 41 | 42 | } 43 | 44 | #[Test] 45 | public function sets_and_returns_correct_archive_name(): void 46 | { 47 | $backup = new Backup($this->dropboxInstance(), $this->working()); 48 | $backup->setArchiveName('test_archive'); 49 | 50 | $this->assertEquals('test_archive.zip', $backup->getArchiveName()); 51 | 52 | } 53 | 54 | #[Test] 55 | public function returns_correct_archive_path(): void 56 | { 57 | $backup = new Backup($this->dropboxInstance(), $this->working()); 58 | $backup->setArchiveName('test_archive'); 59 | 60 | $this->assertEquals($this->archivePath('test_archive.zip'), $backup->archivePath()); 61 | 62 | } 63 | 64 | #[Test] 65 | public function adds_jobs(): void 66 | { 67 | $backup = new Backup($this->dropboxInstance(), $this->working()); 68 | $backup->addJob(new Job( 69 | new MySQLDatabase([ 70 | 'host' => 'foo', 71 | 'name' => 'bar', 72 | 'user' => 'abc', 73 | 'password' => 'password', 74 | ]), 75 | 'database' 76 | )); 77 | $backup->addJob(new Job( 78 | new PostgreSQLDatabase([ 79 | 'host' => 'foo', 80 | 'name' => 'bar', 81 | 'user' => 'abc', 82 | 'password' => 'password', 83 | ]), 84 | 'database' 85 | )); 86 | $backup->addJob(new Job( 87 | new File( 88 | $this->cssFile(), 89 | $this->cssDirectory() 90 | ) 91 | )); 92 | $backup->addJob(new Job( 93 | new Directory( 94 | $this->cssComponentsDirectory(), 95 | $this->cssDirectory() 96 | ) 97 | )); 98 | 99 | $jobs = $backup->getJobs(); 100 | 101 | $this->assertEquals( 102 | 4, 103 | count($jobs), 104 | 'Number of jobs does not equal 4' 105 | ); 106 | 107 | $this->assertEquals( 108 | 'database', 109 | $jobs[0]->namespace, 110 | 'MySQLDatabase job namespace is not set to "database"' 111 | ); 112 | 113 | $this->assertInstanceOf( 114 | MySQLDatabase::class, 115 | $jobs[0]->job, 116 | 'Job is not instance of MySQLDatabase' 117 | ); 118 | 119 | $this->assertInstanceOf( 120 | PostgreSQLDatabase::class, 121 | $jobs[1]->job, 122 | 'Job is not instance of PostgreSQLDatabase' 123 | ); 124 | 125 | $this->assertEmpty( 126 | $jobs[2]->namespace, 127 | 'File job namespace is not empty' 128 | ); 129 | 130 | $this->assertInstanceOf( 131 | File::class, 132 | $jobs[2]->job, 133 | 'Job is not instance of File' 134 | ); 135 | 136 | $this->assertEmpty( 137 | $jobs[3]->namespace, 138 | 'Directory job namespace is not empty' 139 | ); 140 | 141 | $this->assertInstanceOf( 142 | Directory::class, 143 | $jobs[3]->job, 144 | 'Job is not instance of Directory' 145 | ); 146 | 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /tests/Jobs/MySQLDatabaseTest.php: -------------------------------------------------------------------------------- 1 | '127.0.0.1', 16 | 'name' => 'database_name', 17 | 'user' => 'database_user', 18 | 'password' => 'database_password', 19 | ]); 20 | 21 | $this->assertTrue($database->isValid()); 22 | } 23 | 24 | #[Test] 25 | public function is_valid_returns_true_with_method_populated_config(): void 26 | { 27 | $database = new MySQLDatabase; 28 | $database->setHost('127.0.0.1') 29 | ->setName('database_name') 30 | ->setUser('database_user') 31 | ->setPassword('database_password'); 32 | 33 | $this->assertTrue($database->isValid()); 34 | } 35 | 36 | #[Test] 37 | public function is_valid_returns_true_with_property_assigned_config(): void 38 | { 39 | $database = new MySQLDatabase; 40 | $database->host = '127.0.0.1'; 41 | $database->name = 'database_name'; 42 | $database->user = 'database_user'; 43 | $database->password = 'database_password'; 44 | 45 | $this->assertTrue($database->isValid()); 46 | } 47 | 48 | #[Test] 49 | public function is_valid_returns_false_with_missing_property_assignment(): void 50 | { 51 | $database = new MySQLDatabase; 52 | 53 | $this->assertFalse($database->isValid()); 54 | } 55 | 56 | #[Test] 57 | public function file_name_equals_by_constructor_assignment(): void 58 | { 59 | $database = new MySQLDatabase([ 60 | 'host' => '127.0.0.1', 61 | 'name' => 'database_name', 62 | 'user' => 'database_user', 63 | 'password' => 'database_password', 64 | 'fileName' => 'test_database', 65 | ]); 66 | 67 | $this->assertEquals( 68 | 'test_database.sql', 69 | $database->fileName() 70 | ); 71 | } 72 | 73 | #[Test] 74 | public function file_name_equals_by_property_assignment(): void 75 | { 76 | $database = new MySQLDatabase; 77 | $database->host = '127.0.0.1'; 78 | $database->name = 'database_name'; 79 | $database->user = 'database_user'; 80 | $database->password = 'database_password'; 81 | $database->fileName = 'test_database'; 82 | 83 | $this->assertEquals( 84 | 'test_database.sql', 85 | $database->fileName() 86 | ); 87 | } 88 | 89 | #[Test] 90 | public function file_name_equals_by_mutator(): void 91 | { 92 | $database = new MySQLDatabase; 93 | $database->setHost('127.0.0.1') 94 | ->setName('database_name') 95 | ->setUser('database_user') 96 | ->setPassword('database_password') 97 | ->setFileName('test_database'); 98 | 99 | $this->assertEquals( 100 | 'test_database.sql', 101 | $database->fileName() 102 | ); 103 | } 104 | 105 | #[Test] 106 | public function default_file_name(): void 107 | { 108 | $database = new MySQLDatabase([ 109 | 'host' => '127.0.0.1', 110 | 'name' => 'database_name', 111 | 'user' => 'database_user', 112 | 'password' => 'database_password', 113 | ]); 114 | 115 | $this->assertStringStartsWith( 116 | $database->name.'_'.date('Y-m-d'), 117 | $database->fileName() 118 | ); 119 | } 120 | 121 | #[Test] 122 | public function data_type(): void 123 | { 124 | $database = new MySQLDatabase([ 125 | 'host' => '127.0.0.1', 126 | 'name' => 'database_name', 127 | 'user' => 'database_user', 128 | 'password' => 'database_password', 129 | ]); 130 | 131 | $this->assertEquals( 132 | 'mysql', 133 | $database->type() 134 | ); 135 | } 136 | 137 | #[Test] 138 | public function default_port(): void 139 | { 140 | $database = new MySQLDatabase([ 141 | 'host' => '127.0.0.1', 142 | 'name' => 'database_name', 143 | 'user' => 'database_user', 144 | 'password' => 'database_password', 145 | ]); 146 | 147 | $this->assertEquals( 148 | 3306, 149 | $database->port 150 | ); 151 | } 152 | 153 | #[Test] 154 | public function custom_port_by_constructor_assignment(): void 155 | { 156 | $database = new MySQLDatabase([ 157 | 'host' => '127.0.0.1', 158 | 'name' => 'database_name', 159 | 'user' => 'database_user', 160 | 'password' => 'database_password', 161 | 'port' => 555, 162 | ]); 163 | 164 | $this->assertEquals( 165 | 555, 166 | $database->port 167 | ); 168 | } 169 | 170 | #[Test] 171 | public function custom_port_by_property_assignment(): void 172 | { 173 | $database = new MySQLDatabase; 174 | $database->host = '127.0.0.1'; 175 | $database->name = 'database_name'; 176 | $database->user = 'database_user'; 177 | $database->password = 'database_password'; 178 | $database->port = 555; 179 | 180 | $this->assertEquals( 181 | 555, 182 | $database->port 183 | ); 184 | } 185 | 186 | #[Test] 187 | public function custom_port_by_mutator(): void 188 | { 189 | $database = new MySQLDatabase; 190 | $database->setHost('127.0.0.1') 191 | ->setName('database_name') 192 | ->setUser('database_user') 193 | ->setPassword('database_password') 194 | ->setFileName('test_database') 195 | ->setPort(555); 196 | 197 | $this->assertEquals( 198 | 555, 199 | $database->port 200 | ); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /tests/Jobs/PostgreSQLDatabaseTest.php: -------------------------------------------------------------------------------- 1 | '127.0.0.1', 16 | 'name' => 'database_name', 17 | 'user' => 'database_user', 18 | 'password' => 'database_password', 19 | ]); 20 | 21 | $this->assertTrue($database->isValid()); 22 | } 23 | 24 | #[Test] 25 | public function is_valid_returns_true_with_method_populated_config(): void 26 | { 27 | $database = new PostgreSQLDatabase; 28 | $database->setHost('127.0.0.1') 29 | ->setName('database_name') 30 | ->setUser('database_user') 31 | ->setPassword('database_password'); 32 | 33 | $this->assertTrue($database->isValid()); 34 | } 35 | 36 | #[Test] 37 | public function is_valid_returns_true_with_property_assigned_config(): void 38 | { 39 | $database = new PostgreSQLDatabase; 40 | $database->host = '127.0.0.1'; 41 | $database->name = 'database_name'; 42 | $database->user = 'database_user'; 43 | $database->password = 'database_password'; 44 | 45 | $this->assertTrue($database->isValid()); 46 | } 47 | 48 | #[Test] 49 | public function is_valid_returns_false_with_missing_property_assignment(): void 50 | { 51 | $database = new PostgreSQLDatabase; 52 | 53 | $this->assertFalse($database->isValid()); 54 | } 55 | 56 | #[Test] 57 | public function file_name_equals_by_constructor_assignment(): void 58 | { 59 | $database = new PostgreSQLDatabase([ 60 | 'host' => '127.0.0.1', 61 | 'name' => 'database_name', 62 | 'user' => 'database_user', 63 | 'password' => 'database_password', 64 | 'fileName' => 'test_database', 65 | ]); 66 | 67 | $this->assertEquals( 68 | 'test_database.sql', 69 | $database->fileName() 70 | ); 71 | } 72 | 73 | #[Test] 74 | public function file_name_equals_by_property_assignment(): void 75 | { 76 | $database = new PostgreSQLDatabase; 77 | $database->host = '127.0.0.1'; 78 | $database->name = 'database_name'; 79 | $database->user = 'database_user'; 80 | $database->password = 'database_password'; 81 | $database->fileName = 'test_database'; 82 | 83 | $this->assertEquals( 84 | 'test_database.sql', 85 | $database->fileName() 86 | ); 87 | } 88 | 89 | #[Test] 90 | public function file_name_equals_by_mutator(): void 91 | { 92 | $database = new PostgreSQLDatabase; 93 | $database->setHost('127.0.0.1') 94 | ->setName('database_name') 95 | ->setUser('database_user') 96 | ->setPassword('database_password') 97 | ->setFileName('test_database'); 98 | 99 | $this->assertEquals( 100 | 'test_database.sql', 101 | $database->fileName() 102 | ); 103 | } 104 | 105 | #[Test] 106 | public function default_file_name(): void 107 | { 108 | $database = new PostgreSQLDatabase([ 109 | 'host' => '127.0.0.1', 110 | 'name' => 'database_name', 111 | 'user' => 'database_user', 112 | 'password' => 'database_password', 113 | ]); 114 | 115 | $this->assertStringStartsWith( 116 | $database->name.'_'.date('Y-m-d'), 117 | $database->fileName() 118 | ); 119 | } 120 | 121 | #[Test] 122 | public function data_type(): void 123 | { 124 | $database = new PostgreSQLDatabase([ 125 | 'host' => '127.0.0.1', 126 | 'name' => 'database_name', 127 | 'user' => 'database_user', 128 | 'password' => 'database_password', 129 | ]); 130 | 131 | $this->assertEquals( 132 | 'postgresql', 133 | $database->type() 134 | ); 135 | } 136 | 137 | #[Test] 138 | public function default_port(): void 139 | { 140 | $database = new PostgreSQLDatabase([ 141 | 'host' => '127.0.0.1', 142 | 'name' => 'database_name', 143 | 'user' => 'database_user', 144 | 'password' => 'database_password', 145 | ]); 146 | 147 | $this->assertEquals( 148 | 5432, 149 | $database->port 150 | ); 151 | } 152 | 153 | #[Test] 154 | public function custom_port_by_constructor_assignment(): void 155 | { 156 | $database = new PostgreSQLDatabase([ 157 | 'host' => '127.0.0.1', 158 | 'name' => 'database_name', 159 | 'user' => 'database_user', 160 | 'password' => 'database_password', 161 | 'port' => 555, 162 | ]); 163 | 164 | $this->assertEquals( 165 | 555, 166 | $database->port 167 | ); 168 | } 169 | 170 | #[Test] 171 | public function custom_port_by_property_assignment(): void 172 | { 173 | $database = new PostgreSQLDatabase; 174 | $database->host = '127.0.0.1'; 175 | $database->name = 'database_name'; 176 | $database->user = 'database_user'; 177 | $database->password = 'database_password'; 178 | $database->port = 555; 179 | 180 | $this->assertEquals( 181 | 555, 182 | $database->port 183 | ); 184 | } 185 | 186 | #[Test] 187 | public function custom_port_by_mutator(): void 188 | { 189 | $database = new PostgreSQLDatabase; 190 | $database->setHost('127.0.0.1') 191 | ->setName('database_name') 192 | ->setUser('database_user') 193 | ->setPassword('database_password') 194 | ->setFileName('test_database') 195 | ->setPort(555); 196 | 197 | $this->assertEquals( 198 | 555, 199 | $database->port 200 | ); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP Backup 2 | 3 | A simple package for backing up mysql databases, files and directories to Dropbox and FTP. 4 | 5 | This package makes use of 6 | 7 | - [backup-manager/backup-manager](https://github.com/backup-manager/backup-manager) 8 | - [thephpleague/flysystem](https://github.com/thephpleague/flysystem) 9 | - [ZipArchive](http://php.net/manual/en/class.ziparchive.php) 10 | - [briannesbitt/Carbon](https://github.com/briannesbitt/Carbon) 11 | 12 | ## Usage examples 13 | 14 | You can watch this [video tutorial](https://ssdtutorials.com/courses/dropbox-backup) or read below. 15 | 16 | 17 | ### Backing up to Dropbox and sending Slack notifications along the way 18 | 19 | *To send `slack` notifications, `composer require maknz/slack` and [create an incoming webhook](https://my.slack.com/services/new/incoming-webhook) 20 | 21 | ```php 22 | require "../vendor/autoload.php"; 23 | 24 | use SSD\DotEnv\DotEnv; 25 | use SSD\Backup\Backup; 26 | use SSD\Backup\Jobs\File; 27 | use SSD\Backup\Jobs\Directory; 28 | use SSD\Backup\Remotes\Dropbox; 29 | use SSD\Backup\Jobs\MySQLDatabase; 30 | 31 | use Carbon\Carbon; 32 | use Illuminate\Filesystem\Filesystem; 33 | use Maknz\Slack\Client as SlackClient; 34 | 35 | $dotenv = new DotEnv([__DIR__ . '/.env']); 36 | $dotenv->load(); 37 | $dotenv->required([ 38 | 'DROPBOX_OAUTH', 39 | 'REMOTE_DIR_NAME', 40 | 'DB_HOST', 41 | 'DB_PORT', 42 | 'DB_NAME', 43 | 'DB_USER', 44 | 'DB_PASS' 45 | ]); 46 | 47 | // working directory 48 | $workingDirectory = __DIR__ . '/tmp'; 49 | 50 | // Slack client 51 | $client = new SlackClient('https://hooks.slack.com/your_slack_webhook', [ 52 | 'username' => 'your_slack_username', 53 | 'channel' => '#your_slack_channel', 54 | 'link_names' => true 55 | ]); 56 | 57 | $client->send('Project backup started at: ' . Carbon::now()->toDateTimeString()); 58 | 59 | try { 60 | 61 | $remote = new Dropbox( 62 | getenv('DROPBOX_OAUTH') 63 | ); 64 | 65 | $backup = new Backup( 66 | $remote, 67 | $workingDirectory 68 | ); 69 | 70 | // directory to which backup should be saved on the remote server 71 | $backup->setRemoteDirectory(getenv('REMOTE_DIR_NAME')); 72 | 73 | // keep only 7 backups then overwrite the oldest one 74 | $backup->setNumberOfBackups(7); 75 | 76 | // add MySQL database to the backup 77 | $backup->addJob(new Job( 78 | new MySQLDatabase([ 79 | 'host' => getenv('DB_HOST'), 80 | 'name' => getenv('DB_NAME'), 81 | 'user' => getenv('DB_USER'), 82 | 'password' => getenv('DB_PASS') 83 | ]), 84 | 'database' 85 | )); 86 | 87 | // add single file to the backup 88 | $backup->addJob(new Job( 89 | new File( 90 | __DIR__ . '/files/text.txt', 91 | __DIR__ 92 | ), 93 | 'files' 94 | )); 95 | 96 | // add the 'files' directory to the backup 97 | // but exclude the 'css' directory within 98 | $backup->addJob(new Job( 99 | new Directory( 100 | __DIR__ . '/files', 101 | __DIR__, 102 | [ 103 | 'files/css' 104 | ] 105 | ), 106 | 'files' 107 | )); 108 | 109 | // run backup 110 | $backup->run(); 111 | 112 | } catch (Exception $exception) { 113 | 114 | $client->send('Project backup failed at: ' . Carbon::now()->toDateTimeString() .' with message: "'.$exception->getMessage().'"'); 115 | 116 | $filesystem = new Filesystem; 117 | 118 | $filesystem->cleanDirectory($workingDirectory); 119 | 120 | $filesystem->prepend( 121 | $workingDirectory . DIRECTORY_SEPARATOR . 'error_log', 122 | $exception->getMessage() . PHP_EOL 123 | ); 124 | 125 | } finally { 126 | 127 | $client->send('Project backup finished at: ' . Carbon::now()->toDateTimeString()); 128 | 129 | } 130 | ``` 131 | 132 | ### Backing up to Ftp 133 | 134 | ```php 135 | require "../vendor/autoload.php"; 136 | 137 | use SSD\DotEnv\DotEnv; 138 | use SSD\Backup\Backup; 139 | use SSD\Backup\Jobs\File; 140 | use SSD\Backup\Remotes\Ftp; 141 | use SSD\Backup\Jobs\Directory; 142 | use SSD\Backup\Jobs\MySQLDatabase; 143 | 144 | use Illuminate\Filesystem\Filesystem; 145 | 146 | try { 147 | 148 | $dotenv = new DotEnv([ 149 | __DIR__ . '/.env' 150 | ]); 151 | $dotenv->load(); 152 | $dotenv->required([ 153 | 'FTP_HOST', 154 | 'FTP_USER', 155 | 'FTP_PASS', 156 | 'REMOTE_DIR_NAME', 157 | 'DB_HOST', 158 | 'DB_PORT', 159 | 'DB_NAME', 160 | 'DB_USER', 161 | 'DB_PASS' 162 | ]); 163 | 164 | // working directory 165 | $workingDirectory = __DIR__ . '/tmp'; 166 | 167 | $remote = new Ftp( 168 | getenv('FTP_HOST'), 169 | getenv('FTP_USER'), 170 | getenv('FTP_PASS') 171 | ); 172 | 173 | $backup = new Backup( 174 | $remote, 175 | $workingDirectory 176 | ); 177 | 178 | // directory to which backup should be saved on the remote server 179 | $backup->setRemoteDirectory(getenv('REMOTE_DIR_NAME')); 180 | 181 | // keep only 7 backups then overwrite the oldest one 182 | $backup->setNumberOfBackups(7); 183 | 184 | // add MySQL database to the backup 185 | $backup->addJob(new Job( 186 | new MySQLDatabase([ 187 | 'host' => getenv('DB_HOST'), 188 | 'name' => getenv('DB_NAME'), 189 | 'user' => getenv('DB_USER'), 190 | 'password' => getenv('DB_PASS') 191 | ]), 192 | 'database' 193 | )); 194 | 195 | // add single file to the backup 196 | $backup->addJob(new Job( 197 | new File( 198 | __DIR__ . '/files/text.txt', 199 | __DIR__ 200 | ), 201 | 'files' 202 | )); 203 | 204 | // add the entire directory to the backup 205 | $backup->addJob(new Job( 206 | new Directory( 207 | __DIR__ . '/files/css', 208 | __DIR__ . '/files' 209 | ), 210 | 'files' 211 | )); 212 | 213 | // run backup 214 | $backup->run(); 215 | 216 | } catch (Exception $e) { 217 | 218 | $filesystem = new Filesystem; 219 | 220 | $filesystem->cleanDirectory($workingDirectory); 221 | 222 | $filesystem->prepend( 223 | $workingDirectory . DIRECTORY_SEPARATOR . 'error_log', 224 | $e->getMessage() . PHP_EOL 225 | ); 226 | 227 | } 228 | ``` -------------------------------------------------------------------------------- /src/Backup/Backup.php: -------------------------------------------------------------------------------- 1 | remote = $remote; 119 | $this->localWorkingDir = $localWorkingDir; 120 | } 121 | 122 | /** 123 | * Get path to the local working directory. 124 | */ 125 | public function getLocalWorkingDirectory(): string 126 | { 127 | return $this->localWorkingDir; 128 | } 129 | 130 | /** 131 | * Set remote directory. 132 | */ 133 | public function setRemoteDirectory(string $directory): self 134 | { 135 | $this->remoteBackupDir = $directory; 136 | 137 | return $this; 138 | } 139 | 140 | /** 141 | * Get remote directory name. 142 | */ 143 | public function getRemoteDirectory(): string 144 | { 145 | return $this->remoteBackupDir; 146 | } 147 | 148 | /** 149 | * Set archive name. 150 | */ 151 | public function setArchiveName(string $name): self 152 | { 153 | $this->archiveName = $name; 154 | 155 | return $this; 156 | } 157 | 158 | /** 159 | * Get archive name. 160 | */ 161 | public function getArchiveName(): string 162 | { 163 | if (is_null($this->archiveName)) { 164 | $this->archiveName = Carbon::now()->format('Y-m-d_H-i-s'); 165 | } 166 | 167 | return rtrim($this->archiveName, '.zip').'.zip'; 168 | } 169 | 170 | /** 171 | * Full path to the archive file. 172 | */ 173 | public function archivePath(): string 174 | { 175 | return $this->getLocalWorkingDirectory().DIRECTORY_SEPARATOR.$this->getArchiveName(); 176 | } 177 | 178 | /** 179 | * Add new job. 180 | */ 181 | public function addJob(Job $job): self 182 | { 183 | $this->jobs[] = $job; 184 | 185 | return $this; 186 | } 187 | 188 | /** 189 | * Get all jobs. 190 | */ 191 | public function getJobs(): array 192 | { 193 | return $this->jobs; 194 | } 195 | 196 | /** 197 | * Set number of backups 198 | * before overwriting. 199 | */ 200 | public function setNumberOfBackups(int $number): void 201 | { 202 | $this->noOfBackups = $number; 203 | } 204 | 205 | /** 206 | * Get number of backups. 207 | */ 208 | public function getNumberOfBackups(): int 209 | { 210 | return $this->noOfBackups; 211 | } 212 | 213 | /** 214 | * Execute backup. 215 | */ 216 | public function run(): void 217 | { 218 | $this->prepare(); 219 | 220 | $this->processDatabases(); 221 | 222 | $this->processFiles(); 223 | 224 | $this->processDirectories(); 225 | 226 | if (empty($this->collection)) { 227 | return; 228 | } 229 | 230 | $this->archive(); 231 | 232 | $this->send(); 233 | 234 | $this->cleanup(); 235 | } 236 | 237 | /** 238 | * Validate properties and segregate jobs. 239 | * 240 | * @throws InvalidArgumentException 241 | */ 242 | public function prepare(): void 243 | { 244 | if ( 245 | is_null($this->localWorkingDir) || 246 | ! is_dir($this->localWorkingDir) 247 | ) { 248 | throw new InvalidArgumentException('Invalid local working directory.'); 249 | } 250 | 251 | if (empty($this->jobs)) { 252 | throw new InvalidArgumentException('There are no jobs to process.'); 253 | } 254 | 255 | $this->segregateJobs(); 256 | } 257 | 258 | /** 259 | * Segregate jobs by its implementation. 260 | */ 261 | private function segregateJobs(): void 262 | { 263 | foreach ($this->jobs as $job) { 264 | $this->assignJob($job); 265 | } 266 | } 267 | 268 | /** 269 | * Assign job to the right collection. 270 | * 271 | * @throws InvalidArgumentException 272 | */ 273 | private function assignJob(Job $job): void 274 | { 275 | if ($job->job instanceof Database) { 276 | 277 | $this->databases[] = $job; 278 | 279 | } elseif ($job->job instanceof Directory) { 280 | 281 | $this->directories[] = $job; 282 | 283 | } elseif ($job->job instanceof File) { 284 | 285 | $this->files[] = $job; 286 | 287 | } else { 288 | 289 | throw new InvalidArgumentException('Job does not implement a valid contract.'); 290 | } 291 | } 292 | 293 | /** 294 | * Add item to the collection. 295 | */ 296 | public function addToCollection(array $item, string $namespace): void 297 | { 298 | $this->collection[$namespace][] = $item; 299 | } 300 | 301 | /** 302 | * Get entire collection. 303 | */ 304 | public function getCollection(): array 305 | { 306 | return $this->collection; 307 | } 308 | 309 | /** 310 | * Add file to the removal at clean up. 311 | */ 312 | public function addToRemoval(string $path): void 313 | { 314 | $this->removal[] = $path; 315 | } 316 | 317 | /** 318 | * Get removal collection 319 | */ 320 | public function getRemoval(): array 321 | { 322 | return $this->removal; 323 | } 324 | 325 | /** 326 | * Reset collection array. 327 | */ 328 | public function resetCollection(): void 329 | { 330 | $this->collection = []; 331 | } 332 | 333 | /** 334 | * Export all databases. 335 | */ 336 | public function processDatabases(): void 337 | { 338 | if (empty($this->databases)) { 339 | return; 340 | } 341 | 342 | $database = new DatabaseProcessor( 343 | $this, 344 | $this->databases, 345 | $this->localWorkingDir 346 | ); 347 | 348 | $database->execute(); 349 | } 350 | 351 | /** 352 | * Collect all single files. 353 | */ 354 | public function processFiles(): void 355 | { 356 | if (empty($this->files)) { 357 | return; 358 | } 359 | 360 | $file = new FileProcessor( 361 | $this, 362 | $this->files 363 | ); 364 | 365 | $file->execute(); 366 | } 367 | 368 | /** 369 | * Collect all directories recursively. 370 | */ 371 | public function processDirectories(): void 372 | { 373 | if (empty($this->directories)) { 374 | return; 375 | } 376 | 377 | $directory = new DirectoryProcessor( 378 | $this, 379 | $this->directories 380 | ); 381 | 382 | $directory->execute(); 383 | } 384 | 385 | /** 386 | * Archive collection. 387 | */ 388 | private function archive(): void 389 | { 390 | $archive = new ArchiveProcessor( 391 | $this, 392 | new ZipArchive 393 | ); 394 | 395 | $archive->execute(); 396 | } 397 | 398 | /** 399 | * Send archive to the repository. 400 | */ 401 | private function send(): void 402 | { 403 | $distributor = new DistributorProcessor($this); 404 | 405 | $distributor->execute(); 406 | } 407 | 408 | /** 409 | * Remove all files. 410 | */ 411 | private function cleanup(): void 412 | { 413 | $cleanup = new CleanupProcessor($this); 414 | 415 | $cleanup->execute(); 416 | 417 | if ($this->noOfBackups !== 0) { 418 | $cleanup->clearOutdated(); 419 | } 420 | } 421 | } 422 | --------------------------------------------------------------------------------