1<?php
2
3namespace League\Flysystem;
4
5use BadMethodCallException;
6use InvalidArgumentException;
7use League\Flysystem\Plugin\PluggableTrait;
8use League\Flysystem\Plugin\PluginNotFoundException;
9
10/**
11 * @method array getWithMetadata(string $path, array $metadata)
12 * @method array listFiles(string $path = '', boolean $recursive = false)
13 * @method array listPaths(string $path = '', boolean $recursive = false)
14 * @method array listWith(array $keys = [], $directory = '', $recursive = false)
15 */
16class Filesystem implements FilesystemInterface
17{
18
19    /**
20     * @var AdapterInterface
21     */
22    protected $adapter;
23
24    /**
25     * @var Config
26     */
27    protected $config;
28
29    /**
30     * @var array
31     */
32    protected $plugins = array();
33
34    /**
35     * Constructor.
36     *
37     * @param AdapterInterface $adapter
38     * @param Config|array     $config
39     */
40    public function __construct(AdapterInterface $adapter, $config = null)
41    {
42        $this->adapter = $adapter;
43        $this->config = Util::ensureConfig($config);
44    }
45
46    /**
47     * Get the Adapter.
48     *
49     * @return AdapterInterface adapter
50     */
51    public function getAdapter()
52    {
53        return $this->adapter;
54    }
55
56    /**
57     * Get the Config.
58     *
59     * @return Config config object
60     */
61    public function getConfig()
62    {
63        return $this->config;
64    }
65
66    /**
67     * {@inheritdoc}
68     */
69    public function has($path)
70    {
71        $path = Util::normalizePath($path);
72
73        return (bool) $this->adapter->has($path);
74    }
75
76    /**
77     * {@inheritdoc}
78     */
79    public function write($path, $contents, array $config = array())
80    {
81        $path = Util::normalizePath($path);
82        $this->assertAbsent($path);
83        $config = $this->prepareConfig($config);
84
85        return (bool) $this->adapter->write($path, $contents, $config);
86    }
87
88    /**
89     * {@inheritdoc}
90     */
91    public function writeStream($path, $resource, array $config = array())
92    {
93        if (! is_resource($resource)) {
94            throw new InvalidArgumentException(__METHOD__.' expects argument #2 to be a valid resource.');
95        }
96
97        $path = Util::normalizePath($path);
98        $this->assertAbsent($path);
99        $config = $this->prepareConfig($config);
100
101        Util::rewindStream($resource);
102
103        return (bool) $this->adapter->writeStream($path, $resource, $config);
104    }
105
106    /**
107     * Create a file or update if exists.
108     *
109     * @param string $path     path to file
110     * @param string $contents file contents
111     * @param mixed  $config
112     *
113     * @throws FileExistsException
114     *
115     * @return bool success boolean
116     */
117    public function put($path, $contents, array $config = array())
118    {
119        $path = Util::normalizePath($path);
120
121        if ($this->has($path)) {
122            return $this->update($path, $contents, $config);
123        }
124
125        return $this->write($path, $contents, $config);
126    }
127
128    /**
129     * Create a file or update if exists using a stream.
130     *
131     * @param string   $path
132     * @param resource $resource
133     * @param mixed    $config
134     *
135     * @return bool success boolean
136     */
137    public function putStream($path, $resource, array $config = array())
138    {
139        $path = Util::normalizePath($path);
140
141        if ($this->has($path)) {
142            return $this->updateStream($path, $resource, $config);
143        }
144
145        return $this->writeStream($path, $resource, $config);
146    }
147
148    /**
149     * Read and delete a file.
150     *
151     * @param string $path
152     *
153     * @throws FileNotFoundException
154     *
155     * @return string file contents
156     */
157    public function readAndDelete($path)
158    {
159        $path = Util::normalizePath($path);
160        $this->assertPresent($path);
161        $contents = $this->read($path);
162
163        if (! $contents) {
164            return false;
165        }
166
167        $this->delete($path);
168
169        return $contents;
170    }
171
172    /**
173     * Update a file.
174     *
175     * @param string $path     path to file
176     * @param string $contents file contents
177     * @param mixed  $config   Config object or visibility setting
178     *
179     * @throws FileNotFoundException
180     *
181     * @return bool success boolean
182     */
183    public function update($path, $contents, array $config = array())
184    {
185        $path = Util::normalizePath($path);
186        $config = $this->prepareConfig($config);
187
188        $this->assertPresent($path);
189
190        return (bool) $this->adapter->update($path, $contents, $config);
191    }
192
193    /**
194     * Update a file with the contents of a stream.
195     *
196     * @param string   $path
197     * @param resource $resource
198     * @param mixed    $config   Config object or visibility setting
199     *
200     * @throws InvalidArgumentException
201     *
202     * @return bool success boolean
203     */
204    public function updateStream($path, $resource, array $config = array())
205    {
206        if (! is_resource($resource)) {
207            throw new InvalidArgumentException(__METHOD__.' expects argument #2 to be a valid resource.');
208        }
209
210        $path = Util::normalizePath($path);
211        $config = $this->prepareConfig($config);
212        $this->assertPresent($path);
213        Util::rewindStream($resource);
214
215        return (bool) $this->adapter->updateStream($path, $resource, $config);
216    }
217
218    /**
219     * Read a file.
220     *
221     * @param string $path path to file
222     *
223     * @throws FileNotFoundException
224     *
225     * @return string|false file contents or FALSE when fails
226     *                      to read existing file
227     */
228    public function read($path)
229    {
230        $path = Util::normalizePath($path);
231        $this->assertPresent($path);
232
233        if (! ($object = $this->adapter->read($path))) {
234            return false;
235        }
236
237        return $object['contents'];
238    }
239
240    /**
241     * Retrieves a read-stream for a path.
242     *
243     * @param string $path
244     *
245     * @return resource|false path resource or false when on failure
246     */
247    public function readStream($path)
248    {
249        $path = Util::normalizePath($path);
250        $this->assertPresent($path);
251
252        if (! $object = $this->adapter->readStream($path)) {
253            return false;
254        }
255
256        return $object['stream'];
257    }
258
259    /**
260     * Rename a file.
261     *
262     * @param string $path    path to file
263     * @param string $newpath new path
264     *
265     * @throws FileExistsException
266     * @throws FileNotFoundException
267     *
268     * @return bool success boolean
269     */
270    public function rename($path, $newpath)
271    {
272        $path = Util::normalizePath($path);
273        $newpath = Util::normalizePath($newpath);
274        $this->assertPresent($path);
275        $this->assertAbsent($newpath);
276
277        return (bool) $this->adapter->rename($path, $newpath);
278    }
279
280    /**
281     * Copy a file.
282     *
283     * @param string $path
284     * @param string $newpath
285     *
286     * @return bool
287     */
288    public function copy($path, $newpath)
289    {
290        $path = Util::normalizePath($path);
291        $newpath = Util::normalizePath($newpath);
292        $this->assertPresent($path);
293        $this->assertAbsent($newpath);
294
295        return $this->adapter->copy($path, $newpath);
296    }
297
298    /**
299     * Delete a file.
300     *
301     * @param string $path path to file
302     *
303     * @throws FileNotFoundException
304     *
305     * @return bool success boolean
306     */
307    public function delete($path)
308    {
309        $path = Util::normalizePath($path);
310        $this->assertPresent($path);
311
312        return $this->adapter->delete($path);
313    }
314
315    /**
316     * Delete a directory.
317     *
318     * @param string $dirname path to directory
319     *
320     * @return bool success boolean
321     */
322    public function deleteDir($dirname)
323    {
324        $dirname = Util::normalizePath($dirname);
325
326        if ($dirname === '') {
327            throw new RootViolationException('Root directories can not be deleted.');
328        }
329
330        return (bool) $this->adapter->deleteDir($dirname);
331    }
332
333    /**
334     * {@inheritdoc}
335     */
336    public function createDir($dirname, array $config = array())
337    {
338        $dirname = Util::normalizePath($dirname);
339        $config = $this->prepareConfig($config);
340
341        return (bool) $this->adapter->createDir($dirname, $config);
342    }
343
344    /**
345     * List the filesystem contents.
346     *
347     * @param string $directory
348     * @param bool   $recursive
349     *
350     * @return array contents
351     */
352    public function listContents($directory = '', $recursive = false)
353    {
354        $directory = Util::normalizePath($directory);
355        $contents = $this->adapter->listContents($directory, $recursive);
356        $mapper = function ($entry) use ($directory, $recursive) {
357            $entry = $entry + Util::pathinfo($entry['path']);
358
359            if (! empty($directory) && strpos($entry['path'], $directory) === false) {
360                return false;
361            }
362
363            if ($recursive === false && Util::dirname($entry['path']) !== $directory) {
364                return false;
365            }
366
367            return $entry;
368        };
369
370        return array_values(array_filter(array_map($mapper, $contents)));
371    }
372
373    /**
374     * Get a file's mime-type.
375     *
376     * @param string $path path to file
377     *
378     * @throws FileNotFoundException
379     *
380     * @return string|false file mime-type or FALSE when fails
381     *                      to fetch mime-type from existing file
382     */
383    public function getMimetype($path)
384    {
385        $path = Util::normalizePath($path);
386        $this->assertPresent($path);
387
388        if (! $object = $this->adapter->getMimetype($path)) {
389            return false;
390        }
391
392        return $object['mimetype'];
393    }
394
395    /**
396     * Get a file's timestamp.
397     *
398     * @param string $path path to file
399     *
400     * @throws FileNotFoundException
401     *
402     * @return string|false timestamp or FALSE when fails
403     *                      to fetch timestamp from existing file
404     */
405    public function getTimestamp($path)
406    {
407        $path = Util::normalizePath($path);
408        $this->assertPresent($path);
409
410        if (! $object = $this->adapter->getTimestamp($path)) {
411            return false;
412        }
413
414        return $object['timestamp'];
415    }
416
417    /**
418     * Get a file's visibility.
419     *
420     * @param string $path path to file
421     *
422     * @return string|false visibility (public|private) or FALSE
423     *                      when fails to check it in existing file
424     */
425    public function getVisibility($path)
426    {
427        $path = Util::normalizePath($path);
428        $this->assertPresent($path);
429
430        if (($object = $this->adapter->getVisibility($path)) === false) {
431            return false;
432        }
433
434        return $object['visibility'];
435    }
436
437    /**
438     * Get a file's size.
439     *
440     * @param string $path path to file
441     *
442     * @return int|false file size or FALSE when fails
443     *                   to check size of existing file
444     */
445    public function getSize($path)
446    {
447        $path = Util::normalizePath($path);
448
449        if (($object = $this->adapter->getSize($path)) === false || !isset($object['size'])) {
450            return false;
451        }
452
453        return (int) $object['size'];
454    }
455
456    /**
457     * Get a file's size.
458     *
459     * @param string $path       path to file
460     * @param string $visibility visibility
461     *
462     * @return bool success boolean
463     */
464    public function setVisibility($path, $visibility)
465    {
466        $path = Util::normalizePath($path);
467
468        return (bool) $this->adapter->setVisibility($path, $visibility);
469    }
470
471    /**
472     * Get a file's metadata.
473     *
474     * @param string $path path to file
475     *
476     * @throws FileNotFoundException
477     *
478     * @return array|false file metadata or FALSE when fails
479     *                     to fetch it from existing file
480     */
481    public function getMetadata($path)
482    {
483        $path = Util::normalizePath($path);
484        $this->assertPresent($path);
485
486        return $this->adapter->getMetadata($path);
487    }
488
489    /**
490     * Get a file/directory handler.
491     *
492     * @param string  $path
493     * @param Handler $handler
494     *
495     * @return Handler file or directory handler
496     */
497    public function get($path, Handler $handler = null)
498    {
499        $path = Util::normalizePath($path);
500
501        if (! $handler) {
502            $metadata = $this->getMetadata($path);
503            $handler = $metadata['type'] === 'file' ? new File($this, $path) : new Directory($this, $path);
504        }
505
506        $handler->setPath($path);
507        $handler->setFilesystem($this);
508
509        return $handler;
510    }
511
512    /**
513     * Convert a config array to a Config object with the correct fallback.
514     *
515     * @param array $config
516     *
517     * @return Config
518     */
519    protected function prepareConfig(array $config)
520    {
521        $config = new Config($config);
522        $config->setFallback($this->config);
523
524        return $config;
525    }
526
527    /**
528     * Assert a file is present.
529     *
530     * @param string $path path to file
531     *
532     * @throws FileNotFoundException
533     */
534    public function assertPresent($path)
535    {
536        if (! $this->has($path)) {
537            throw new FileNotFoundException($path);
538        }
539    }
540
541    /**
542     * Assert a file is absent.
543     *
544     * @param string $path path to file
545     *
546     * @throws FileExistsException
547     */
548    public function assertAbsent($path)
549    {
550        if ($this->has($path)) {
551            throw new FileExistsException($path);
552        }
553    }
554
555    /**
556     * Plugins pass-through.
557     *
558     * @param string $method
559     * @param array  $arguments
560     *
561     * @throws BadMethodCallException
562     *
563     * @return mixed
564     */
565    public function __call($method, array $arguments)
566    {
567        try {
568            return $this->invokePlugin($method, $arguments, $this);
569        } catch (PluginNotFoundException $e) {
570            throw new BadMethodCallException(
571                'Call to undefined method '
572                .__CLASS__
573                .'::'.$method
574            );
575        }
576    }
577
578       /**
579     * Register a plugin.
580     *
581     * @param PluginInterface $plugin
582     *
583     * @return $this
584     */
585    public function addPlugin(PluginInterface $plugin)
586    {
587        $this->plugins[$plugin->getMethod()] = $plugin;
588
589        return $this;
590    }
591
592    /**
593     * Register a plugin.
594     *
595     * @param string $method
596     *
597     * @throws LogicException
598     *
599     * @return PluginInterface $plugin
600     */
601    protected function findPlugin($method)
602    {
603        if (! isset($this->plugins[$method])) {
604            throw new PluginNotFoundException('Plugin not found for method: '.$method);
605        }
606
607        if (! method_exists($this->plugins[$method], 'handle')) {
608            throw new LogicException(get_class($this->plugins[$method]).' does not have a handle method.');
609        }
610
611        return $this->plugins[$method];
612    }
613
614    /**
615     * Invoke a plugin by method name.
616     *
617     * @param string $method
618     * @param array  $arguments
619     *
620     * @return mixed
621     */
622    protected function invokePlugin($method, array $arguments, FilesystemInterface $filesystem)
623    {
624        $plugin = $this->findPlugin($method);
625        $plugin->setFilesystem($filesystem);
626        $callback = array($plugin, 'handle');
627
628        return call_user_func_array($callback, $arguments);
629    }
630}
631