ReleasesStrategy   C
last analyzed

Complexity

Total Complexity 70

Size/Duplication

Total Lines 522
Duplicated Lines 2.49 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 70
lcom 1
cbo 7
dl 13
loc 522
ccs 0
cts 306
cp 0
rs 5.6163
c 0
b 0
f 0

19 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A setOptions() 0 4 1
A setTransporter() 0 11 3
A getRequiredDirectories() 0 7 1
A getCurrentReleasePath() 0 4 1
A getUploadPath() 0 18 3
A getSubscribedEvents() 0 7 1
A onStagePreExecute() 0 11 3
B onStagePostExecute() 0 14 5
D cleanupOldReleases() 0 115 15
A safeGuard() 0 17 4
B tryAgain() 13 30 5
A array_qsort2() 0 10 3
A putDeferedSharedFiles() 0 21 4
B symlinkSharedFilesAndFolders() 0 40 6
A filterSharedFilesAndFolders() 0 15 3
A updateCurrentReleasePathSymlink() 0 12 1
A prepareUploadPath() 0 16 2
C prepareSharedFilesAndFolders() 0 32 8

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 ReleasesStrategy 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 ReleasesStrategy, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * This file is part of the Conveyor package.
5
 *
6
 * (c) Jeroen Fiege <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Webcreate\Conveyor\Strategy;
13
14
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
15
use Webcreate\Conveyor\Context;
16
use Webcreate\Conveyor\DependencyInjection\TransporterAwareInterface;
17
use Webcreate\Conveyor\Event\StageEvent;
18
use Webcreate\Conveyor\Event\StageEvents;
19
use Webcreate\Conveyor\IO\IOInterface;
20
use Webcreate\Conveyor\Repository\Version;
21
use Webcreate\Conveyor\Transporter\AbstractTransporter;
22
use Webcreate\Conveyor\Transporter\ReadOnlyTransporter;
23
use Webcreate\Conveyor\Transporter\TransactionalTransporterInterface;
24
use Webcreate\Conveyor\Util\FileCollection;
25
use Webcreate\Conveyor\Util\FilePath;
26
27
class ReleasesStrategy implements StrategyInterface, TransporterAwareInterface, EventSubscriberInterface
0 ignored issues
show
Complexity introduced by
This class has a complexity of 70 which exceeds the configured maximum of 50.

The class complexity is the sum of the complexity of all methods. A very high value is usually an indication that your class does not follow the single reponsibility principle and does more than one job.

Some resources for further reading:

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

