Completed
Push — master ( 8d277a...4cf76b )
by Pierre
03:04
created

Ftp::size()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 10
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 5
nc 2
nop 1
1
<?php
2
3
namespace Gaufrette\Adapter;
4
5
use Gaufrette\Adapter;
6
use Gaufrette\File;
7
use Gaufrette\Filesystem;
8
9
/**
10
 * Ftp adapter.
11
 *
12
 * @author  Antoine Hérault <[email protected]>
13
 */
14
class Ftp implements Adapter,
0 ignored issues
show
Coding Style introduced by
The first item in a multi-line implements list must be on the line following the implements keyword
Loading history...
15
                     FileFactory,
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces before interface name; 21 found
Loading history...
16
                     ListKeysAware,
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces before interface name; 21 found
Loading history...
17
                     SizeCalculator
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces before interface name; 21 found
Loading history...
18
{
19
    protected $connection = null;
20
    protected $directory;
21
    protected $host;
22
    protected $port;
23
    protected $username;
24
    protected $password;
25
    protected $passive;
26
    protected $create;
27
    protected $mode;
28
    protected $ssl;
29
    protected $timeout;
30
    protected $fileData = array();
31
    protected $utf8;
32
33
    /**
34
     * @param string $directory The directory to use in the ftp server
35
     * @param string $host      The host of the ftp server
36
     * @param array  $options   The options like port, username, password, passive, create, mode
37
     */
38
    public function __construct($directory, $host, $options = array())
39
    {
40
        if (!extension_loaded('ftp')) {
41
            throw new \RuntimeException('Unable to use Gaufrette\Adapter\Ftp as the FTP extension is not available.');
42
        }
43
44
        $this->directory = (string) $directory;
45
        $this->host = $host;
46
        $this->port = isset($options['port']) ? $options['port'] : 21;
47
        $this->username = isset($options['username']) ? $options['username'] : null;
48
        $this->password = isset($options['password']) ? $options['password'] : null;
49
        $this->passive = isset($options['passive']) ? $options['passive'] : false;
50
        $this->create = isset($options['create']) ? $options['create'] : false;
51
        $this->mode = isset($options['mode']) ? $options['mode'] : FTP_BINARY;
52
        $this->ssl = isset($options['ssl']) ? $options['ssl'] : false;
53
        $this->timeout = isset($options['timeout']) ? $options['timeout'] : 90;
54
        $this->utf8 = isset($options['utf8']) ? $options['utf8'] : false;
55
    }
56
57
    /**
58
     * {@inheritdoc}
59
     */
60
    public function read($key)
61
    {
62
        $this->ensureDirectoryExists($this->directory, $this->create);
63
64
        $temp = fopen('php://temp', 'r+');
65
66
        if (!ftp_fget($this->getConnection(), $temp, $this->computePath($key), $this->mode)) {
67
            return false;
68
        }
69
70
        rewind($temp);
71
        $contents = stream_get_contents($temp);
72
        fclose($temp);
73
74
        return $contents;
75
    }
76
77
    /**
78
     * {@inheritdoc}
79
     */
80
    public function write($key, $content)
81
    {
82
        $this->ensureDirectoryExists($this->directory, $this->create);
83
84
        $path = $this->computePath($key);
85
        $directory = \Gaufrette\Util\Path::dirname($path);
86
87
        $this->ensureDirectoryExists($directory, true);
88
89
        $temp = fopen('php://temp', 'r+');
90
        $size = fwrite($temp, $content);
91
        rewind($temp);
92
93
        if (!ftp_fput($this->getConnection(), $path, $temp, $this->mode)) {
94
            fclose($temp);
95
96
            return false;
97
        }
98
99
        fclose($temp);
100
101
        return $size;
102
    }
103
104
    /**
105
     * {@inheritdoc}
106
     */
107 View Code Duplication
    public function rename($sourceKey, $targetKey)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
108
    {
109
        $this->ensureDirectoryExists($this->directory, $this->create);
110
111
        $sourcePath = $this->computePath($sourceKey);
112
        $targetPath = $this->computePath($targetKey);
113
114
        $this->ensureDirectoryExists(\Gaufrette\Util\Path::dirname($targetPath));
115
116
        return ftp_rename($this->getConnection(), $sourcePath, $targetPath);
117
    }
118
119
    /**
120
     * {@inheritdoc}
121
     */
122
    public function exists($key)
123
    {
124
        $this->ensureDirectoryExists($this->directory, $this->create);
125
126
        $file  = $this->computePath($key);
127
        $lines = ftp_rawlist($this->getConnection(), '-al ' . \Gaufrette\Util\Path::dirname($file));
128
129
        if (false === $lines) {
130
            return false;
131
        }
132
133
        $pattern = '{(?<!->) '.preg_quote(basename($file)).'( -> |$)}m';
134
        foreach ($lines as $line) {
135
            if (preg_match($pattern, $line)) {
136
                return true;
137
            }
138
        }
139
140
        return false;
141
    }
142
143
    /**
144
     * {@inheritdoc}
145
     */
146
    public function keys()
147
    {
148
        $this->ensureDirectoryExists($this->directory, $this->create);
149
150
        $keys = $this->fetchKeys();
151
152
        return $keys['keys'];
153
    }
154
155
    /**
156
     * {@inheritdoc}
157
     */
158
    public function listKeys($prefix = '')
159
    {
160
        $this->ensureDirectoryExists($this->directory, $this->create);
161
162
        preg_match('/(.*?)[^\/]*$/', $prefix, $match);
163
        $directory = rtrim($match[1], '/');
164
165
        $keys = $this->fetchKeys($directory, false);
166
167
        if ($directory === $prefix) {
168
            return $keys;
169
        }
170
171
        $filteredKeys = array();
172 View Code Duplication
        foreach (array('keys', 'dirs') as $hash) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
173
            $filteredKeys[$hash] = array();
174
            foreach ($keys[$hash] as $key) {
175
                if (0 === strpos($key, $prefix)) {
176
                    $filteredKeys[$hash][] = $key;
177
                }
178
            }
179
        }
180
181
        return $filteredKeys;
182
    }
