├── .gitignore ├── .travis.yml ├── README.md ├── composer.json ├── phpunit.xml.dist ├── src └── PDOAdapter.php └── tests ├── Fixtures └── files │ ├── bar.jpg │ └── foo.txt ├── PDOAdapterTest.php └── bootstrap.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | composer.lock 3 | 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.6 5 | - 7.0 6 | - 7.1 7 | - 7.2 8 | 9 | install: 10 | - composer install -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Flysystem PDO Database Adapter 2 | ============================== 3 | 4 | [![Build Status](https://img.shields.io/travis/IntegralSoftware/flysystem-pdo-adapter/master.svg?style=flat-square)](https://travis-ci.org/IntegralSoftware/flysystem-pdo-adapter) 5 | [![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](https://opensource.org/licenses/MIT) 6 | 7 | PDO database adapter for [Flysystem](https://github.com/thephpleague/flysystem) filesystem abstraction. No additional dependencies, only PDO extension required. 8 | 9 | ## Installation 10 | 11 | ``` 12 | composer require integral/flysystem-pdo-adapter 13 | ``` 14 | 15 | ## Database Configuration 16 | 17 | At the beginning you have to create a table that will be used to store files. 18 | 19 | SQL table schema examples for MySQL, SQLite and PostgreSQL are presented below. 20 | 21 | **MySQL** 22 | ```sql 23 | CREATE TABLE files ( 24 | id int(11) NOT NULL AUTO_INCREMENT, 25 | path varchar(255) NOT NULL, 26 | type enum('file','dir') NOT NULL, 27 | contents longblob, 28 | size int(11) NOT NULL DEFAULT 0, 29 | mimetype varchar(127), 30 | timestamp int(11) NOT NULL DEFAULT 0, 31 | PRIMARY KEY (id), 32 | UNIQUE KEY path_unique (path) 33 | ); 34 | ``` 35 | 36 | **SQLite** 37 | ```sql 38 | CREATE TABLE files ( 39 | id INTEGER PRIMARY KEY, 40 | path TEXT NOT NULL UNIQUE, 41 | type TEXT NOT NULL, 42 | contents BLOB, 43 | size INTEGER NOT NULL DEFAULT 0, 44 | mimetype TEXT, 45 | timestamp INTEGER NOT NULL DEFAULT 0 46 | ) 47 | ``` 48 | 49 | **PostgreSQL** 50 | ```sql 51 | CREATE TABLE public.files ( 52 | id serial NOT NULL, 53 | path varchar(255) NOT NULL, 54 | type varchar(4) NOT NULL, 55 | contents bytea, 56 | size integer NOT NULL DEFAULT 0, 57 | mimetype varchar(127), 58 | "timestamp" integer NOT NULL DEFAULT 0, 59 | is_compressed boolean NOT NULL DEFAULT true, 60 | update_ts timestamp(0) with time zone DEFAULT NOW(), 61 | CONSTRAINT files_pkey PRIMARY KEY (id), 62 | CONSTRAINT type_check CHECK (type='dir' or type='file'), 63 | CONSTRAINT path_unique UNIQUE (path) 64 | ); 65 | ``` 66 | 67 | ## Usage 68 | 69 | Create an adapter by passing a valid `PDO` object and table name as constructor arguments: 70 | 71 | **MySQL** 72 | ```php 73 | // http://php.net/manual/pl/ref.pdo-mysql.connection.php 74 | $pdo = new PDO('mysql:host=hostname;dbname=database_name', 'username', 'password'); 75 | $adapter = new PDOAdapter($pdo, 'files'); 76 | ``` 77 | 78 | **SQLite** 79 | ```php 80 | // http://php.net/manual/pl/ref.pdo-sqlite.connection.php 81 | $pdo = new PDO('sqlite:/absolute/path/to/database.sqlite'); 82 | $adapter = new PDOAdapter($pdo, 'files'); 83 | ``` 84 | 85 | **PostgreSQL** 86 | ```php 87 | // http://php.net/manual/pl/ref.pdo-pgsql.php 88 | $pdo = new PDO('pgsql:host=localhost;port=5432;dbname=testdb;user=bruce;password=mypass'); 89 | $adapter = new PDOAdapter($pdo, 'public.files'); 90 | ``` 91 | 92 | Then simply pass the created adapter to `\League\Flysystem\Filesystem`: 93 | 94 | ```php 95 | $filesystem = new Filesystem($adapter); 96 | ``` 97 | 98 | Done! At this point the `$filesystem` is ready to use. 99 | 100 | ## Note 101 | 102 | This implementation emulates a tree structured filesystem, therefore some of the operations 103 | (like renaming or deleting a folder) produce quite a lot of database queries, which may result 104 | in a poor performance for some scenarios. 105 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "integral/flysystem-pdo-adapter", 3 | "description": "PDO adapter for Flysystem file abstraction layer", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Pawel Wiaderny", 8 | "email": "pawel.wiaderny@gmail.com" 9 | } 10 | ], 11 | "require": { 12 | "league/flysystem": "^1.0", 13 | "ext-pdo": "*" 14 | }, 15 | "require-dev": { 16 | "phpunit/phpunit": "~4.8" 17 | }, 18 | "autoload": { 19 | "psr-4": { 20 | "Integral\\Flysystem\\Adapter\\": "src/" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ./tests/ 7 | 8 | 9 | 10 | 11 | src 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/PDOAdapter.php: -------------------------------------------------------------------------------- 1 | pdo = $pdo; 38 | 39 | if (!preg_match('/^[a-zA-Z_][a-zA-Z0-9_.]*$/', $tableName)) { 40 | throw new \InvalidArgumentException('Invalid table name'); 41 | } 42 | 43 | $this->setPathPrefix($pathPrefix); 44 | 45 | $this->table = $tableName; 46 | } 47 | 48 | /** 49 | * {@inheritdoc} 50 | */ 51 | public function write($path, $contents, Config $config) 52 | { 53 | $statement = $this->pdo->prepare( 54 | "INSERT INTO {$this->table} (path, contents, size, type, mimetype, timestamp) VALUES(:path, :contents, :size, :type, :mimetype, :timestamp)" 55 | ); 56 | 57 | $size = strlen($contents); 58 | $type = 'file'; 59 | $mimetype = Util::guessMimeType($path, $contents); 60 | $timestamp = $config->get('timestamp', time()); 61 | 62 | $pathWithPrefix = $this->applyPathPrefix($path); 63 | 64 | $statement->bindParam(':path', $pathWithPrefix, PDO::PARAM_STR); 65 | $statement->bindParam(':contents', $contents, PDO::PARAM_LOB); 66 | $statement->bindParam(':size', $size, PDO::PARAM_INT); 67 | $statement->bindParam(':type', $type, PDO::PARAM_STR); 68 | $statement->bindParam(':mimetype', $mimetype, PDO::PARAM_STR); 69 | $statement->bindParam(':timestamp', $timestamp, PDO::PARAM_INT); 70 | 71 | return $statement->execute() ? compact('path', 'contents', 'size', 'type', 'mimetype', 'timestamp') : false; 72 | } 73 | 74 | /** 75 | * {@inheritdoc} 76 | */ 77 | public function writeStream($path, $resource, Config $config) 78 | { 79 | $statement = $this->pdo->prepare( 80 | "INSERT INTO {$this->table} (path, contents, size, type, mimetype, timestamp) VALUES(:path, :contents, :size, :type, :mimetype, :timestamp)" 81 | ); 82 | 83 | $size = 0; // see below 84 | $type = 'file'; 85 | $mimetype = Util::guessMimeType($path, ''); 86 | $timestamp = $config->get('timestamp', time()); 87 | 88 | $pathWithPrefix = $this->applyPathPrefix($path); 89 | 90 | $statement->bindParam(':path', $pathWithPrefix, PDO::PARAM_STR); 91 | $statement->bindParam(':contents', $resource, PDO::PARAM_LOB); 92 | $statement->bindParam(':size', $size, PDO::PARAM_INT); 93 | $statement->bindParam(':type', $type, PDO::PARAM_STR); 94 | $statement->bindParam(':mimetype', $mimetype, PDO::PARAM_STR); 95 | $statement->bindParam(':timestamp', $timestamp, PDO::PARAM_INT); 96 | $output = $statement->execute() ? compact('path', 'type', 'mimetype', 'timestamp') : false; 97 | 98 | if ($output) { 99 | // Correct the size afterwards 100 | // It seems all drivers are happy with LENGTH(binary) 101 | $statement = $this->pdo->prepare("UPDATE {$this->table} SET size = LENGTH(contents) WHERE path = :path"); 102 | $statement->bindParam(':path', $pathWithPrefix, PDO::PARAM_STR); 103 | $statement->execute(); 104 | } 105 | 106 | return $output; 107 | } 108 | 109 | /** 110 | * {@inheritdoc} 111 | */ 112 | public function update($path, $contents, Config $config) 113 | { 114 | $statement = $this->pdo->prepare( 115 | "UPDATE {$this->table} SET contents=:newcontents, mimetype=:mimetype, size=:size, timestamp=:timestamp WHERE path=:path" 116 | ); 117 | 118 | $size = strlen($contents); 119 | $mimetype = Util::guessMimeType($path, $contents); 120 | $timestamp = $config->get('timestamp', time()); 121 | 122 | $pathWithPrefix = $this->applyPathPrefix($path); 123 | 124 | $statement->bindParam(':size', $size, PDO::PARAM_INT); 125 | $statement->bindParam(':mimetype', $mimetype, PDO::PARAM_STR); 126 | $statement->bindParam(':newcontents', $contents, PDO::PARAM_LOB); 127 | $statement->bindParam(':path', $pathWithPrefix, PDO::PARAM_STR); 128 | $statement->bindParam(':timestamp', $timestamp, PDO::PARAM_INT); 129 | 130 | return $statement->execute() ? compact('path', 'contents', 'size', 'mimetype', 'timestamp') : false; 131 | } 132 | 133 | /** 134 | * {@inheritdoc} 135 | */ 136 | public function updateStream($path, $resource, Config $config) 137 | { 138 | $statement = $this->pdo->prepare( 139 | "UPDATE {$this->table} SET contents=:newcontents, mimetype=:mimetype, timestamp=:timestamp WHERE path=:path" 140 | ); 141 | 142 | $mimetype = Util::guessMimeType($path, ''); 143 | $timestamp = $config->get('timestamp', time()); 144 | 145 | $pathWithPrefix = $this->applyPathPrefix($path); 146 | 147 | $statement->bindParam(':mimetype', $mimetype, PDO::PARAM_STR); 148 | $statement->bindParam(':newcontents', $resource, PDO::PARAM_LOB); 149 | $statement->bindParam(':path', $pathWithPrefix, PDO::PARAM_STR); 150 | $statement->bindParam(':timestamp', $timestamp, PDO::PARAM_INT); 151 | $output = $statement->execute() ? compact('path', 'mimetype', 'timestamp') : false; 152 | 153 | if ($output) { 154 | // Correct the size afterwards 155 | // It seems all drivers are happy with LENGTH(binary) 156 | $statement = $this->pdo->prepare("UPDATE {$this->table} SET size = LENGTH(contents) WHERE path = :path"); 157 | $statement->bindParam(':path', $pathWithPrefix, PDO::PARAM_STR); 158 | $statement->execute(); 159 | } 160 | 161 | return $output; 162 | } 163 | 164 | /** 165 | * {@inheritdoc} 166 | */ 167 | public function rename($path, $newpath) 168 | { 169 | $pathWithPrefix = $this->applyPathPrefix($path); 170 | $statement = $this->pdo->prepare("SELECT type FROM {$this->table} WHERE path=:path"); 171 | $statement->bindParam(':path', $pathWithPrefix, PDO::PARAM_STR); 172 | 173 | if ($statement->execute()) { 174 | $object = $statement->fetch(PDO::FETCH_ASSOC); 175 | 176 | if ($object['type'] === 'dir') { 177 | $dirContents = $this->listContents($path, true); 178 | 179 | $statement = $this->pdo->prepare("UPDATE {$this->table} SET path=:newpath WHERE path=:path"); 180 | 181 | $pathLength = strlen($path); 182 | 183 | $statement->bindParam(':path', $currentObjectPath, PDO::PARAM_STR); 184 | $statement->bindParam(':newpath', $newObjectPath, PDO::PARAM_STR); 185 | 186 | foreach ($dirContents as $object) { 187 | $currentObjectPath = $this->applyPathPrefix($object['path']); 188 | $newObjectPath = $this->applyPathPrefix($newpath.substr($object['path'], $pathLength)); 189 | 190 | $statement->execute(); 191 | } 192 | } 193 | } 194 | 195 | $statement = $this->pdo->prepare("UPDATE {$this->table} SET path=:newpath WHERE path=:path"); 196 | 197 | $pathWithPrefix = $this->applyPathPrefix($path); 198 | $newPathWithPrefix = $this->applyPathPrefix($newpath); 199 | $statement->bindParam(':path', $pathWithPrefix, PDO::PARAM_STR); 200 | $statement->bindParam(':newpath', $newPathWithPrefix, PDO::PARAM_STR); 201 | 202 | return $statement->execute(); 203 | } 204 | 205 | /** 206 | * {@inheritdoc} 207 | */ 208 | public function copy($path, $newpath) 209 | { 210 | $newPathWithPrefix = $this->applyPathPrefix($newpath); 211 | $pathWithPrefix = $this->applyPathPrefix($path); 212 | // Use a one-liner to avoid race condition 213 | // between a $this->has() and the actual copy. 214 | return (bool)$this->pdo->exec( 215 | sprintf( 216 | "INSERT INTO %s (path, contents, size, type, mimetype, timestamp) 217 | SELECT %s, contents, size, type, mimetype, timestamp FROM %s WHERE path = %s", 218 | $this->table, 219 | $this->pdo->quote($newPathWithPrefix), 220 | $this->table, 221 | $this->pdo->quote($pathWithPrefix) 222 | ) 223 | ); 224 | } 225 | 226 | /** 227 | * {@inheritdoc} 228 | */ 229 | public function delete($path) 230 | { 231 | $statement = $this->pdo->prepare("DELETE FROM {$this->table} WHERE path=:path"); 232 | $pathWithPrefix = $this->applyPathPrefix($path); 233 | $statement->bindParam(':path', $pathWithPrefix, PDO::PARAM_STR); 234 | 235 | return $statement->execute(); 236 | } 237 | 238 | /** 239 | * {@inheritdoc} 240 | */ 241 | public function deleteDir($dirname) 242 | { 243 | $dirContents = $this->listContents($dirname, true); 244 | 245 | if (!empty($dirContents)) { 246 | $statement = $this->pdo->prepare("DELETE FROM {$this->table} WHERE path=:path"); 247 | 248 | $statement->bindParam(':path', $currentObjectPath, PDO::PARAM_STR); 249 | 250 | foreach ($dirContents as $object) { 251 | $currentObjectPath = $this->applyPathPrefix($object['path']); 252 | $statement->execute(); 253 | } 254 | } 255 | 256 | $statement = $this->pdo->prepare("DELETE FROM {$this->table} WHERE path=:path AND type='dir'"); 257 | $dirnameWithPrefix = $this->applyPathPrefix($dirname); 258 | $statement->bindParam(':path', $dirnameWithPrefix, PDO::PARAM_STR); 259 | 260 | return $statement->execute(); 261 | } 262 | 263 | /** 264 | * {@inheritdoc} 265 | */ 266 | public function createDir($dirname, Config $config) 267 | { 268 | $statement = $this->pdo->prepare( 269 | "INSERT INTO {$this->table} (path, type, timestamp) VALUES(:path, :type, :timestamp)" 270 | ); 271 | 272 | $timestamp = $config->get('timestamp', time()); 273 | 274 | $dirnameWithPrefix = $this->applyPathPrefix($dirname); 275 | 276 | $statement->bindParam(':path', $dirnameWithPrefix, PDO::PARAM_STR); 277 | $statement->bindValue(':type', 'dir', PDO::PARAM_STR); 278 | $statement->bindValue(':timestamp', $timestamp, PDO::PARAM_STR); 279 | 280 | return $statement->execute(); 281 | } 282 | 283 | /** 284 | * {@inheritdoc} 285 | */ 286 | public function has($path) 287 | { 288 | $statement = $this->pdo->prepare("SELECT id FROM {$this->table} WHERE path=:path"); 289 | 290 | $pathWithPrefix = $this->applyPathPrefix($path); 291 | $statement->bindParam(':path', $pathWithPrefix, PDO::PARAM_STR); 292 | 293 | if ($statement->execute()) { 294 | return (bool)$statement->fetch(PDO::FETCH_ASSOC); 295 | } 296 | 297 | return false; 298 | } 299 | 300 | /** 301 | * This function prepare the way for read() and readStream() 302 | * 303 | * @param string $path 304 | * 305 | * @return \PDOStatement|false 306 | */ 307 | public function readPrepare($path) 308 | { 309 | $statement = $this->pdo->prepare("SELECT contents FROM {$this->table} WHERE path=:path"); 310 | 311 | $pathWithPrefix = $this->applyPathPrefix($path); 312 | $statement->bindParam(':path', $pathWithPrefix, PDO::PARAM_STR); 313 | 314 | if ($statement->execute()) { 315 | return $statement; 316 | } 317 | 318 | return false; 319 | } 320 | 321 | /** 322 | * {@inheritdoc} 323 | */ 324 | public function read($path) 325 | { 326 | $statement = $this->readPrepare($path); 327 | if ($statement && ($result = $statement->fetch(PDO::FETCH_ASSOC))) { 328 | if (is_resource($result['contents'])) { 329 | // Some PDO drivers return a stream (as should be for LOB) 330 | // so we need to retrieve it entirely. 331 | $result['contents'] = stream_get_contents($result['contents']); 332 | } 333 | 334 | return $result; 335 | } 336 | 337 | return false; 338 | } 339 | 340 | /** 341 | * {@inheritdoc} 342 | */ 343 | public function readStream($path) 344 | { 345 | if (!($statement = $this->readPrepare($path))) { 346 | return false; 347 | } 348 | 349 | $statement->bindColumn(1, $stream, PDO::PARAM_LOB); 350 | if (!$statement->fetch(PDO::FETCH_BOUND)) { 351 | return false; 352 | } 353 | 354 | if (!is_resource($stream)) { 355 | // Some PDO drivers (MySQL, SQLite) don't return a stream, so we simulate one 356 | // see https://bugs.php.net/bug.php?id=40913 357 | $result = $stream; 358 | $stream = fopen('php://temp', 'w+'); 359 | fwrite($stream, $result); 360 | rewind($stream); 361 | } 362 | 363 | return compact('stream'); 364 | } 365 | 366 | /** 367 | * {@inheritdoc} 368 | */ 369 | public function listContents($directory = '', $recursive = false) 370 | { 371 | $query = "SELECT path, size, type, mimetype, timestamp FROM {$this->table}"; 372 | 373 | $useWhere = (bool)strlen($this->applyPathPrefix($directory)); 374 | 375 | if ($useWhere) { 376 | $query .= " WHERE path LIKE :path_prefix OR path=:path"; 377 | } 378 | 379 | $statement = $this->pdo->prepare($query); 380 | 381 | if ($useWhere) { 382 | $pathPrefix = $this->applyPathPrefix($directory.'/').'%'; 383 | $statement->bindParam(':path_prefix', $pathPrefix, PDO::PARAM_STR); 384 | $directoryWithPrefix = $this->applyPathPrefix($directory); 385 | $statement->bindParam(':path', $directoryWithPrefix, PDO::PARAM_STR); 386 | } 387 | 388 | if (!$statement->execute()) { 389 | return []; 390 | } 391 | 392 | $result = $statement->fetchAll(PDO::FETCH_ASSOC); 393 | 394 | $result = array_map( 395 | function ($v) { 396 | $v['timestamp'] = (int)$v['timestamp']; 397 | $v['size'] = (int)$v['size']; 398 | $v['path'] = $this->removePathPrefix($v['path']); 399 | $v['dirname'] = Util::dirname($v['path']); 400 | 401 | if ($v['type'] === 'dir') { 402 | unset($v['mimetype']); 403 | unset($v['size']); 404 | unset($v['contents']); 405 | } 406 | 407 | return $v; 408 | }, 409 | $result 410 | ); 411 | 412 | return $recursive ? $result : Util::emulateDirectories($result); 413 | } 414 | 415 | /** 416 | * Get all the meta data of a file or directory. 417 | * 418 | * @param string $path 419 | * 420 | * @return array|false 421 | */ 422 | public function getMetadata($path) 423 | { 424 | $statement = $this->pdo->prepare( 425 | "SELECT id, path, size, type, mimetype, timestamp FROM {$this->table} WHERE path=:path" 426 | ); 427 | 428 | $pathWithPrefix = $this->applyPathPrefix($path); 429 | $statement->bindParam(':path', $pathWithPrefix, PDO::PARAM_STR); 430 | 431 | if ($statement->execute()) { 432 | return $statement->fetch(PDO::FETCH_ASSOC); 433 | } 434 | 435 | return false; 436 | } 437 | 438 | /** 439 | * Get all the meta data of a file or directory. 440 | * 441 | * @param string $path 442 | * 443 | * @return array|false 444 | */ 445 | public function getSize($path) 446 | { 447 | return $this->getMetadata($path); 448 | } 449 | 450 | /** 451 | * Get the mimetype of a file. 452 | * 453 | * @param string $path 454 | * 455 | * @return array|false 456 | */ 457 | public function getMimetype($path) 458 | { 459 | return $this->getMetadata($path); 460 | } 461 | 462 | /** 463 | * Get the timestamp of a file. 464 | * 465 | * @param string $path 466 | * 467 | * @return array|false 468 | */ 469 | public function getTimestamp($path) 470 | { 471 | return $this->getMetadata($path); 472 | } 473 | } 474 | -------------------------------------------------------------------------------- /tests/Fixtures/files/bar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IntegralSoftware/flysystem-pdo-adapter/a42aadd612c664d6ce5899a960de8695704fb0d7/tests/Fixtures/files/bar.jpg -------------------------------------------------------------------------------- /tests/Fixtures/files/foo.txt: -------------------------------------------------------------------------------- 1 | Foo bar -------------------------------------------------------------------------------- /tests/PDOAdapterTest.php: -------------------------------------------------------------------------------- 1 | pdo = new PDO('sqlite::memory:'); 39 | $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); 40 | 41 | switch($this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME)) { 42 | case 'sqlite': 43 | $createTableSql = 44 | "CREATE TABLE {$this->table} ( 45 | id INTEGER PRIMARY KEY, 46 | path TEXT NOT NULL UNIQUE, 47 | type TEXT NOT NULL, 48 | contents BLOB, 49 | size INTEGER NOT NULL DEFAULT 0, 50 | mimetype TEXT, 51 | timestamp INTEGER NOT NULL DEFAULT 0 52 | )"; 53 | break; 54 | 55 | case 'mysql': 56 | $createTableSql = 57 | "CREATE TABLE {$this->table} ( 58 | id INTEGER PRIMARY KEY AUTO_INCREMENT, 59 | path VARCHAR(191) NOT NULL UNIQUE, 60 | type enum('file','dir') NOT NULL, 61 | contents LONGBLOB, 62 | size INTEGER NOT NULL DEFAULT 0, 63 | mimetype VARCHAR(127), 64 | timestamp INTEGER NOT NULL DEFAULT 0 65 | )"; 66 | break; 67 | 68 | case 'pgsql': 69 | $createTableSql = 70 | "CREATE TABLE {$this->table} ( 71 | id SERIAL PRIMARY KEY, 72 | path TEXT NOT NULL UNIQUE, 73 | type TEXT NOT NULL, 74 | contents BYTEA, 75 | size INTEGER NOT NULL DEFAULT 0, 76 | mimetype TEXT, 77 | timestamp INTEGER NOT NULL DEFAULT 0, 78 | CONSTRAINT type_check CHECK (type='dir' or type='file') 79 | )"; 80 | break; 81 | } 82 | 83 | $this->pdo->exec($createTableSql); 84 | 85 | $this->adapter = new PDOAdapter($this->pdo, $this->table, $this->pathPrefix); 86 | $this->filesystem = new Filesystem($this->adapter); 87 | } 88 | 89 | public function tearDown() 90 | { 91 | $this->pdo->exec("DROP TABLE {$this->table}"); 92 | } 93 | 94 | protected function getTableContents($stripTimestampFromReturnedRows = true) 95 | { 96 | $statement = $this->pdo->prepare("SELECT * FROM {$this->table}"); 97 | $statement->execute(); 98 | $result = $statement->fetchAll(PDO::FETCH_ASSOC); 99 | 100 | return array_map(function ($v) use ($stripTimestampFromReturnedRows) { 101 | unset($v['id']); 102 | if ($stripTimestampFromReturnedRows) { 103 | unset($v['timestamp']); 104 | } 105 | $v['path'] = $this->adapter->removePathPrefix($v['path']); 106 | $v['size'] = (int)$v['size']; 107 | if (is_resource($v['contents'])) { 108 | $fh = $v['contents']; 109 | $v['contents'] = stream_get_contents($fh); 110 | fclose($fh); 111 | } 112 | 113 | return $v; 114 | }, $result); 115 | } 116 | 117 | protected function assertTableContains($expected, $stripTimestampFromReturnedRows = true) 118 | { 119 | $this->assertEquals($expected, $this->getTableContents($stripTimestampFromReturnedRows)); 120 | } 121 | 122 | protected function filterContents($contents) 123 | { 124 | return array_map(function ($v) { 125 | unset($v['timestamp']); 126 | 127 | return $v; 128 | }, $contents); 129 | } 130 | 131 | public function invalidTableNamesProvider() 132 | { 133 | return [ 134 | ['a$b'], 135 | ['a/b'], 136 | ['a*b'], 137 | ['ab*'], 138 | ['Abcde_f&'] 139 | ]; 140 | } 141 | 142 | /** 143 | * @dataProvider invalidTableNamesProvider 144 | * 145 | * @expectedException \InvalidArgumentException 146 | */ 147 | public function testInvalidTableName($tableName) 148 | { 149 | $adapter = new PDOAdapter($this->pdo, $tableName); 150 | } 151 | 152 | public function testBasicWrite() 153 | { 154 | $this->assertTrue($this->filesystem->createDir('foo')); 155 | 156 | $path1 = 'foo/bar.txt'; 157 | $contents1 = 'ala ma kota'; 158 | $this->assertTrue($this->filesystem->write($path1, $contents1)); 159 | 160 | $this->assertTableContains([ 161 | ['path' => 'foo', 'contents' => null, 'type' => 'dir', 'size' => 0, 'mimetype' => null], 162 | [ 163 | 'path' => $path1, 164 | 'contents' => $contents1, 165 | 'type' => 'file', 166 | 'size' => strlen($contents1), 167 | 'mimetype' => 'text/plain' 168 | ] 169 | ]); 170 | 171 | $path2 = 'foo/bar/baz.txt'; 172 | $contents2 = 'kot ma ale'; 173 | $this->assertTrue($this->filesystem->write($path2, $contents2)); 174 | 175 | $this->assertTableContains([ 176 | ['path' => 'foo', 'contents' => null, 'type' => 'dir', 'size' => 0, 'mimetype' => null], 177 | [ 178 | 'path' => $path1, 179 | 'contents' => $contents1, 180 | 'type' => 'file', 181 | 'size' => strlen($contents1), 182 | 'mimetype' => 'text/plain' 183 | ], 184 | [ 185 | 'path' => $path2, 186 | 'contents' => $contents2, 187 | 'type' => 'file', 188 | 'size' => strlen($contents2), 189 | 'mimetype' => 'text/plain' 190 | ] 191 | ]); 192 | } 193 | 194 | public function testWriteWithSpecificTimestamp() 195 | { 196 | $timestamp = \mktime(0, 0, 0, 1, 1, 2000); 197 | $config = array('timestamp' => $timestamp); 198 | 199 | $this->assertTrue($this->filesystem->createDir('foo', $config)); 200 | 201 | $path1 = 'foo/bar.txt'; 202 | $contents1 = 'ala ma kota'; 203 | $this->assertTrue($this->filesystem->write($path1, $contents1, $config)); 204 | 205 | $this->assertTableContains([ 206 | [ 207 | 'path' => 'foo', 208 | 'contents' => null, 209 | 'type' => 'dir', 210 | 'size' => 0, 211 | 'mimetype' => null, 212 | 'timestamp' => $timestamp 213 | ], 214 | [ 215 | 'path' => $path1, 216 | 'contents' => $contents1, 217 | 'type' => 'file', 218 | 'size' => strlen($contents1), 219 | 'mimetype' => 'text/plain', 220 | 'timestamp' => $timestamp 221 | ] 222 | ], 223 | false 224 | ); 225 | } 226 | 227 | public function testListContents() 228 | { 229 | $this->testBasicWrite(); 230 | 231 | $contents = $this->filterContents($this->filesystem->listContents('/foo')); 232 | 233 | $expected = [ 234 | [ 235 | 'dirname' => 'foo', 236 | 'basename' => 'bar', 237 | 'filename' => 'bar', 238 | 'path' => 'foo/bar', 239 | 'type' => 'dir' 240 | ], 241 | [ 242 | 'path' => 'foo/bar.txt', 243 | 'size' => 11, 244 | 'type' => 'file', 245 | 'mimetype' => 'text/plain', 246 | 'dirname' => 'foo', 247 | 'basename' => 'bar.txt', 248 | 'extension' => 'txt', 249 | 'filename' => 'bar' 250 | ] 251 | ]; 252 | 253 | $this->assertEquals($expected, $contents); 254 | 255 | // List / 256 | $contents = $this->filterContents($this->filesystem->listContents()); 257 | 258 | $expected = [ 259 | [ 260 | 'dirname' => '', 261 | 'basename' => 'foo', 262 | 'filename' => 'foo', 263 | 'path' => 'foo', 264 | 'type' => 'dir' 265 | ] 266 | ]; 267 | 268 | $this->assertEquals($expected, $contents); 269 | 270 | $this->assertTrue($this->filesystem->write('foo2/bar.txt', 'abc')); 271 | 272 | $contents = $this->filterContents($this->filesystem->listContents('/')); 273 | 274 | $expected = [ 275 | ['dirname' => '', 'basename' => 'foo', 'filename' => 'foo', 'path' => 'foo', 'type' => 'dir'], 276 | ['dirname' => '', 'basename' => 'foo2', 'filename' => 'foo2', 'path' => 'foo2', 'type' => 'dir'] 277 | ]; 278 | 279 | $this->assertEquals($expected, $contents); 280 | } 281 | 282 | public function testListContentsForDirWithPercentCharacter() 283 | { 284 | $this->assertTrue($this->filesystem->write('foo%/bar.txt', 'abc')); 285 | $this->assertTrue($this->filesystem->write('foo%/baz.txt', 'abc')); 286 | $this->assertTrue($this->filesystem->write('foozzz/bar.txt', 'abc')); 287 | $this->assertTrue($this->filesystem->write('foo%zz/barzz.txt', 'abcdef')); 288 | 289 | $expected = [ 290 | [ 291 | 'path' => 'foo%/bar.txt', 292 | 'size' => 3, 293 | 'type' => 'file', 294 | 'mimetype' => 'text/plain', 295 | 'dirname' => 'foo%', 296 | 'basename' => 'bar.txt', 297 | 'extension' => 'txt', 298 | 'filename' => 'bar', 299 | ], 300 | [ 301 | 'path' => 'foo%/baz.txt', 302 | 'size' => 3, 303 | 'type' => 'file', 304 | 'mimetype' => 'text/plain', 305 | 'dirname' => 'foo%', 306 | 'basename' => 'baz.txt', 307 | 'extension' => 'txt', 308 | 'filename' => 'baz', 309 | ] 310 | ]; 311 | 312 | $this->assertEquals($expected, $this->filterContents($this->filesystem->listContents('/foo%/'))); 313 | } 314 | 315 | public function testSettingMimetype() 316 | { 317 | $this->assertTrue($this->filesystem->write('foo/bar.jpg', 'abc')); 318 | $this->assertTrue($this->filesystem->write('foo/bar.png', 'abc')); 319 | $this->assertTrue($this->filesystem->write('foo/bar.mp3', 'abc')); 320 | 321 | $this->assertSame('image/jpeg', $this->filesystem->getMimetype('/foo/bar.jpg')); 322 | $this->assertSame('image/png', $this->filesystem->getMimetype('/foo/bar.png')); 323 | $this->assertSame('audio/mpeg', $this->filesystem->getMimetype('/foo/bar.mp3')); 324 | } 325 | 326 | public function testWriteRead() 327 | { 328 | $this->filesystem->write('foo/bar.txt', 'abc123'); 329 | $this->assertTrue($this->filesystem->write('foo/bar/baz.txt', 'def456')); 330 | $imageData = file_get_contents(__DIR__ . '/Fixtures/files/bar.jpg'); 331 | $this->assertTrue($this->filesystem->write('test/image.jpg', $imageData)); 332 | 333 | $this->assertSame('abc123', $this->filesystem->read('/foo/bar.txt')); 334 | $this->assertSame('def456', $this->filesystem->read('/foo/bar/baz.txt')); 335 | $this->assertSame($imageData, $this->filesystem->read('test/image.jpg')); 336 | } 337 | 338 | public function testWriteReadStream() 339 | { 340 | $imageStream = fopen(__DIR__ . '/Fixtures/files/bar.jpg', 'r'); 341 | $this->assertTrue($this->filesystem->writeStream('foo/bar.jpg', $imageStream)); 342 | $fileStream = fopen(__DIR__ . '/Fixtures/files/foo.txt', 'r'); 343 | $this->assertTrue($this->filesystem->writeStream('foo.txt', $fileStream)); 344 | 345 | $imageReadStream = $this->filesystem->readStream('/foo/bar.jpg'); 346 | $fileReadStream = $this->filesystem->readStream('/foo.txt'); 347 | 348 | rewind($imageStream); 349 | rewind($fileStream); 350 | 351 | $this->assertTrue(is_resource($imageReadStream)); 352 | $this->assertTrue(is_resource($fileReadStream)); 353 | $this->assertSame(stream_get_contents($imageStream), stream_get_contents($imageReadStream)); 354 | $this->assertSame(stream_get_contents($fileStream), stream_get_contents($fileReadStream)); 355 | } 356 | 357 | public function testUpdate() 358 | { 359 | $this->assertTrue($this->filesystem->write('foo/bar.txt', 'abc')); 360 | $this->assertSame('abc', $this->filesystem->read('foo/bar.txt')); 361 | $this->assertTrue($this->filesystem->update('foo/bar.txt', 'def')); 362 | $this->assertSame('def', $this->filesystem->read('foo/bar.txt')); 363 | } 364 | 365 | public function testUpdateStream() 366 | { 367 | $this->assertTrue($this->filesystem->write('foo/bar.txt', 'abc')); 368 | $this->assertSame('abc', $this->filesystem->read('foo/bar.txt')); 369 | 370 | $fileStream = fopen(__DIR__ . '/Fixtures/files/foo.txt', 'r'); 371 | $this->assertTrue($this->filesystem->updateStream('foo/bar.txt', $fileStream)); 372 | rewind($fileStream); 373 | $this->assertSame(stream_get_contents($fileStream), $this->filesystem->read('foo/bar.txt')); 374 | } 375 | 376 | public function testRenameOfNestedObjectsInDirectory() 377 | { 378 | $this->assertTrue($this->filesystem->createDir('foo')); 379 | $this->assertTrue($this->filesystem->write('foo/bar.txt', 'abc')); 380 | $this->assertTrue($this->filesystem->createDir('foo/baz')); 381 | $this->assertTrue($this->filesystem->write('foo/baz/buzz.txt', 'def')); 382 | 383 | $this->assertTrue($this->filesystem->has('foo')); 384 | $this->assertFalse($this->filesystem->has('foo/bar')); 385 | $this->assertTrue($this->filesystem->has('foo/baz')); 386 | 387 | $this->filesystem->rename('foo', 'renamed'); 388 | 389 | $this->assertFalse($this->filesystem->has('foo')); 390 | $this->assertFalse($this->filesystem->has('foo/baz')); 391 | $this->assertTrue($this->filesystem->has('renamed')); 392 | $this->assertTrue($this->filesystem->has('renamed/baz')); 393 | 394 | $this->assertEquals('abc', $this->filesystem->read('renamed/bar.txt')); 395 | $this->assertEquals('def', $this->filesystem->read('renamed/baz/buzz.txt')); 396 | } 397 | 398 | public function testDeleteOfNestedObjectsInDirectory() 399 | { 400 | $this->assertTrue($this->filesystem->createDir('foo')); 401 | $this->assertTrue($this->filesystem->write('foo/bar.txt', 'abc')); 402 | $this->assertTrue($this->filesystem->createDir('foo/baz')); 403 | $this->assertTrue($this->filesystem->write('foo/baz/buzz.txt', 'def')); 404 | 405 | $this->assertTrue($this->filesystem->has('foo')); 406 | $this->assertTrue($this->filesystem->has('foo/baz')); 407 | 408 | $this->filesystem->deleteDir('foo'); 409 | 410 | $this->assertEmpty($this->getTableContents()); 411 | } 412 | 413 | public function testCopy() 414 | { 415 | $this->assertTrue($this->filesystem->createDir('foo')); 416 | $this->assertTrue($this->filesystem->write('foo/bar.txt', 'abc')); 417 | 418 | $this->assertFalse($this->adapter->copy('foo/nonexisting_bar.txt', 'copied.txt')); 419 | 420 | $this->assertTrue($this->filesystem->copy('foo/bar.txt', 'copied.txt')); 421 | $this->assertSame($this->filesystem->read('foo/bar.txt'), $this->filesystem->read('copied.txt')); 422 | } 423 | 424 | public function testDelete() 425 | { 426 | $this->assertTrue($this->filesystem->createDir('foo')); 427 | $this->assertTrue($this->filesystem->write('foo/bar.txt', 'abc')); 428 | $this->assertTrue($this->filesystem->has('foo/bar.txt')); 429 | $this->assertTrue($this->filesystem->delete('foo/bar.txt')); 430 | $this->assertFalse($this->filesystem->has('foo/bar.txt')); 431 | } 432 | 433 | public function testHasForNonexistingPath() 434 | { 435 | $this->assertFalse($this->filesystem->has('foo/nonexisting_bar.txt')); 436 | } 437 | 438 | public function testReadStreamForNonexistingPath() 439 | { 440 | $this->assertFalse($this->adapter->readStream('foo/nonexisting_bar.txt')); 441 | } 442 | 443 | public function testMetadata() 444 | { 445 | $this->assertTrue($this->filesystem->write('foo/bar.jpg', 'abc')); 446 | 447 | $this->assertTrue($this->filesystem->has('foo/bar.jpg')); 448 | $this->assertEquals('image/jpeg', $this->filesystem->getMimetype('foo/bar.jpg')); 449 | $this->assertEquals(3, $this->filesystem->getSize('foo/bar.jpg')); 450 | $this->assertTrue($this->filesystem->getTimestamp('foo/bar.jpg') > 1448170000); 451 | } 452 | } 453 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | add('Integral\Tests', __DIR__); --------------------------------------------------------------------------------