Completed
Push — master ( 8ca833...6b8906 )
by Vladimir
01:55
created

CurlFtpAdapter   B

Complexity

Total Complexity 54

Size/Duplication

Total Lines 558
Duplicated Lines 3.23 %

Coupling/Cohesion

Components 1
Dependencies 5

Importance

Changes 6
Bugs 1 Features 0
Metric Value
wmc 54
c 6
b 1
f 0
lcom 1
cbo 5
dl 18
loc 558
rs 7.0642

27 Methods

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