183
184
    /**
185
     * {@inheritdoc}
186
     */
187
    public function mtime($key)
188
    {
189
        $this->ensureDirectoryExists($this->directory, $this->create);
190
191
        $mtime = ftp_mdtm($this->getConnection(), $this->computePath($key));
192
193
        // the server does not support this function
194
        if (-1 === $mtime) {
195
            throw new \RuntimeException('Server does not support ftp_mdtm function.');
196
        }
197
198
        return $mtime;
199
    }
200
201
    /**
202
     * {@inheritdoc}
203
     */
204
    public function delete($key)
205
    {
206
        $this->ensureDirectoryExists($this->directory, $this->create);
207
208
        if ($this->isDirectory($key)) {
209
            return ftp_rmdir($this->getConnection(), $this->computePath($key));
210
        }
211
212
        return ftp_delete($this->getConnection(), $this->computePath($key));
213
    }
214
215
    /**
216
     * {@inheritdoc}
217
     */
218
    public function isDirectory($key)
219
    {
220
        $this->ensureDirectoryExists($this->directory, $this->create);
221
222
        return $this->isDir($this->computePath($key));
223
    }
224
225
    /**
226
     * Lists files from the specified directory. If a pattern is
227
     * specified, it only returns files matching it.
228
     *
229
     * @param string $directory The path of the directory to list from
230
     *
231
     * @return array An array of keys and dirs
232
     */
233
    public function listDirectory($directory = '')
234
    {
235
        $this->ensureDirectoryExists($this->directory, $this->create);
236
237
        $directory = preg_replace('/^[\/]*([^\/].*)$/', '/$1', $directory);
238
239
        $items = $this->parseRawlist(
240
            ftp_rawlist($this->getConnection(), '-al '.$this->directory.$directory) ?: array()
241
        );
242
243
        $fileData = $dirs = array();
244
        foreach ($items as $itemData) {
245
            if ('..' === $itemData['name'] || '.' === $itemData['name']) {
246
                continue;
247
            }
248
249
            $item = array(
250
                'name' => $itemData['name'],
251
                'path' => trim(($directory ? $directory.'/' : '').$itemData['name'], '/'),
252
                'time' => $itemData['time'],
253
                'size' => $itemData['size'],
254
            );
255
256
            if ('-' === substr($itemData['perms'], 0, 1)) {
257
                $fileData[$item['path']] = $item;
258
            } elseif ('d' === substr($itemData['perms'], 0, 1)) {
259
                $dirs[] = $item['path'];
260
            }
261
        }
262
263
        $this->fileData = array_merge($fileData, $this->fileData);
264
265
        return array(
266
           'keys' => array_keys($fileData),
267
           'dirs' => $dirs,
268
        );
269
    }
270
271
    /**
272
     * {@inheritdoc}
273
     */
274
    public function createFile($key, Filesystem $filesystem)
275
    {
276
        $this->ensureDirectoryExists($this->directory, $this->create);
277
278
        $file = new File($key, $filesystem);
279
280
        if (!array_key_exists($key, $this->fileData)) {
281
            $dirname = \Gaufrette\Util\Path::dirname($key);
282
            $directory = $dirname == '.' ? '' : $dirname;
283
            $this->listDirectory($directory);
284
        }
285
286
        if (isset($this->fileData[$key])) {
287
            $fileData = $this->fileData[$key];
288
289
            $file->setName($fileData['name']);
290
            $file->setSize($fileData['size']);
291
        }
292
293
        return $file;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $file; (Gaufrette\File) is incompatible with the return type declared by the interface Gaufrette\Adapter\FileFactory::createFile of type Gaufrette\Adapter\File.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
294
    }
