Completed
Push — master ( 6b8906...4dd8f1 )
by Vladimir
10s
created

CurlFtpAdapter   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 561
Duplicated Lines 3.21 %

Coupling/Cohesion

Components 1
Dependencies 5

Importance

Changes 7
Bugs 1 Features 0
Metric Value
wmc 55
c 7
b 1
f 0
lcom 1
cbo 5
dl 18
loc 561
rs 6.8

27 Methods

Rating   Name   Duplication   Size   Complexity  
A copy() 0 10 2
A connect() 0 19 2
A disconnect() 0 7 2
A isConnected() 0 4 1
A write() 0 17 2
A writeStream() 0 18 2
A update() 0 4 1
A updateStream() 0 4 1
A rename() 0 15 2
A delete() 9 9 1
A deleteDir() 9 9 1
A createDir() 0 12 2
A setVisibility() 0 19 3
A read() 0 12 2
A readStream() 0 21 2
A getMetadata() 0 17 3
A getMimetype() 0 10 2
A getTimestamp() 0 12 2
A listDirectoryContents() 0 20 4
A listDirectoryContentsRecursive() 0 20 4
A normalizePermissions() 0 17 1
A normalizePath() 0 15 3
A isPureFtpdServer() 0 10 2
A rawCommand() 0 15 1
A getBaseUri() 0 6 2
A pingConnection() 0 7 2
A setConnectionRoot() 0 14 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like CurlFtpAdapter often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use CurlFtpAdapter, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace VladimirYuldashev\Flysystem;
4
5
use DateTime;
6
use Normalizer;
7
use RuntimeException;
8
use League\Flysystem\Util;
9
use League\Flysystem\Config;
10
use League\Flysystem\Util\MimeType;
11
use League\Flysystem\AdapterInterface;
12
use League\Flysystem\Adapter\AbstractFtpAdapter;
13
14
class CurlFtpAdapter extends AbstractFtpAdapter
15
{
16
    protected $configurable = [
17
        'host',
18
        'port',
19
        'username',
20
        'password',
21
        'root',
22
        'ssl',
23
    ];
24
25
    /** @var Curl */
26
    protected $connection;
27
28
    /** @var bool */
29
    protected $isPureFtpd;
30
31
    /**
32
     * Establish a connection.
33
     */
34
    public function connect()
35
    {
36
        $this->connection = new Curl();
37
        $this->connection->setOptions([
38
            CURLOPT_URL => $this->getBaseUri(),
39
            CURLOPT_USERPWD => $this->getUsername() . ':' . $this->getPassword(),
40
            CURLOPT_SSL_VERIFYPEER => false,
41
            CURLOPT_SSL_VERIFYHOST => false,
42
            CURLOPT_FTPSSLAUTH => CURLFTPAUTH_TLS,
43
            CURLOPT_RETURNTRANSFER => true,
44
        ]);
45
46
        if ($this->ssl) {
47
            $this->connection->setOption(CURLOPT_FTP_SSL, CURLFTPSSL_ALL);
48
        }
49
50
        $this->pingConnection();
51
        $this->setConnectionRoot();
52
    }
53
54
    /**
55
     * Close the connection.
56
     */
57
    public function disconnect()
58
    {
59
        if ($this->connection !== null) {
60
            $this->connection = null;
61
        }
62
        $this->isPureFtpd = null;
63
    }
64
65
    /**
66
     * Check if a connection is active.
67
     *
68
     * @return bool
69
     */
70
    public function isConnected()
71
    {
72
        return $this->connection !== null;
73
    }
74
75
    /**
76
     * Write a new file.
77
     *
78
     * @param string $path
79
     * @param string $contents
80
     * @param Config $config Config object
81
     *
82
     * @return array|false false on failure file meta data on success
83
     */
84
    public function write($path, $contents, Config $config)
85
    {
86
        $stream = fopen('php://temp', 'w+b');
87
        fwrite($stream, $contents);
88
        rewind($stream);
89
90
        $result = $this->writeStream($path, $stream, $config);
91
92
        if ($result === false) {
93
            return false;
94
        }
95
96
        $result['contents'] = $contents;
97
        $result['mimetype'] = Util::guessMimeType($path, $contents);
98
99
        return $result;
100
    }
101
102
    /**
103
     * Write a new file using a stream.
104
     *
105
     * @param string   $path
106
     * @param resource $resource
107
     * @param Config   $config Config object
108
     *
109
     * @return array|false false on failure file meta data on success
110
     */
111
    public function writeStream($path, $resource, Config $config)
112
    {
113
        $connection = $this->getConnection();
114
115
        $result = $connection->exec([
116
            CURLOPT_URL => $this->getBaseUri() . '/' . $path,
117
            CURLOPT_UPLOAD => 1,
118
            CURLOPT_INFILE => $resource,
119
        ]);
120
121
        if ($result === false) {
122
            return false;
123
        }
124
125
        $type = 'file';
126
127
        return compact('type', 'path');
128
    }
129
130
    /**
131
     * Update a file.
132
     *
133
     * @param string $path
134
     * @param string $contents
135
     * @param Config $config Config object
136
     *
137
     * @return array|false false on failure file meta data on success
138
     */
139
    public function update($path, $contents, Config $config)
140
    {
141
        return $this->write($path, $contents, $config);
142
    }
143
144
    /**
145
     * Update a file using a stream.
146
     *
147
     * @param string   $path
148
     * @param resource $resource
149
     * @param Config   $config Config object
150
     *
151
     * @return array|false false on failure file meta data on success
152
     */
153
    public function updateStream($path, $resource, Config $config)
154
    {
155
        return $this->writeStream($path, $resource, $config);
156
    }
157
158
    /**
159
     * Rename a file.
160
     *
161
     * @param string $path
162
     * @param string $newpath
163
     *
164
     * @return bool
165
     */
166
    public function rename($path, $newpath)
167
    {
168
        $connection = $this->getConnection();
169
170
        $response = $this->rawCommand($connection, 'RNFR ' . $path);
171
        list($code) = explode(' ', end($response), 2);
172
        if ((int) $code !== 350) {
173
            return false;
174
        }
175
176
        $response = $this->rawCommand($connection, 'RNTO ' . $newpath);
177
        list($code) = explode(' ', end($response), 2);
178
179
        return (int) $code === 250;
180
    }
181
182
    /**
183
     * Copy a file.
184
     *
185
     * @param string $path
186
     * @param string $newpath
187
     *
188
     * @return bool
189
     */
190
    public function copy($path, $newpath)
191
    {
192
        $file = $this->read($path);
193
194
        if ($file === false) {
195
            return false;
196
        }
197
198
        return $this->write($newpath, $file['contents'], new Config()) !== false;
199
    }
200
201
    /**
202
     * Delete a file.
203
     *
204
     * @param string $path
205
     *
206
     * @return bool
207
     */
208 View Code Duplication
    public function delete($path)
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...
209
    {
210
        $connection = $this->getConnection();
211
212
        $response = $this->rawCommand($connection, 'DELE ' . $path);
213
        list($code) = explode(' ', end($response), 2);
214
215
        return (int) $code === 250;
216
    }
217
218
    /**
219
     * Delete a directory.
220
     *
221
     * @param string $dirname
222
     *
223
     * @return bool
224
     */
225 View Code Duplication
    public function deleteDir($dirname)
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...
226
    {
227
        $connection = $this->getConnection();
228
229
        $response = $this->rawCommand($connection, 'RMD ' . $dirname);
230
        list($code) = explode(' ', end($response), 2);
231
232
        return (int) $code === 250;
233
    }
234
235
    /**
236
     * Create a directory.
237
     *
238
     * @param string $dirname directory name
239
     * @param Config $config
240
     *
241
     * @return array|false
242
     */
243
    public function createDir($dirname, Config $config)
244
    {
245
        $connection = $this->getConnection();
246
247
        $response = $this->rawCommand($connection, 'MKD ' . $dirname);
248
        list($code) = explode(' ', end($response), 2);
249
        if ((int) $code !== 257) {
250
            return false;
251
        }
252
253
        return ['type' => 'dir', 'path' => $dirname];
254
    }
255
256
    /**
257
     * Set the visibility for a file.
258
     *
259
     * @param string $path
260
     * @param string $visibility
261
     *
262
     * @return array|false file meta data
263
     */
264
    public function setVisibility($path, $visibility)
265
    {
266
        $connection = $this->getConnection();
267
268
        if ($visibility === AdapterInterface::VISIBILITY_PUBLIC) {
269
            $mode = $this->getPermPublic();
270
        } else {
271
            $mode = $this->getPermPrivate();
272
        }
273
274
        $request = sprintf('SITE CHMOD %o %s', $mode, $path);
275
        $response = $this->rawCommand($connection, $request);
276
        list($code) = explode(' ', end($response), 2);
277
        if ((int) $code !== 200) {
278
            return false;
279
        }
280
281
        return $this->getMetadata($path);
282
    }
283
284
    /**
285
     * Read a file.
286
     *
287
     * @param string $path
288
     *
289
     * @return array|false
290
     */
291
    public function read($path)
292
    {
293
        if (!$object = $this->readStream($path)) {
294
            return false;
295
        }
296
297
        $object['contents'] = stream_get_contents($object['stream']);
298
        fclose($object['stream']);
299
        unset($object['stream']);
300
301
        return $object;
302
    }
303
304
    /**
305
     * Read a file as a stream.
306
     *
307
     * @param string $path
308
     *
309
     * @return array|false
310
     */
311
    public function readStream($path)
312
    {
313
        $stream = fopen('php://temp', 'w+b');
314
315
        $connection = $this->getConnection();
316
317
        $result = $connection->exec([
318
            CURLOPT_URL => $this->getBaseUri() . '/' . $path,
319
            CURLOPT_FILE => $stream,
320
        ]);
321
322
        if (!$result) {
323
            fclose($stream);
324
325
            return false;
326
        }
327
328
        rewind($stream);
329
330
        return ['type' => 'file', 'path' => $path, 'stream' => $stream];
331
    }
332
333
    /**
334
     * Get all the meta data of a file or directory.
335
     *
336
     * @param string $path
337
     *
338
     * @return array|false
339
     */
340
    public function getMetadata($path)
341
    {
342
        if ($path === '') {
343
            return ['type' => 'dir', 'path' => ''];
344
        }
345
346
        $request = rtrim('LIST -A ' . $this->normalizePath($path));
347
348
        $connection = $this->getConnection();
349
        $result = $connection->exec([CURLOPT_CUSTOMREQUEST => $request]);
350
        if ($result === false) {
351
            return false;
352
        }
353
        $listing = $this->normalizeListing(explode(PHP_EOL, $result), '');
354
355
        return current($listing);
356
    }
357
358
    /**
359
     * Get the mimetype of a file.
360
     *
361
     * @param string $path
362
     *
363
     * @return array|false
364
     */
365
    public function getMimetype($path)
366
    {
367
        if (!$metadata = $this->getMetadata($path)) {
368
            return false;
369
        }
370
371
        $metadata['mimetype'] = MimeType::detectByFilename($path);
372
373
        return $metadata;
374
    }
375
376
    /**
377
     * Get the timestamp of a file.
378
     *
379
     * @param string $path
380
     *
381
     * @return array|false
382
     */
383
    public function getTimestamp($path)
384
    {
385
        $response = $this->rawCommand($this->getConnection(), 'MDTM ' . $path);
386
        list($code, $time) = explode(' ', end($response), 2);
387
        if ($code !== '213') {
388
            return false;
389
        }
390
391
        $datetime = DateTime::createFromFormat('YmdHis', $time);
392
393
        return ['path' => $path, 'timestamp' => $datetime->getTimestamp()];
394
    }
395
396
    /**
397
     * {@inheritdoc}
398
     *
399
     * @param string $directory
400
     */
401
    protected function listDirectoryContents($directory, $recursive = false)
402
    {
403
        if ($recursive === true) {
404
            return $this->listDirectoryContentsRecursive($directory);
405
        }
406
407
        $request = rtrim('LIST -aln ' . $this->normalizePath($directory));
408
409
        $connection = $this->getConnection();
410
        $result = $connection->exec([CURLOPT_CUSTOMREQUEST => $request]);
411
        if ($result === false) {
412
            return false;
413
        }
414
415
        if ($directory === '/') {
416
            $directory = '';
417
        }
418
419
        return $this->normalizeListing(explode(PHP_EOL, $result), $directory);
420
    }
421
422
    /**
423
     * {@inheritdoc}
424
     *
425
     * @param string $directory
426
     */
427
    protected function listDirectoryContentsRecursive($directory)
428
    {
429
        $request = rtrim('LIST -aln ' . $this->normalizePath($directory));
430
431
        $connection = $this->getConnection();
432
        $result = $connection->exec([CURLOPT_CUSTOMREQUEST => $request]);
433
434
        $listing = $this->normalizeListing(explode(PHP_EOL, $result), $directory);
435
        $output = [];
436
437
        foreach ($listing as $item) {
438
            if ($item['type'] === 'file') {
439
                $output[] = $item;
440
            } elseif ($item['type'] === 'dir') {
441
                $output = array_merge($output, $this->listDirectoryContentsRecursive($item['path']));
442
            }
443
        }
444
445
        return $output;
446
    }
447
448
    /**
449
     * Normalize a permissions string.
450
     *
451
     * @param string $permissions
452
     *
453
     * @return int
454
     */
455
    protected function normalizePermissions($permissions)
456
    {
457
        // remove the type identifier
458
        $permissions = substr($permissions, 1);
459
        // map the string rights to the numeric counterparts
460
        $map = ['-' => '0', 'r' => '4', 'w' => '2', 'x' => '1'];
461
        $permissions = strtr($permissions, $map);
462
        // split up the permission groups
463
        $parts = str_split($permissions, 3);
464
        // convert the groups
465
        $mapper = function ($part) {
466
            return array_sum(str_split($part));
467
        };
468
469
        // converts to decimal number
470
        return octdec(implode('', array_map($mapper, $parts)));
471
    }
472
473
    /**
474
     * Normalize path depending on server.
475
     *
476
     * @param string $path
477
     *
478
     * @return string
479
     */
480
    protected function normalizePath($path)
481
    {
482
        if (empty($path)) {
483
            return '';
484
        }
485
        $path = Normalizer::normalize($path);
486
487
        if ($this->isPureFtpdServer()) {
488
            $path = str_replace(' ', '\ ', $path);
489
        }
490
491
        $path = str_replace('*', '\\*', $path);
492
493
        return $path;
494
    }
495
496
    /**
497
     * @return bool
498
     */
499
    protected function isPureFtpdServer()
500
    {
501
        if ($this->isPureFtpd === null) {
502
            $response = $this->rawCommand($this->getConnection(), 'HELP');
503
            $response = end($response);
504
            $this->isPureFtpd = stripos($response, 'Pure-FTPd') !== false;
505
        }
506
507
        return $this->isPureFtpd;
508
    }
509
510
    /**
511
     * Sends an arbitrary command to an FTP server.
512
     *
513
     * @param  Curl   $connection The CURL instance
514
     * @param  string $command    The command to execute
515
     *
516
     * @return array Returns the server's response as an array of strings
517
     */
518
    protected function rawCommand($connection, $command)
519
    {
520
        $response = '';
521
        $callback = function ($ch, $string) use (&$response) {
522
            $response .= $string;
523
524
            return strlen($string);
525
        };
526
        $connection->exec([
527
            CURLOPT_CUSTOMREQUEST => $command,
528
            CURLOPT_HEADERFUNCTION => $callback,
529
        ]);
530
531
        return explode(PHP_EOL, trim($response));
532
    }
533
534
    /**
535
     * Returns the base url of the connection.
536
     *
537
     * @return string
538
     */
539
    protected function getBaseUri()
540
    {
541
        $protocol = $this->ssl ? 'ftps' : 'ftp';
542
543
        return $protocol . '://' . $this->getHost() . ':' . $this->getPort();
544
    }
545
546
    /**
547
     * Check the connection is established.
548
     */
549
    protected function pingConnection()
550
    {
551
        // We can't use the getConnection, because it will lead to an infinite cycle
552
        if ($this->connection->exec() === false) {
553
            throw new RuntimeException('Could not connect to host: ' . $this->getHost() . ', port:' . $this->getPort());
554
        }
555
    }
556
557
    /**
558
     * Set the connection root.
559
     */
560
    protected function setConnectionRoot()
561
    {
562
        $root = $this->getRoot();
563
        if (empty($root)) {
564
            return;
565
        }
566
567
        // We can't use the getConnection, because it will lead to an infinite cycle
568
        $response = $this->rawCommand($this->connection, 'CWD ' . $root);
569
        list($code) = explode(' ', end($response), 2);
570
        if ((int) $code !== 250) {
571
            throw new RuntimeException('Root is invalid or does not exist: ' . $this->getRoot());
572
        }
573
    }
574
}
575