├── tests ├── sync-test │ ├── slave │ │ ├── delete-dir │ │ │ ├── me1.txt │ │ │ └── delete-three │ │ │ │ └── huh.txt │ │ └── folder1 │ │ │ ├── on-both.txt │ │ │ ├── slave.txt │ │ │ └── delete.txt │ └── master │ │ ├── create-dir │ │ ├── 1.txt │ │ ├── 3.php │ │ ├── 2.yml │ │ └── 1.json │ │ └── folder1 │ │ ├── master.txt │ │ └── on-both.txt ├── sync-test-seed │ ├── slave │ │ ├── delete-dir │ │ │ ├── me1.txt │ │ │ └── delete-three │ │ │ │ └── huh.txt │ │ └── folder1 │ │ │ ├── on-both.txt │ │ │ ├── slave.txt │ │ │ └── delete.txt │ └── master │ │ ├── create-dir │ │ ├── 1.txt │ │ ├── 3.php │ │ ├── 2.yml │ │ └── 1.json │ │ └── folder1 │ │ ├── master.txt │ │ └── on-both.txt ├── bootstrap.php ├── UtilTest.php ├── TestTrait.php └── SyncTest.php ├── .gitignore ├── phpunit.xml.dist ├── composer.json ├── README.md └── src ├── Sync.php └── Util.php /tests/sync-test/slave/delete-dir/me1.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/sync-test/slave/folder1/on-both.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/sync-test/slave/folder1/slave.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/sync-test-seed/slave/delete-dir/me1.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/sync-test-seed/slave/folder1/on-both.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/sync-test-seed/slave/folder1/slave.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/sync-test/slave/folder1/delete.txt: -------------------------------------------------------------------------------- 1 | DELETE TXT -------------------------------------------------------------------------------- /tests/sync-test-seed/slave/folder1/delete.txt: -------------------------------------------------------------------------------- 1 | DELETE TXT -------------------------------------------------------------------------------- /tests/sync-test/master/create-dir/1.txt: -------------------------------------------------------------------------------- 1 | 2 | Me here 3 | -------------------------------------------------------------------------------- /tests/sync-test-seed/master/create-dir/1.txt: -------------------------------------------------------------------------------- 1 | 2 | Me here 3 | -------------------------------------------------------------------------------- /tests/sync-test/master/folder1/master.txt: -------------------------------------------------------------------------------- 1 | 2 | In master folder1 -------------------------------------------------------------------------------- /tests/sync-test/master/folder1/on-both.txt: -------------------------------------------------------------------------------- 1 | 2 | ON BOTH.txt 3 | -------------------------------------------------------------------------------- /tests/sync-test/slave/delete-dir/delete-three/huh.txt: -------------------------------------------------------------------------------- 1 | HUH??? -------------------------------------------------------------------------------- /tests/sync-test-seed/master/folder1/master.txt: -------------------------------------------------------------------------------- 1 | 2 | In master folder1 -------------------------------------------------------------------------------- /tests/sync-test-seed/master/folder1/on-both.txt: -------------------------------------------------------------------------------- 1 | 2 | ON BOTH.txt 3 | -------------------------------------------------------------------------------- /tests/sync-test-seed/slave/delete-dir/delete-three/huh.txt: -------------------------------------------------------------------------------- 1 | HUH??? -------------------------------------------------------------------------------- /tests/sync-test/master/create-dir/3.php: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | tests 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "thadbryson/flysystem-sync", 3 | "description": "Flysystem wrapper to sync directories.", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Thad Bryson", 8 | "email": "thadbry@gmail.com", 9 | "homepage": "http://thadbryson.co" 10 | } 11 | ], 12 | "require": { 13 | "php": "^7.2 || ^8.0", 14 | "league/flysystem": ">=2.0" 15 | }, 16 | "require-dev": { 17 | "phpunit/phpunit": "^8.5 || ^9.4" 18 | }, 19 | "autoload": { 20 | "psr-4": { 21 | "TCB\\FlysystemSync\\": "src/" 22 | } 23 | }, 24 | "autoload-dev": { 25 | "psr-4": { 26 | "Tests\\": "tests/" 27 | } 28 | }, 29 | "prefer-stable": true 30 | } 31 | -------------------------------------------------------------------------------- /tests/UtilTest.php: -------------------------------------------------------------------------------- 1 | getWrites() 15 | */ 16 | public function testGetWrites(): void 17 | { 18 | $paths = $this->sync->getUtil()->getWrites(); 19 | 20 | $this->assertCount(6, $paths); 21 | $this->assertEquals('create-dir', $paths['create-dir']->path()); 22 | $this->assertEquals('create-dir/1.json', $paths['create-dir/1.json']->path()); 23 | $this->assertEquals('create-dir/1.txt', $paths['create-dir/1.txt']->path()); 24 | $this->assertEquals('create-dir/2.yml', $paths['create-dir/2.yml']->path()); 25 | $this->assertEquals('create-dir/3.php', $paths['create-dir/3.php']->path()); 26 | $this->assertEquals('folder1/master.txt', $paths['folder1/master.txt']->path()); 27 | } 28 | 29 | /** 30 | * Test Sync->getDeletes() 31 | */ 32 | public function testGetDeletes(): void 33 | { 34 | $paths = $this->sync->getUtil()->getDeletes(); 35 | 36 | $this->assertCount(6, $paths); 37 | $this->assertEquals('delete-dir', $paths['delete-dir']->path()); 38 | $this->assertEquals('delete-dir/delete-three', $paths['delete-dir/delete-three']->path()); 39 | $this->assertEquals('delete-dir/delete-three/huh.txt', $paths['delete-dir/delete-three/huh.txt']->path()); 40 | $this->assertEquals('delete-dir/me1.txt', $paths['delete-dir/me1.txt']->path()); 41 | $this->assertEquals('folder1/delete.txt', $paths['folder1/delete.txt']->path()); 42 | $this->assertEquals('folder1/slave.txt', $paths['folder1/slave.txt']->path()); 43 | } 44 | 45 | /** 46 | * Test Sync->getUpdates() 47 | */ 48 | public function testGetUpdates(): void 49 | { 50 | $paths = $this->sync->getUtil()->getUpdates(); 51 | 52 | $this->assertCount(1, $paths); 53 | $this->assertEquals('folder1/on-both.txt', $paths['folder1/on-both.txt']->path()); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/TestTrait.php: -------------------------------------------------------------------------------- 1 | output = __DIR__ . '/sync-test'; 30 | $this->deleteDirectory($this->output); 31 | 32 | $this->copyDir(__DIR__ . '/sync-test-seed', __DIR__ . '/sync-test-seed', $this->output); 33 | 34 | $master = new Filesystem(new Adapter(__DIR__ . '/sync-test/master')); 35 | $slave = new Filesystem(new Adapter(__DIR__ . '/sync-test/slave')); 36 | 37 | $this->sync = new Sync($master, $slave); 38 | } 39 | 40 | /** 41 | * Copy one dir to another. Uses SPL. 42 | */ 43 | protected function copyDir(string $dir, string $src, string $dest): void 44 | { 45 | $files = glob("{$dir}/*"); 46 | 47 | foreach ($files as $file) { 48 | 49 | $destFile = str_replace($src, $dest, $file); 50 | 51 | if (is_dir($file)) { 52 | 53 | if (!is_dir($destFile)) { 54 | mkdir($destFile, 0755, true); 55 | } 56 | 57 | $this->copyDir($file, $src, $dest); 58 | 59 | continue; 60 | } 61 | 62 | copy($file, $destFile); 63 | } 64 | } 65 | 66 | /** 67 | * Delete a dir. Uses SPL. 68 | */ 69 | protected function deleteDirectory(string $dir): void 70 | { 71 | $files = glob("{$dir}/*"); 72 | 73 | foreach ($files as $file) { 74 | 75 | if (is_dir($file) === true) { 76 | $this->deleteDirectory($file); 77 | 78 | rmdir($file); 79 | 80 | continue; 81 | } 82 | 83 | unlink($file); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Flysystem Sync 2 | ============== 3 | 4 | About 5 | ----- 6 | This is a wrapper for [Flysystem](https://github.com/thephpleague/flysystem) version 2 and 3. 7 | Note that v1.* is a plugin for Flysystem v1.*. 8 | 9 | Installation 10 | ------------ 11 | Use Composer: 12 | ``` 13 | "thadbryson/flysystem-sync": ">=2.0" 14 | ``` 15 | 16 | Supports PHP v7.2 and up. 17 | 18 | It helps you sync 2 directories at a time. There are two directory types. 19 | 20 | Master 21 | ------ 22 | Contents of this directory are moved to Slave for writing and updating. If this folder has a path that Slave doesn't then the path on Slave is deleted. 23 | 24 | Slave 25 | ----- 26 | Target directory. Where things are moved to or deleted from. 27 | 28 | How To 29 | ====== 30 | 31 | Here is some example code to set everything up. 32 | 33 | ```php 34 | 35 | use TCB\FlysystemSync\Sync; 36 | 37 | use League\Flysystem\Filesystem; 38 | use League\Flysystem\Local\LocalFilesystemAdapter as Adapter; 39 | 40 | // Setup file system. 41 | $master = new Filesystem(new Adapter(__DIR__ . '/sync-test/master')); 42 | $slave = new Filesystem(new Adapter(__DIR__ . '/sync-test/slave')); 43 | 44 | // Create the Sync object. Use root directory. That '/' variable can be any subpath directory. 45 | $sync = new Sync($master, $slave, $config = [], $directory = '/'); 46 | 47 | // Here is how to actually sync things. 48 | 49 | // Add all folders and files ON MASTER and NOT ON SLAVE. 50 | $sync->syncWrites(); 51 | 52 | // Delete all folders and files NOT ON MASTER and on SLAVE. 53 | $sync->syncDeletes(); 54 | 55 | // Update all folders and files that are on both MASTER and SLAVE. 56 | $sync->syncUpdates(); 57 | 58 | // This will do ->syncWrites(), ->syncDeletes(), and ->syncUpdates(). 59 | $sync->sync(); 60 | 61 | // And you can get what all these paths are going to be separately. 62 | 63 | $paths = $sync->getUtil()->getWrites(); // On Master but not on Slave. 64 | $paths = $sync->getUtil()->getDeletes(); // On Slave but not on Master. 65 | $paths = $sync->getUtil()->getUpdates(); // On both Master and Slave but with different properties. 66 | 67 | ``` 68 | 69 | Example Path Array 70 | ================== 71 | 72 | ``` 73 | array(2) { 74 | 'create-dir/' => \League\Flysystem\DirectoryAttributes, 75 | 'create-dir/3.php' => \League\Flysystem\FileAttributes 76 | } 77 | ``` 78 | 79 | That's an example from a var_dump() of what a path will give you. 80 | 81 | -------------------------------------------------------------------------------- /src/Sync.php: -------------------------------------------------------------------------------- 1 | master = $master; 43 | $this->slave = $slave; 44 | 45 | $this->config = $config; 46 | 47 | $this->util = new Util($master, $slave, $directory); 48 | } 49 | 50 | /** 51 | * Get Util helper object used for getting WRITE, UPDATE, and DELETE paths. 52 | */ 53 | public function getUtil(): Util 54 | { 55 | return $this->util; 56 | } 57 | 58 | /** 59 | * Sync any writes. 60 | * 61 | * @return $this 62 | */ 63 | public function syncWrites(): Sync 64 | { 65 | foreach ($this->util->getWrites() as $path) { 66 | $this->put($path); 67 | } 68 | 69 | return $this; 70 | } 71 | 72 | /** 73 | * Sync any updates. 74 | * 75 | * @return $this 76 | */ 77 | public function syncUpdates(): Sync 78 | { 79 | foreach ($this->util->getUpdates() as $path) { 80 | $this->put($path); 81 | } 82 | 83 | return $this; 84 | } 85 | 86 | /** 87 | * Sync any deletes. 88 | * 89 | * @return $this 90 | */ 91 | public function syncDeletes(): Sync 92 | { 93 | foreach ($this->util->getDeletes() as $path) { 94 | 95 | // A dir delete may of deleted this path already. 96 | if ($path->isFile()) { 97 | $this->slave->delete($path->path()); 98 | } 99 | // A dir? They're deleted a special way. 100 | elseif ($path->isDir()) { 101 | $this->slave->deleteDirectory($path->path()); 102 | } 103 | } 104 | 105 | return $this; 106 | } 107 | 108 | /** 109 | * Call $this->syncWrites(), $this->syncUpdates(), and $this->syncDeletes() 110 | * 111 | * @return $this 112 | */ 113 | public function sync(): Sync 114 | { 115 | return $this 116 | ->syncWrites() 117 | ->syncUpdates() 118 | ->syncDeletes(); 119 | } 120 | 121 | /** 122 | * Call ->put() on $slave. Update/Write content from $master. Also sets visibility on slave. 123 | */ 124 | protected function put(StorageAttributes $path): void 125 | { 126 | // Otherwise create or update the file. 127 | if ($path->isFile()) { 128 | $contents = $this->master->readStream($path->path()); 129 | 130 | $this->slave->writeStream($path->path(), $contents, $this->config); 131 | } 132 | // A dir? Create it. 133 | elseif ($path->isDir()) { 134 | $this->slave->createDirectory($path->path(), $this->config); 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/Util.php: -------------------------------------------------------------------------------- 1 | getPaths($master, $dir); 41 | $slave = $this->getPaths($slave, $dir); 42 | 43 | // Get all file paths. 44 | $all = array_merge( 45 | array_keys($master), 46 | array_keys($slave) 47 | ); 48 | 49 | // Find all WRITE, UPDATE, and DELETE paths. 50 | foreach ($all as $path) { 51 | $on_master = isset($master[$path]) === true; 52 | $on_slave = isset($slave[$path]) === true; 53 | 54 | $path_master = $on_master ? $master[$path] : null; 55 | $path_slave = $on_slave ? $slave[$path] : null; 56 | 57 | // Update: On both and different properties 58 | if ($on_master === true && $on_slave === true && static::isSame($path_master, $path_slave) === false) { 59 | $this->updates[$path] = $path_master; 60 | } 61 | // Write: on Master, not Slave 62 | elseif ($on_master === true && $on_slave === false) { 63 | $this->writes[$path] = $path_master; 64 | } 65 | // Delete: not on Master, on Slave 66 | elseif ($on_master === false && $on_slave === true) { 67 | $this->deletes[$path] = $path_slave; 68 | } 69 | } 70 | } 71 | 72 | /** 73 | * Should these paths be updated? 74 | */ 75 | public static function isSame(StorageAttributes $one, StorageAttributes $two): bool 76 | { 77 | if ($one->path() !== $two->path() || 78 | $one->isDir() !== $two->isDir() || 79 | $one->isFile() !== $two->isFile() || 80 | $one->type() !== $two->type() || 81 | $one->lastModified() !== $two->lastModified() || 82 | $one->visibility() !== $two->visibility()) { 83 | return false; 84 | } 85 | 86 | if ($one instanceof FileAttributes && $two instanceof FileAttributes && $one->isFile() && $two->isFile()) { 87 | return $one->fileSize() === $two->fileSize(); 88 | } 89 | 90 | return true; 91 | } 92 | 93 | /** 94 | * Get paths to WRITE. 95 | * 96 | * @return string[] 97 | */ 98 | public function getWrites(): array 99 | { 100 | return $this->writes; 101 | } 102 | 103 | /** 104 | * Get paths to DELETE. 105 | * 106 | * @return string[] 107 | */ 108 | public function getDeletes(): array 109 | { 110 | return $this->deletes; 111 | } 112 | 113 | /** 114 | * Get paths to UPDATES. 115 | * 116 | * @return string[] 117 | */ 118 | public function getUpdates(): array 119 | { 120 | return $this->updates; 121 | } 122 | 123 | /** 124 | * Get paths on Filesystem. 125 | * 126 | * @return StorageAttributes[] 127 | */ 128 | protected function getPaths(Filesystem $filesystem, string $dir): array 129 | { 130 | $paths = []; 131 | 132 | foreach ($filesystem->listContents($dir, true) as $content) { 133 | 134 | // Use filepath as key for comparison between MASTER and SLAVE. 135 | $paths[$content->path()] = $content; 136 | } 137 | 138 | // Sort by key (filepath). 139 | ksort($paths); 140 | 141 | return $paths; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /tests/SyncTest.php: -------------------------------------------------------------------------------- 1 | syncWrites() 13 | */ 14 | public function testSyncWrites(): void 15 | { 16 | $this->sync->syncWrites(); 17 | 18 | $this->assertTrue(is_dir($this->output . '/slave/create-dir')); 19 | $this->assertTrue(is_file($this->output . '/slave/create-dir/1.json')); 20 | $this->assertTrue(is_file($this->output . '/slave/create-dir/1.txt')); 21 | $this->assertTrue(is_file($this->output . '/slave/create-dir/2.yml')); 22 | $this->assertTrue(is_file($this->output . '/slave/create-dir/3.php')); 23 | $this->assertTrue(is_dir($this->output . '/slave/delete-dir')); 24 | $this->assertTrue(is_dir($this->output . '/slave/delete-dir/delete-three')); 25 | $this->assertTrue(is_file($this->output . '/slave/delete-dir/delete-three/huh.txt')); 26 | $this->assertTrue(is_file($this->output . '/slave/delete-dir/me1.txt')); 27 | $this->assertTrue(is_dir($this->output . '/slave/folder1')); 28 | $this->assertTrue(is_file($this->output . '/slave/folder1/delete.txt')); 29 | $this->assertTrue(is_file($this->output . '/slave/folder1/master.txt')); 30 | $this->assertTrue(is_file($this->output . '/slave/folder1/on-both.txt')); 31 | } 32 | 33 | /** 34 | * Test Sync->syncDeletes() 35 | */ 36 | public function testSyncDeletes(): void 37 | { 38 | $this->sync->syncDeletes(); 39 | 40 | $this->assertFalse(is_dir($this->output . '/slave/create-dir')); 41 | $this->assertFalse(is_file($this->output . '/slave/create-dir/1.json')); 42 | $this->assertFalse(is_file($this->output . '/slave/create-dir/1.txt')); 43 | $this->assertFalse(is_file($this->output . '/slave/create-dir/2.yml')); 44 | $this->assertFalse(is_file($this->output . '/slave/create-dir/3.php')); 45 | $this->assertFalse(is_dir($this->output . '/slave/delete-dir')); 46 | $this->assertFalse(is_dir($this->output . '/slave/delete-dir/delete-three')); 47 | $this->assertFalse(is_file($this->output . '/slave/delete-dir/delete-three/huh.txt')); 48 | $this->assertFalse(is_dir($this->output . '/slave/delete-dir/delete-too')); 49 | $this->assertFalse(is_file($this->output . '/slave/delete-dir/me1.txt')); 50 | $this->assertTrue(is_dir($this->output . '/slave/folder1')); 51 | $this->assertFalse(is_file($this->output . '/slave/folder1/delete.txt')); 52 | $this->assertFalse(is_file($this->output . '/slave/folder1/master.txt')); 53 | $this->assertTrue(is_file($this->output . '/slave/folder1/on-both.txt')); 54 | } 55 | 56 | /** 57 | * Test Sync->syncUpdates() 58 | */ 59 | public function testSyncUpdates(): void 60 | { 61 | $this->sync->syncUpdates(); 62 | 63 | $this->assertFalse(is_dir($this->output . '/slave/create-dir')); 64 | $this->assertFalse(is_file($this->output . '/slave/create-dir/1.json')); 65 | $this->assertFalse(is_file($this->output . '/slave/create-dir/1.txt')); 66 | $this->assertFalse(is_file($this->output . '/slave/create-dir/2.yml')); 67 | $this->assertFalse(is_file($this->output . '/slave/create-dir/3.php')); 68 | $this->assertTrue(is_dir($this->output . '/slave/delete-dir')); 69 | $this->assertTrue(is_dir($this->output . '/slave/delete-dir/delete-three')); 70 | $this->assertTrue(is_file($this->output . '/slave/delete-dir/delete-three/huh.txt')); 71 | $this->assertTrue(is_file($this->output . '/slave/delete-dir/me1.txt')); 72 | $this->assertTrue(is_dir($this->output . '/slave/folder1')); 73 | $this->assertTrue(is_file($this->output . '/slave/folder1/delete.txt')); 74 | $this->assertFalse(is_file($this->output . '/slave/folder1/master.txt')); 75 | $this->assertTrue(is_file($this->output . '/slave/folder1/on-both.txt')); 76 | } 77 | 78 | /** 79 | * Test Sync->sync() 80 | */ 81 | public function testSync(): void 82 | { 83 | $this->sync->sync(); 84 | 85 | $this->assertTrue(is_dir($this->output . '/slave/create-dir')); 86 | $this->assertTrue(is_file($this->output . '/slave/create-dir/1.json')); 87 | $this->assertTrue(is_file($this->output . '/slave/create-dir/1.txt')); 88 | $this->assertTrue(is_file($this->output . '/slave/create-dir/2.yml')); 89 | $this->assertTrue(is_file($this->output . '/slave/create-dir/3.php')); 90 | $this->assertFalse(is_dir($this->output . '/slave/delete-dir')); 91 | $this->assertFalse(is_dir($this->output . '/slave/delete-dir/delete-three')); 92 | $this->assertFalse(is_file($this->output . '/slave/delete-dir/delete-three/huh.txt')); 93 | $this->assertFalse(is_dir($this->output . '/slave/delete-dir/delete-too')); 94 | $this->assertFalse(is_file($this->output . '/slave/delete-dir/me1.txt')); 95 | $this->assertTrue(is_dir($this->output . '/slave/folder1')); 96 | $this->assertFalse(is_file($this->output . '/slave/folder1/delete.txt')); 97 | $this->assertTrue(is_file($this->output . '/slave/folder1/master.txt')); 98 | $this->assertTrue(is_file($this->output . '/slave/folder1/on-both.txt')); 99 | } 100 | } 101 | --------------------------------------------------------------------------------