295
296
    /**
297
     * @param string $key
298
     *
299
     * @return int
300
     *
301
     * @throws \RuntimeException
302
     */
303
    public function size($key)
304
    {
305
        $this->ensureDirectoryExists($this->directory, $this->create);
306
307
        if (-1 === $size = ftp_size($this->connection, $key)) {
308
            throw new \RuntimeException(sprintf('Unable to fetch the size of "%s".', $key));
309
        }
310
311
        return $size;
312
    }
313
314
    /**
315
     * Ensures the specified directory exists. If it does not, and the create
316
     * parameter is set to TRUE, it tries to create it.
317
     *
318
     * @param string $directory
319
     * @param bool   $create    Whether to create the directory if it does not
320
     *                          exist
321
     *
322
     * @throws RuntimeException if the directory does not exist and could not
323
     *                          be created
324
     */
325
    protected function ensureDirectoryExists($directory, $create = false)
326
    {
327
        if (!$this->isDir($directory)) {
328
            if (!$create) {
329
                throw new \RuntimeException(sprintf('The directory \'%s\' does not exist.', $directory));
330
            }
331
332
            $this->createDirectory($directory);
333
        }
334
    }
335
336
    /**
337
     * Creates the specified directory and its parent directories.
338
     *
339
     * @param string $directory Directory to create
340
     *
341
     * @throws RuntimeException if the directory could not be created
342
     */
343
    protected function createDirectory($directory)
344
    {
345
        // create parent directory if needed
346
        $parent = \Gaufrette\Util\Path::dirname($directory);
347
        if (!$this->isDir($parent)) {
348
            $this->createDirectory($parent);
349
        }
350
351
        // create the specified directory
352
        $created = ftp_mkdir($this->getConnection(), $directory);
353
        if (false === $created) {
354
            throw new \RuntimeException(sprintf('Could not create the \'%s\' directory.', $directory));
355
        }
356
    }
357
358
    /**
359
     * @param string $directory - full directory path
360
     *
361
     * @return bool
362
     */
363
    private function isDir($directory)
364
    {
365
        if ('/' === $directory) {
366
            return true;
367
        }
368
369
        if (!@ftp_chdir($this->getConnection(), $directory)) {
370
            return false;
371
        }
372
373
        // change directory again to return in the base directory
374
        ftp_chdir($this->getConnection(), $this->directory);
375
376
        return true;
377
    }
378
379
    private function fetchKeys($directory = '', $onlyKeys = true)
380
    {
381
        $directory = preg_replace('/^[\/]*([^\/].*)$/', '/$1', $directory);
382
383
        $lines = ftp_rawlist($this->getConnection(), '-alR '.$this->directory.$directory);
384
385
        if (false === $lines) {
386
            return array('keys' => array(), 'dirs' => array());
387
        }
388
389
        $regexDir = '/'.preg_quote($this->directory.$directory, '/').'\/?(.+):$/u';
390
        $regexItem = '/^(?:([d\-\d])\S+)\s+\S+(?:(?:\s+\S+){5})?\s+(\S+)\s+(.+?)$/';
391
392
        $prevLine = null;
393
        $directories = array();
394
        $keys = array('keys' => array(), 'dirs' => array());
395
396
        foreach ((array) $lines as $line) {
397
            if ('' === $prevLine && preg_match($regexDir, $line, $match)) {
398
                $directory = $match[1];
399
                unset($directories[$directory]);
400 View Code Duplication
                if ($onlyKeys) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
401
                    $keys = array(
402
                        'keys' => array_merge($keys['keys'], $keys['dirs']),
403
                        'dirs' => array(),
404
                    );
405
                }
406
            } elseif (preg_match($regexItem, $line, $tokens)) {
407
                $name = $tokens[3];
408
409
                if ('.' === $name || '..' === $name) {
410
                    continue;
411
                }
412
413
                $path = ltrim($directory.'/'.$name, '/');
414
415
                if ('d' === $tokens[1] || '<dir>' === $tokens[2]) {
416
                    $keys['dirs'][] = $path;
417
                    $directories[$path] = true;
418
                } else {
419
                    $keys['keys'][] = $path;
420
                }
421
            }
422
            $prevLine = $line;
423
        }
424
425 View Code Duplication
        if ($onlyKeys) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
426
            $keys = array(
427
                'keys' => array_merge($keys['keys'], $keys['dirs']),
428
                'dirs' => array(),
429
            );
430
        }