Loading history...
28
{
29
    /**
30
     * @var AbstractTransporter
31
     */
32
    protected $transporter;
33
34
    protected $options;
35
    protected $io;
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $io. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
36
37
    protected $sharedFiles;
38
    protected $uploadPath;
39
40
    public function __construct(IOInterface $io)
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $io. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
41
    {
42
        $this->io = $io;
43
    }
44
45
    /**
46
     * Sets options
47
     *
48
     * @param  array $options
49
     * @return mixed
50
     */
51
    public function setOptions(array $options)
52
    {
53
        $this->options = $options;
54
    }
55
56
    /**
57
     * Sets transporter
58
     *
59
     * @param $transporter
60
     * @throws \InvalidArgumentException
61
     */
62
    public function setTransporter($transporter)
63
    {
64
        if (
65
            $transporter instanceof TransactionalTransporterInterface
66
            && !$transporter instanceof ReadOnlyTransporter
67
        ) {
68
            throw new \InvalidArgumentException(sprintf('Transporter "%s" is not supported by the releases strategy', get_class($transporter)));
69
        }
70
71
        $this->transporter = $transporter;
72
    }
73
74
    /**
75
     * Returns an array contain the required directories relative
76
     * to the target's basepath
77
     *
78
     * @return string[]
79
     */
80
    public function getRequiredDirectories()
81
    {
82
        return array(
83
            'releases',
84
            'shared',
85
        );
86
    }
87
88
    /**
89
     * Returns the relative path to the current release
90
     *
91
     * @return string
92
     */
93
    public function getCurrentReleasePath()
94
    {
95
        return 'current';
96
    }
97
98
    /**
99
     * Returns the upload path for a specific version. Adds
100
     * a suffix if the path already exists (can happen with a
101
     * full deploy)
102
     *
103
     * @param  \Webcreate\Conveyor\Repository\Version $version
104
     * @return string
105
     */
106
    public function getUploadPath(Version $version)
107
    {
108
        $basepath = $this->transporter->getPath();
109
110
        if (null === $this->uploadPath) {
111
            $suffix = '';
112
            $count = 0;
113
114
            do {
115
                $releasePath = 'releases/' . $version->getName() . '-' . substr($version->getBuild(), 0, 6) . $suffix;
116
                $suffix = '_' . ++$count;
117
            } while ($this->transporter->exists($basepath . '/' . $releasePath));
118
119
            $this->uploadPath = $releasePath;
120
        }
121
122
        return $this->uploadPath;
123
    }
124
125
    /**
126
     * Returns an array of event names this subscriber wants to listen to.
127
     *
128
     * The array keys are event names and the value can be:
129
     *
130
     *  * The method name to call (priority defaults to 0)
131
     *  * An array composed of the method name to call and the priority
132
     *  * An array of arrays composed of the method names to call and respective
133
     *    priorities, or 0 if unset
134
     *
135
     * For instance:
136
     *
137
     *  * array('eventName' => 'methodName')
138
     *  * array('eventName' => array('methodName', $priority))
139
     *  * array('eventName' => array(array('methodName1', $priority), array('methodName2'))
140
     *
141
     * @return array The event names to listen to
142
     *
143
     * @api
144
     */
145
    public static function getSubscribedEvents()
146
    {
147
        return array(
148
            StageEvents::STAGE_PRE_EXECUTE => 'onStagePreExecute',
149
            StageEvents::STAGE_POST_EXECUTE => 'onStagePostExecute',
150
        );
151
    }
152
153
    public function onStagePreExecute(StageEvent $e)
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $e. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
154
    {
155
        if ('deploy.before' === $e->getStageName()) {
156
            $this->uploadPath = null; // reset the cached uploadPath
157
158
            $this->prepareUploadPath($e->getContext());
159
            $this->prepareSharedFilesAndFolders($e->getContext());
160
        } elseif ('transfer' === $e->getStageName()) {
161
            $this->filterSharedFilesAndFolders($e->getContext());
162
        }
163
    }
164
165
    public function onStagePostExecute(StageEvent $e)
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $e. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
166
    {
167
        if ('deploy.after' === $e->getStageName()) {
168
            $this->updateCurrentReleasePathSymlink($e->getContext());
169
        } elseif ('transfer' === $e->getStageName()) {
170
            $this->symlinkSharedFilesAndFolders($e->getContext());
171
172
            if (count($this->sharedFiles) > 0) {
173
                $this->putDeferedSharedFiles($this->sharedFiles, $e->getContext());
174
            }
175
        } elseif ('deploy.final' === $e->getStageName()) {
176
            $this->cleanupOldReleases($this->options['keep']);
177
        }
178
    }
179
180
    /**
181
     * Removes old releases
182
     *
183
     * @param $keep
184
     */
185
    protected function cleanupOldReleases($keep)
186
    {
187
        $basepath   = $this->transporter->getPath();
188
        $uploadPath = $basepath . '/releases';
189
        $transporter = $this->transporter;
190
191
        $this->io->write('Searching for outdated releases');
192
        $this->io->increaseIndention(1);
193
194
        $files = $transporter->ls($uploadPath);
195
196
        $this->array_qsort2($files, 'mtime', $order='DESC');
0 ignored issues
show
Coding Style introduced by
Expected 1 space before = sign of default value
Loading history...
Coding Style introduced by
Expected 1 space after = sign of default value
Loading history...
197
198
        $removableFiles = array();
199
        $i = 0;
200
201
        foreach ($files as $file => $info) {
202
            if ($i > $keep) {
203
                $removableFiles[] = $file;
204
            }
205
206
            $i++;
207
        }
208
209
        if (0 === count($removableFiles)) {
210
            $this->io->write('None found');
211
        }
212
213
        // if it's only 1 directory to remove, just remove it without asking
214
        // for permission of the user
215
        if (1 === count($removableFiles)) {
216
            $file = array_pop($removableFiles);
217
218
            // use safe guard, because transport can throw an exception in case
219
            // of a permission denied
220
            $this->safeGuard(
221
                function () use ($transporter, $uploadPath, $file) {
222
                    $transporter->remove($uploadPath . '/' . $file, true);
223
                }
224
            );
225
        }
226
227
        // More then 1 file to remove? Let's inform the used and ask for permission.
228
        // This can happen if the user decides to turn this feature on after some
229
        // deploys have already been done.
230
        if (count($removableFiles) > 0) {
231
            $askAgain = true;
232
233
            while ($askAgain) {
234
                $askAgain = false; // don't ask again on default
235
236
                $answer = $this->io->select(
237
                    sprintf('Found %d out of %d releases which can be removed, would you like to remove them?', count($removableFiles), count($files)),
238
                    array(
239
                        'y' => 'remove outdated releases step by step',
240
                        'a' => 'remove all outdated releases at once',
241
                        'n' => 'abort and let you manually clean things up',
242
                        'v' => 'view outdated release folders',
243
                    ),
244
                    "a",
0 ignored issues
show
Documentation introduced by
'a' is of type string, but the function expects a boolean|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
245
                    5
0 ignored issues
show
Documentation introduced by
5 is of type integer, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
246
                );
247
248
                switch ($answer) {
249
                    case 'y':
250
                        while ($file = array_pop($removableFiles)) {
251
                            if ($this->io->askConfirmation(sprintf('Would you like to remove %s? [Y/n] ', $file), true)) {
252
                                // use safe guard, because transport can throw an exception in case
253
                                // of a permission denied
254
                                $this->safeGuard(
255
                                    function () use ($transporter, $uploadPath, $file) {
256
                                        $transporter->remove($uploadPath . '/' . $file, true);
257
                                    }
258
                                );
259
                            } else {
260
                                $askAgain = true;
261
                                break;
262
                            }
263
                        }
264
                        break;
265
266
                    case 'a':
267
                        // remove all in reversed order (oldest first)
268
                        while ($file = array_pop($removableFiles)) {
269
                            // use safe guard, because transport can throw an exception in case
270
                            // of a permission denied
271
                            $this->safeGuard(
272
                                function () use ($transporter, $uploadPath, $file) {
273
                                    $transporter->remove($uploadPath . '/' . $file, true);
274
                                }
275
                            );
276
                        }
277
                        break;
278
279
                    case 'n':
280
                        break;
281
282
                    case 'v':
283
                    default:
284
                        $askAgain = true;
285
286
                        foreach ($removableFiles as $file) {
287
                            $info = $files[$file];
288
289
                            $this->io->write(sprintf('[%s] <comment>%s</comment>', $info['mtime']->format('Y-m-d H:i:s'), $uploadPath . '/' . $file));
290
291
                            $i++;
292
                        }
293
                        break;
294
                }
295
            }
296
        }
297
298
        $this->io->decreaseIndention(1);
299
    }
300
301
    /**
302
     * Catches exceptions and asks if you want to retry
303
     *
304
     * @param callable $operation
305
     */
306
    protected function safeGuard($operation)
307
    {
308
        while (true) {
309
            try {
310
                $operation();
311
312
                break;
313
            } catch (\Exception $e) {
314
                $this->io->renderException($e);
315
316
                if (false === $this->tryAgain()) {
317
                    // skip
318
                    break;
319
                }
320
            }
321
        }
322
    }
323
324
    protected function tryAgain()
325
    {
326
        while (true) {
327
            $answer = $this->io->select(
328
                '<info>Select an action</info>',
329
                array(
330
                    'a' => 'abort',
331
                    'r' => 'retry this operation',
332
                    's' => 'skip this operation and continue with the next',
333
                ),
334
                'r'
0 ignored issues
show
Documentation introduced by
'r' is of type string, but the function expects a boolean|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
335
            );
336
337 View Code Duplication
            switch ($answer) {
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...
338
                case "a":
339
                    $this->io->setIndention(0);
340
                    $this->io->write('Aborted.');
341
                    die();
0 ignored issues
show
Coding Style Compatibility introduced by
The method tryAgain() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
342
                    break;
0 ignored issues
show
Unused Code introduced by
break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
343
                case "r":
344
                    return true;
345
                    break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
346
                case "s":
347
                    return false;
348
                    break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
349
            }
350
        }
351
352
        return true;
353
    }
354
355
    /*
356
     * Sort a multidimensional array on a column
357
     *
358
     * For example:
359
     * <code>
360
     * <?php array_qsort2($users, "username", "ASC"); ?>
361
     * </code>
362
     *
363
     * @param array $array  array with hash array
364
     * @param mixed $column key that you want to sort on
365
     * @param enum  $order  asc or desc
366
     */
367
    protected function array_qsort2(&$array, $column=0, $order="ASC")
0 ignored issues
show
Coding Style introduced by
Method name "ReleasesStrategy::array_qsort2" is not in camel caps format
Loading history...
Coding Style introduced by
Incorrect spacing between argument "$column" and equals sign; expected 1 but found 0
Loading history...
Coding Style introduced by
Incorrect spacing between default value and equals sign for argument "$column"; expected 1 but found 0
Loading history...
Coding Style introduced by
Incorrect spacing between argument "$order" and equals sign; expected 1 but found 0
Loading history...
Coding Style introduced by
Incorrect spacing between default value and equals sign for argument "$order"; expected 1 but found 0
Loading history...
368
    {
369
        $oper = ($order == "ASC")?">":"<";
370
371
        if(!is_array($array)) return;
0 ignored issues
show
Coding Style introduced by
Please always use braces to surround the code block of IF statements.
Loading history...
Coding Style Best Practice introduced by
It is generally a best practice to always use braces with control structures.

Adding braces to control structures avoids accidental mistakes as your code changes:

// Without braces (not recommended)
if (true)
    doSomething();

// Recommended
if (true) {
    doSomething();
}
Loading history...
Coding Style introduced by
Expected 1 space after IF keyword; 0 found
Loading history...
372
373
        uasort($array, create_function('$a,$b',"return (\$a['$column'] $oper \$b['$column']);"));
374
375
        reset($array);
376
    }
377
378
    /**
379
     *
380
     * @param FileCollection $sharedFiles
381
     * @param Context        $context
382
     */
383
    protected function putDeferedSharedFiles($sharedFiles, Context $context)
384
    {
385
        $basepath   = $this->transporter->getPath();
386
        $sharedPath = $basepath . '/shared';
387
388
        foreach ($sharedFiles as $file) {
389
            $src = FilePath::join($context->getBuilddir(), $file);
390
            $sharedFilepath = $sharedPath . '/' . $file;
391
392
            if (false === file_exists($src)) {
393
                // @todo we might wanna ask the user if he likes to continue or abort
394
                if ($this->io) {
395
                    $this->io->write(sprintf('<error>Warning</error> <comment>%s</comment> not found', $src));
396
                }
397
398
                continue;
399
            }
400
401
            $this->transporter->put($src, $sharedFilepath);
402
        }
403
    }
404
405
    /**
406
     * Symlinks the shared locations
407
     *
408
     * @param Context $context
409
     */
410
    protected function symlinkSharedFilesAndFolders(Context $context)
411
    {
412
        $basepath   = $this->transporter->getPath();
413
        $sharedPath = $basepath . '/shared';
414
        $uploadPath = $basepath . '/' . $this->getUploadPath($context->getVersion());
415
416
        $shared = (array) $this->options['shared'];
417
418
        // add some white space to the output
419
        if (count($shared) > 0) {
420
            $this->io->write('');
421
        }
422
423
        foreach ($shared as $fileOrFolder) {
424
            $sharedFilepath = $sharedPath . '/' . $fileOrFolder;
425
            $uploadFilepath = $uploadPath . '/' . $fileOrFolder;
426
427
            // make sure the symlink destination doesn't exist
428
            if (true === $this->transporter->exists($uploadFilepath)) {
429
                // Ok, it exists, but it's a symlink... let's assume that it was created
430
                // in an earlier deploy with conveyor
431
                if (true === $this->transporter->isSymlink($uploadFilepath)) {
432
                    continue;
433
                }
434
435
                $answer = $this->io->askConfirmation(
436
                    sprintf('<error>Warning</error> Shared file/folder <info>%s</info> already exists, do you want to overwrite it? (n/Y): ', $uploadFilepath),
437
                    false
438
                );
439
440
                if ($answer) {
441
                    $this->transporter->remove($uploadFilepath, true);
442
                } else {
443
                    continue;
444
                }
445
            }
446
447
            $this->transporter->symlink($sharedFilepath, $uploadFilepath);
448
        }
449
    }
450
451
    /**
452
     * Makes sure we don't upload to locations that are shared
453
     *
454
     * @param Context $context
455
     */
456
    protected function filterSharedFilesAndFolders(Context $context)
457
    {
458
        $this->sharedFiles = new FileCollection($context->getBuilddir());
459
460
        $filesModified = $context->getFilesModified();
461
462
        $shared = (array) $this->options['shared'];
463
        foreach ($shared as $fileOrFolder) {
464
            if ($filesModified->has($fileOrFolder)) {
465
                $filesModified->remove($fileOrFolder);
466
467
                $this->sharedFiles->add($fileOrFolder);
468
            }
469
        }
470
    }
471
472
    /**
473
     * Updates the symlink for the current release
474
     *
475
     * @param Context $context
476
     */
477
    protected function updateCurrentReleasePathSymlink(Context $context)
478
    {
479
        $basepath = $this->transporter->getPath();
480
481
        // add some white space to the output
482
        $this->io->write('');
483
484
        $this->transporter->symlink(
485
            $basepath . '/' . $this->getUploadPath($context->getVersion()),
486
            $basepath . '/' . $this->getCurrentReleasePath()
487
        );
488
    }
489
490
    /**
491
     * Copies the latest version server-side to the uploaddir,
492
     * in case this is a incremental deploy.
493
     *
494
     * @param \Webcreate\Conveyor\Context $context
495
     */
496
    protected function prepareUploadPath($context)
497
    {
498
        $basepath           = $this->transporter->getPath();
499
        $uploadPath         = $basepath . '/' . $this->getUploadPath($context->getVersion());
500
        $currentReleasePath = $basepath . '/' . $this->getCurrentReleasePath() . '/';
501
502
        if (false === $context->isFullDeploy()) {
503
            // add some white space to the output
504
            $this->io->write('');
505
506
            $this->transporter->copy(
507
                $currentReleasePath,
508
                $uploadPath
509
            );
510
        }
511
    }
512
513
    /**
514
     * Creates shared files and folders
515
     */
516
    protected function prepareSharedFilesAndFolders(Context $context)
517
    {
518
        $basepath   = $this->transporter->getPath();
519
        $sharedPath = $basepath . '/shared';
520
521
        $shared = (array) $this->options['shared'];
522
523
        // add some white space to the output
524
        if (count($shared) > 0) {
525
            $this->io->write('');
526
        }
527
528
        foreach ($shared as $fileOrFolder) {
529
            $sharedFilepath = $sharedPath . '/' . $fileOrFolder;
530
            $localFilepath = $context->getBuilddir() . '/' . $fileOrFolder;
531
532
            if (false === $this->transporter->exists($sharedFilepath)) {
533
                // Hmm, the shared entity doesn't exist
534
535
                // is it a directory?
536
                if (is_dir($localFilepath) || '/' === substr($sharedFilepath, -1)) {
537
                    $this->transporter->mkdir($sharedFilepath);
538
                } else {
539
                    $parentDir = dirname($sharedFilepath);
540
                    if (false === $this->transporter->exists($parentDir) && $parentDir != $sharedPath) {
541
                        $this->transporter->mkdir($parentDir);
542
                    }
543
                    $this->transporter->putContent('', $sharedFilepath); // make a dummy file
544
                }
545
            }
546
        }
547
    }
548
}
549