LocalAdapterTests.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584
  1. <?php
  2. namespace League\Flysystem\Adapter;
  3. use League\Flysystem\Config;
  4. use League\Flysystem\Exception;
  5. use League\Flysystem\NotSupportedException;
  6. use PHPUnit\Framework\TestCase;
  7. use RuntimeException;
  8. function fopen($result, $mode)
  9. {
  10. if (substr($result, -5) === 'false') {
  11. return false;
  12. }
  13. if (substr($result, -10) === 'fail.close') {
  14. return \fopen('data://text/plain,fail.close', $mode);
  15. }
  16. return call_user_func_array('fopen', func_get_args());
  17. }
  18. function fclose($result)
  19. {
  20. if (is_resource($result) && stream_get_contents($result) === 'fail.close') {
  21. \fclose($result);
  22. return false;
  23. }
  24. return call_user_func_array('fclose', func_get_args());
  25. }
  26. function chmod($filename, $mode)
  27. {
  28. if (strpos($filename, 'chmod.fail') !== false) {
  29. return false;
  30. }
  31. return \chmod($filename, $mode);
  32. }
  33. function mkdir($pathname, $mode = 0777, $recursive = false, $context = null)
  34. {
  35. if (strpos($pathname, 'fail.plz') !== false) {
  36. return false;
  37. }
  38. return call_user_func_array('mkdir', func_get_args());
  39. }
  40. class LocalAdapterTests extends TestCase
  41. {
  42. /**
  43. * @var Local
  44. */
  45. protected $adapter;
  46. protected $root;
  47. public function setup(): void
  48. {
  49. $this->root = __DIR__ . '/files/';
  50. $this->adapter = new Local($this->root);
  51. }
  52. public function teardown(): void
  53. {
  54. $it = new \RecursiveDirectoryIterator($this->root, \RecursiveDirectoryIterator::SKIP_DOTS);
  55. $files = new \RecursiveIteratorIterator(
  56. $it,
  57. \RecursiveIteratorIterator::CHILD_FIRST
  58. );
  59. foreach ($files as $file) {
  60. if ($file->getFilename() === '.' || $file->getFilename() === '..') {
  61. continue;
  62. }
  63. if ($file->isDir()) {
  64. rmdir($file->getRealPath());
  65. } else {
  66. unlink($file->getPathname());
  67. }
  68. }
  69. }
  70. public function testUpdateSetsNewVisibility()
  71. {
  72. $this->adapter->write('file.txt', 'old contents', new Config(['visibility' => 'public']));
  73. $this->assertEquals('old contents', $this->adapter->read('file.txt')['contents']);
  74. $this->assertEquals('public', $this->adapter->getVisibility('file.txt')['visibility']);
  75. $this->adapter->update('file.txt', 'new contents', new Config(['visibility' => 'private']));
  76. $this->assertEquals('new contents', $this->adapter->read('file.txt')['contents']);
  77. $this->assertEquals('private', $this->adapter->getVisibility('file.txt')['visibility']);
  78. }
  79. public function testStreamWrappersAreSupported()
  80. {
  81. if (IS_WINDOWS) {
  82. $this->markTestSkipped('Windows does not support this.');
  83. }
  84. $this->adapter->write('file.txt', 'contents', new Config());
  85. $adapter = new Local('file://' . $this->root);
  86. $this->assertCount(1, $adapter->listContents());
  87. }
  88. public function testRelativeRootsAreSupportes()
  89. {
  90. (new Local(__DIR__ . '/files'))->write('file.txt', 'contents', new Config());
  91. $adapter = new Local(__DIR__ . '/files/../files');
  92. $this->assertCount(1, $adapter->listContents());
  93. }
  94. public function testHasWithDir()
  95. {
  96. $this->adapter->createDir('0', new Config());
  97. $this->assertTrue($this->adapter->has('0'));
  98. $this->adapter->deleteDir('0');
  99. }
  100. public function testHasWithFile()
  101. {
  102. $adapter = $this->adapter;
  103. $adapter->write('file.txt', 'content', new Config());
  104. $this->assertTrue($adapter->has('file.txt'));
  105. $adapter->delete('file.txt');
  106. }
  107. public function testReadStream()
  108. {
  109. $adapter = $this->adapter;
  110. $adapter->write('file.txt', 'contents', new Config());
  111. $result = $adapter->readStream('file.txt');
  112. $this->assertIsArray($result);
  113. $this->assertArrayHasKey('stream', $result);
  114. $this->assertIsResource($result['stream']);
  115. fclose($result['stream']);
  116. $adapter->delete('file.txt');
  117. }
  118. public function testWriteStream()
  119. {
  120. $adapter = $this->adapter;
  121. $temp = tmpfile();
  122. fwrite($temp, 'dummy');
  123. rewind($temp);
  124. $adapter->writeStream('dir/file.txt', $temp, new Config(['visibility' => 'public']));
  125. fclose($temp);
  126. $this->assertTrue($adapter->has('dir/file.txt'));
  127. $result = $adapter->read('dir/file.txt');
  128. $this->assertEquals('dummy', $result['contents']);
  129. $adapter->deleteDir('dir');
  130. }
  131. public function testListingNonexistingDirectory()
  132. {
  133. $result = $this->adapter->listContents('nonexisting/directory');
  134. $this->assertEquals([], $result);
  135. }
  136. public function testUpdateStream()
  137. {
  138. $adapter = $this->adapter;
  139. $adapter->write('file.txt', 'initial', new Config());
  140. $temp = tmpfile();
  141. fwrite($temp, 'dummy');
  142. $adapter->updateStream('file.txt', $temp, new Config());
  143. fclose($temp);
  144. $this->assertTrue($adapter->has('file.txt'));
  145. $adapter->delete('file.txt');
  146. }
  147. public function testCreateZeroDir()
  148. {
  149. $this->adapter->createDir('0', new Config());
  150. $this->assertTrue(is_dir($this->adapter->applyPathPrefix('0')));
  151. $this->adapter->deleteDir('0');
  152. }
  153. public function testCopy()
  154. {
  155. $adapter = $this->adapter;
  156. $adapter->write('file.ext', 'content', new Config(['visibility' => 'public']));
  157. $this->assertTrue($adapter->copy('file.ext', 'new.ext'));
  158. $this->assertTrue($adapter->has('new.ext'));
  159. $adapter->delete('file.ext');
  160. $adapter->delete('new.ext');
  161. }
  162. public function testFailingStreamCalls()
  163. {
  164. $this->assertFalse($this->adapter->writeStream('false', tmpfile(), new Config()));
  165. $this->assertFalse($this->adapter->writeStream('fail.close', tmpfile(), new Config()));
  166. }
  167. public function testNullPrefix()
  168. {
  169. $this->adapter->setPathPrefix('');
  170. $path = 'some' . DIRECTORY_SEPARATOR . 'path.ext';
  171. $this->assertEquals($path, $this->adapter->applyPathPrefix($path));
  172. $this->assertEquals($path, $this->adapter->removePathPrefix($path));
  173. }
  174. public function testWindowsPrefix()
  175. {
  176. $path = 'some' . DIRECTORY_SEPARATOR . 'path.ext';
  177. $expected = 'c:' . DIRECTORY_SEPARATOR . $path;
  178. $this->adapter->setPathPrefix('c:/');
  179. $prefixed = $this->adapter->applyPathPrefix($path);
  180. $this->assertEquals($expected, $prefixed);
  181. $this->assertEquals($path, $this->adapter->removePathPrefix($prefixed));
  182. $expected = 'c:\\\\some\\dir' . DIRECTORY_SEPARATOR . $path;
  183. $this->adapter->setPathPrefix('c:\\\\some\\dir\\');
  184. $prefixed = $this->adapter->applyPathPrefix($path);
  185. $this->assertEquals($expected, $prefixed);
  186. $this->assertEquals($path, $this->adapter->removePathPrefix($prefixed));
  187. }
  188. public function testGetPathPrefix()
  189. {
  190. $this->assertEquals(realpath($this->root), realpath($this->adapter->getPathPrefix()));
  191. }
  192. public function testRenameToNonExistsingDirectory()
  193. {
  194. $this->adapter->write('file.txt', 'contents', new Config());
  195. $dirname = uniqid();
  196. $this->assertFalse(is_dir($this->root . DIRECTORY_SEPARATOR . $dirname));
  197. $this->assertTrue($this->adapter->rename('file.txt', $dirname . '/file.txt'));
  198. }
  199. public function testNotWritableRoot()
  200. {
  201. if (IS_WINDOWS) {
  202. $this->markTestSkipped("File permissions not supported on Windows.");
  203. }
  204. if (posix_getuid() === 0) {
  205. $this->markTestSkipped("Cannot make non-writable for the root user.");
  206. }
  207. try {
  208. $root = $this->root . 'not-writable';
  209. mkdir($root, 0000, true);
  210. $this->expectException('LogicException');
  211. new Local($root);
  212. } catch (\Exception $e) {
  213. rmdir($root);
  214. throw $e;
  215. }
  216. }
  217. public function testListContents()
  218. {
  219. $this->adapter->write('dirname/file.txt', 'contents', new Config());
  220. $contents = $this->adapter->listContents('dirname', false);
  221. $this->assertCount(1, $contents);
  222. $this->assertArrayHasKey('type', $contents[0]);
  223. }
  224. public function testListContentsRecursive()
  225. {
  226. $this->adapter->write('dirname/file.txt', 'contents', new Config());
  227. $this->adapter->write('dirname/other.txt', 'contents', new Config());
  228. $contents = $this->adapter->listContents('', true);
  229. $this->assertCount(3, $contents);
  230. }
  231. public function testGetSize()
  232. {
  233. $this->adapter->write('dummy.txt', '1234', new Config());
  234. $result = $this->adapter->getSize('dummy.txt');
  235. $this->assertIsArray($result);
  236. $this->assertArrayHasKey('size', $result);
  237. $this->assertEquals(4, $result['size']);
  238. }
  239. public function testGetTimestamp()
  240. {
  241. $this->adapter->write('dummy.txt', '1234', new Config());
  242. $result = $this->adapter->getTimestamp('dummy.txt');
  243. $this->assertIsArray($result);
  244. $this->assertArrayHasKey('timestamp', $result);
  245. $this->assertIsInt($result['timestamp']);
  246. }
  247. public function testGetMimetype()
  248. {
  249. $this->adapter->write('text.txt', 'contents', new Config());
  250. $result = $this->adapter->getMimetype('text.txt');
  251. $this->assertIsArray($result);
  252. $this->assertArrayHasKey('mimetype', $result);
  253. $this->assertEquals('text/plain', $result['mimetype']);
  254. }
  255. public function testCreateDirFail()
  256. {
  257. $this->assertFalse($this->adapter->createDir('fail.plz', new Config()));
  258. }
  259. public function testCreateDirDefaultVisibility()
  260. {
  261. $this->adapter->createDir('test-dir', new Config());
  262. $output = $this->adapter->getVisibility('test-dir');
  263. $this->assertIsArray($output);
  264. $this->assertArrayHasKey('visibility', $output);
  265. $this->assertEquals('public', $output['visibility']);
  266. }
  267. public function testDeleteDir()
  268. {
  269. $this->adapter->write('nested/dir/path.txt', 'contents', new Config());
  270. $this->assertTrue(is_dir(__DIR__ . '/files/nested/dir'));
  271. $this->adapter->deleteDir('nested');
  272. $this->assertFalse($this->adapter->has('nested/dir/path.txt'));
  273. $this->assertFalse(is_dir(__DIR__ . '/files/nested/dir'));
  274. }
  275. public function testVisibilityPublicFile()
  276. {
  277. if (IS_WINDOWS) {
  278. $this->markTestSkipped("Visibility not supported on Windows.");
  279. }
  280. $this->adapter->write('path.txt', 'contents', new Config());
  281. $this->adapter->setVisibility('path.txt', 'public');
  282. $output = $this->adapter->getVisibility('path.txt');
  283. $this->assertIsArray($output);
  284. $this->assertArrayHasKey('visibility', $output);
  285. $this->assertEquals('public', $output['visibility']);
  286. $this->assertEquals("0644", substr(sprintf('%o', fileperms($this->root . 'path.txt')), -4));
  287. }
  288. public function testVisibilityPublicDir()
  289. {
  290. if (IS_WINDOWS) {
  291. $this->markTestSkipped("Visibility not supported on Windows.");
  292. }
  293. $this->adapter->createDir('public-dir', new Config());
  294. $this->adapter->setVisibility('public-dir', 'public');
  295. $output = $this->adapter->getVisibility('public-dir');
  296. $this->assertIsArray($output);
  297. $this->assertArrayHasKey('visibility', $output);
  298. $this->assertEquals('public', $output['visibility']);
  299. }
  300. public function testVisibilityPrivateFile()
  301. {
  302. if (IS_WINDOWS) {
  303. $this->markTestSkipped("Visibility not supported on Windows.");
  304. }
  305. $this->adapter->write('path.txt', 'contents', new Config());
  306. $this->adapter->setVisibility('path.txt', 'private');
  307. $output = $this->adapter->getVisibility('path.txt');
  308. $this->assertIsArray($output);
  309. $this->assertArrayHasKey('visibility', $output);
  310. $this->assertEquals('private', $output['visibility']);
  311. $this->assertEquals("0600", substr(sprintf('%o', fileperms($this->root . 'path.txt')), -4));
  312. }
  313. public function testVisibilityPrivateDir()
  314. {
  315. if (IS_WINDOWS) {
  316. $this->markTestSkipped("Visibility not supported on Windows.");
  317. }
  318. $this->adapter->createDir('private-dir', new Config());
  319. $this->adapter->setVisibility('private-dir', 'private');
  320. $output = $this->adapter->getVisibility('private-dir');
  321. $this->assertIsArray($output);
  322. $this->assertArrayHasKey('visibility', $output);
  323. $this->assertEquals('private', $output['visibility']);
  324. }
  325. public function testVisibilityFail()
  326. {
  327. $this->assertFalse(
  328. $this->adapter->setVisibility('chmod.fail', 'public')
  329. );
  330. }
  331. public function testUnknownVisibility()
  332. {
  333. if (IS_WINDOWS) {
  334. $this->markTestSkipped("Visibility not supported on Windows.");
  335. }
  336. $umask = umask(0);
  337. mkdir($this->root . 'subdir', 0750);
  338. umask($umask);
  339. $output = $this->adapter->getVisibility('subdir');
  340. $this->assertNotEquals('private', $output['visibility']); // private is 0700 not 0750
  341. $this->assertNotEquals('public', $output['visibility']); // public is 0755 not 0750
  342. $this->assertEquals('0750', $output['visibility']);
  343. }
  344. public function testCustomizedVisibility()
  345. {
  346. if (IS_WINDOWS) {
  347. $this->markTestSkipped("Visibility not supported on Windows.");
  348. }
  349. // override a permission mapping
  350. $permissions = [
  351. 'dir' => [
  352. 'private' => 0770, // private to me and the gang
  353. ],
  354. ];
  355. $adapter = new Local($this->root, LOCK_EX, Local::DISALLOW_LINKS, $permissions);
  356. $adapter->createDir('private-dir', new Config());
  357. $adapter->setVisibility('private-dir', 'private');
  358. $output = $adapter->getVisibility('private-dir');
  359. $this->assertEquals('private', $output['visibility']);
  360. $this->assertEquals('0770', substr(sprintf('%o', fileperms($this->root . 'private-dir')), -4));
  361. }
  362. public function testCustomVisibility()
  363. {
  364. if (IS_WINDOWS) {
  365. $this->markTestSkipped("Visibility not supported on Windows.");
  366. }
  367. // add a permission mapping
  368. $permissions = [
  369. 'dir' => [
  370. 'yolo' => 0777,
  371. ],
  372. ];
  373. $adapter = new Local($this->root, LOCK_EX, Local::DISALLOW_LINKS, $permissions);
  374. $adapter->createDir('yolo-dir', new Config());
  375. $adapter->setVisibility('yolo-dir', 'yolo');
  376. $location = $this->root . 'yolo-dir';
  377. $output = $adapter->getVisibility('yolo-dir');
  378. $this->assertEquals('yolo', $output['visibility']);
  379. $this->assertEquals('0777', substr(sprintf('%o', fileperms($location)), -4));
  380. }
  381. public function testFirstVisibilityOctet()
  382. {
  383. if (IS_WINDOWS) {
  384. $this->markTestSkipped("Visibility not supported on Windows.");
  385. }
  386. $permissions = [
  387. 'file' => [
  388. 'public' => 0644,
  389. 'private' => 0600,
  390. ],
  391. 'dir' => [
  392. 'sticky' => 01777,
  393. 'public' => 0755,
  394. 'private' => 0700,
  395. ],
  396. ];
  397. $adapter = new Local($this->root, LOCK_EX, Local::DISALLOW_LINKS, $permissions);
  398. $adapter->createDir('sticky-dir', new Config());
  399. $adapter->setVisibility('sticky-dir', 'sticky');
  400. $output = $adapter->getVisibility('sticky-dir');
  401. $this->assertEquals('sticky', $output['visibility']);
  402. $this->assertEquals('1777', substr(sprintf('%o', fileperms($this->root . 'sticky-dir')), -4));
  403. }
  404. public function testApplyPathPrefix()
  405. {
  406. $this->adapter->setPathPrefix('');
  407. $this->assertEquals('', $this->adapter->applyPathPrefix(''));
  408. }
  409. public function testConstructorWithLink()
  410. {
  411. if (IS_WINDOWS) {
  412. $this->markTestSkipped("File permissions not supported on Windows.");
  413. }
  414. $target = $this->root;
  415. $link = __DIR__ . DIRECTORY_SEPARATOR . 'link_to_files';
  416. symlink($target, $link);
  417. $adapter = new Local($link);
  418. $this->assertEquals($target, $adapter->getPathPrefix());
  419. unlink($link);
  420. }
  421. public function testLinkCausedUnsupportedException()
  422. {
  423. $this->expectException(NotSupportedException::class);
  424. $original = $this->root . 'original.txt';
  425. $link = $this->root . 'link.txt';
  426. file_put_contents($original, 'something');
  427. symlink($original, $link);
  428. $this->adapter->listContents();
  429. }
  430. public function testLinkIsSkipped()
  431. {
  432. $original = $this->root . 'original.txt';
  433. $link = $this->root . 'link.txt';
  434. file_put_contents($original, 'something');
  435. symlink($original, $link);
  436. $adapter = new Local($this->root, LOCK_EX, Local::SKIP_LINKS);
  437. $result = $adapter->listContents();
  438. $this->assertCount(1, $result);
  439. }
  440. public function testLinksAreDeletedDuringDeleteDir()
  441. {
  442. mkdir($this->root . 'subdir', 0777, true);
  443. $original = $this->root . 'original.txt';
  444. $link = $this->root . 'subdir/link.txt';
  445. file_put_contents($original, 'something');
  446. symlink($original, $link);
  447. $adapter = new Local($this->root, LOCK_EX, Local::SKIP_LINKS);
  448. $this->assertTrue(is_link($link));
  449. $adapter->deleteDir('subdir');
  450. $this->assertFalse(is_link($link));
  451. }
  452. public function testUnreadableFilesCauseAnError()
  453. {
  454. $this->expectException('League\Flysystem\UnreadableFileException');
  455. $adapter = new Local($this->root, LOCK_EX, Local::SKIP_LINKS);
  456. $reflection = new \ReflectionClass($adapter);
  457. $method = $reflection->getMethod('guardAgainstUnreadableFileInfo');
  458. $method->setAccessible(true);
  459. /** @var \SplFileInfo $fileInfo */
  460. $fileInfo = $this->prophesize('SplFileInfo');
  461. $fileInfo->getRealPath()->willReturn('somewhere');
  462. $fileInfo->isReadable()->willReturn(false);
  463. $method->invoke($adapter, $fileInfo->reveal());
  464. }
  465. public function testMimetypeFallbackOnExtension()
  466. {
  467. $this->adapter->write('test.xlsx', '', new Config());
  468. $this->assertEquals('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', $this->adapter->getMimetype('test.xlsx')['mimetype']);
  469. }
  470. public function testDeleteFileShouldReturnTrue()
  471. {
  472. $original = $this->root . 'delete.txt';
  473. file_put_contents($original, 'something');
  474. $this->assertTrue($this->adapter->delete('delete.txt'));
  475. }
  476. public function testDeleteMissingFileShouldReturnFalse()
  477. {
  478. $this->assertFalse($this->adapter->delete('missing.txt'));
  479. }
  480. public function testRootDirectoryCreationProblemCausesAnError()
  481. {
  482. $this->expectException(Exception::class);
  483. $root = $this->root . 'fail.plz';
  484. new Local($root);
  485. }
  486. }