431
432
        foreach (array_keys($directories) as $directory) {
433
            $keys = array_merge_recursive($keys, $this->fetchKeys($directory, $onlyKeys));
434
        }
435
436
        return $keys;
437
    }
438
439
    /**
440
     * Parses the given raw list.
441
     *
442
     * @param array $rawlist
443
     *
444
     * @return array
445
     */
446
    private function parseRawlist(array $rawlist)
447
    {
448
        $parsed = array();
449
        foreach ($rawlist as $line) {
450
            $infos = preg_split("/[\s]+/", $line, 9);
451
452
            if ($this->isLinuxListing($infos)) {
453
                $infos[7] = (strrpos($infos[7], ':') != 2) ? ($infos[7].' 00:00') : (date('Y').' '.$infos[7]);
454
                if ('total' !== $infos[0]) {
455
                    $parsed[] = array(
456
                        'perms' => $infos[0],
457
                        'num' => $infos[1],
458
                        'size' => $infos[4],
459
                        'time' => strtotime($infos[5].' '.$infos[6].'. '.$infos[7]),
460
                        'name' => $infos[8],
461
                    );
462
                }
463
            } elseif (count($infos) >= 4) {
464
                $isDir = (boolean) ('<dir>' === $infos[2]);
465
                $parsed[] = array(
466
                    'perms' => $isDir ? 'd' : '-',
467
                    'num' => '',
468
                    'size' => $isDir ? '' : $infos[2],
469
                    'time' => strtotime($infos[0].' '.$infos[1]),
470
                    'name' => $infos[3],
471
                );
472
            }
473
        }
474
475
        return $parsed;
476
    }
477
478
    /**
479
     * Computes the path for the given key.
480
     *
481
     * @param string $key
482
     */
483
    private function computePath($key)
484
    {
485
        return rtrim($this->directory, '/').'/'.$key;
486
    }
487
488
    /**
489
     * Indicates whether the adapter has an open ftp connection.
490
     *
491
     * @return bool
492
     */
493
    private function isConnected()
494
    {
495
        return is_resource($this->connection);
496
    }
497
498
    /**
499
     * Returns an opened ftp connection resource. If the connection is not
500
     * already opened, it open it before.
501
     *
502
     * @return resource The ftp connection
503
     */
504
    private function getConnection()
505
    {
506
        if (!$this->isConnected()) {
507
            $this->connect();
508
        }
509
510
        return $this->connection;
511
    }
512
513
    /**
514
     * Opens the adapter's ftp connection.
515
     *
516
     * @throws RuntimeException if could not connect
517
     */
518
    private function connect()
519
    {
520
        // open ftp connection
521
        if (!$this->ssl) {
522
            $this->connection = ftp_connect($this->host, $this->port, $this->timeout);
523
        } else {
524
            if (function_exists('ftp_ssl_connect')) {
525
                $this->connection = ftp_ssl_connect($this->host, $this->port, $this->timeout);
526
            } else {
527
                throw new \RuntimeException('This Server Has No SSL-FTP Available.');
528
            }
529
        }
530
        if (!$this->connection) {
531
            throw new \RuntimeException(sprintf('Could not connect to \'%s\' (port: %s).', $this->host, $this->port));
532
        }
533
534
        $username = $this->username ?: 'anonymous';
535
        $password = $this->password ?: '';
536
537
        // login ftp user
538
        if (!@ftp_login($this->connection, $username, $password)) {
539
            $this->close();
540
            throw new \RuntimeException(sprintf('Could not login as %s.', $username));
541
        }
542
543
        // switch to passive mode if needed
544
        if ($this->passive && !ftp_pasv($this->connection, true)) {
545
            $this->close();
546
            throw new \RuntimeException('Could not turn passive mode on.');
547
        }
548
549
        // enable utf8 mode if configured
550
        if($this->utf8 == true) {
551
            ftp_raw($this->connection, "OPTS UTF8 ON");
552
        }
553
554
        // ensure the adapter's directory exists
555
        if ('/' !== $this->directory) {
556
            try {
557
                $this->ensureDirectoryExists($this->directory, $this->create);
558
            } catch (\RuntimeException $e) {
559
                $this->close();
560
                throw $e;
561
            }
562
563
            // change the current directory for the adapter's directory
564
            if (!ftp_chdir($this->connection, $this->directory)) {
565
                $this->close();
566
                throw new \RuntimeException(sprintf('Could not change current directory for the \'%s\' directory.', $this->directory));
567
            }
568
        }
569
    }
570
571
    /**
572
     * Closes the adapter's ftp connection.
573
     */
574
    private function close()
575
    {
576
        if ($this->isConnected()) {
577
            ftp_close($this->connection);
578
        }
579
    }
580
581
    private function isLinuxListing($info)
582
    {
583
        return count($info) >= 9;
584
    }
585
}
586