Completed
Push — master ( 0b2179...cf8a23 )
by Anton
07:33 queued 05:04
created

functions.php ➔ download()   B

Complexity

Conditions 6
Paths 9

Size

Total Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
cc 6
nc 9
nop 3
dl 0
loc 26
ccs 0
cts 15
cp 0
crap 42
rs 8.8817
c 0
b 0
f 0
1
<?php
2
/* (c) Anton Medvedev <[email protected]>
3
 *
4
 * For the full copyright and license information, please view the LICENSE
5
 * file that was distributed with this source code.
6
 */
7
8
namespace Deployer;
9
10
use Deployer\Exception\RuntimeException;
11
use Deployer\Host\FileLoader;
12
use Deployer\Host\Host;
13
use Deployer\Host\Localhost;
14
use Deployer\Host\Range;
15
use Deployer\Support\Proxy;
16
use Deployer\Task\Context;
17
use Deployer\Task\GroupTask;
18
use Deployer\Task\Task as T;
19
use Symfony\Component\Console\Input\InputArgument;
20
use Symfony\Component\Console\Input\InputInterface;
21
use Symfony\Component\Console\Input\InputOption;
22
use Symfony\Component\Console\Output\OutputInterface;
23
use Symfony\Component\Console\Question\ChoiceQuestion;
24
use Symfony\Component\Console\Question\ConfirmationQuestion;
25
use Symfony\Component\Console\Question\Question;
26
use function Deployer\Support\array_to_string;
27
28
// There are two types of functions: Deployer dependent and Context dependent.
29
// Deployer dependent function uses in definition stage of recipe and may require Deployer::get() method.
30
// Context dependent function uses while task execution and must require only Context::get() method.
31
// But there is also a third type of functions: mixed. Mixed function uses in definition stage and in task
32
// execution stage. They are acts like two different function, but have same name. Example of such function
33
// is set() func. This function determine in which stage it was called by Context::get() method.
34
35
/**
36
 * @param array ...$hostnames
37
 * @return Host|Host[]|Proxy
38
 */
39
function host(...$hostnames)
40
{
41 2
    $deployer = Deployer::get();
42 2
    $hostnames = Range::expand($hostnames);
43
44
    // Return hosts if has
45 2
    if ($deployer->hosts->has($hostnames[0])) {
46 2
        if (count($hostnames) === 1) {
47 1
            return $deployer->hosts->get($hostnames[0]);
48
        } else {
49 1
            return array_map([$deployer->hosts, 'get'], $hostnames);
50
        }
51
    }
52
53
    // Add otherwise
54 1
    if (count($hostnames) === 1) {
55 1
        $host = new Host($hostnames[0]);
56 1
        $deployer->hosts->set($hostnames[0], $host);
57 1
        return $host;
58
    } else {
59
        $hosts = array_map(function ($hostname) use ($deployer) {
60 1
            $host = new Host($hostname);
61 1
            $deployer->hosts->set($hostname, $host);
62 1
            return $host;
63 1
        }, $hostnames);
64 1
        return new Proxy($hosts);
65
    }
66
}
67
68
/**
69
 * @param array ...$hostnames
70
 * @return Localhost|Localhost[]|Proxy
71
 */
72
function localhost(...$hostnames)
73
{
74 16
    $deployer = Deployer::get();
75 16
    $hostnames = Range::expand($hostnames);
76
77 16
    if (count($hostnames) <= 1) {
78 10
        $host = count($hostnames) === 1 ? new Localhost($hostnames[0]) : new Localhost();
79 10
        $deployer->hosts->set($host->getHostname(), $host);
80 10
        return $host;
81
    } else {
82
        $hosts = array_map(function ($hostname) use ($deployer) {
83 6
            $host = new Localhost($hostname);
84 6
            $deployer->hosts->set($host->getHostname(), $host);
85 6
            return $host;
86 6
        }, $hostnames);
87 6
        return new Proxy($hosts);
88
    }
89
}
90
91
/**
92
 * Load list of hosts from file
93
 *
94
 * @param string $file
95
 * @return Proxy
96
 */
97
function inventory($file)
98
{
99 1
    $deployer = Deployer::get();
100 1
    $fileLoader = new FileLoader();
101 1
    $fileLoader->load($file);
102
103 1
    $hosts = $fileLoader->getHosts();
104 1
    foreach ($hosts as $host) {
105 1
        $deployer->hosts->set($host->getHostname(), $host);
106
    }
107
108 1
    return new Proxy($hosts);
109
}
110
111
/**
112
 * Set task description.
113
 *
114
 * @param string $title
115
 * @return string
116
 */
117
function desc($title = null)
118
{
119 18
    static $store = null;
120
121 18
    if ($title === null) {
122 18
        return $store;
123
    } else {
124 10
        return $store = $title;
125
    }
126
}
127
128
/**
129
 * Define a new task and save to tasks list.
130
 *
131
 * Alternatively get a defined task.
132
 *
133
 * @param string $name Name of current task.
134
 * @param callable|array|string|null $body Callable task, array of other tasks names or nothing to get a defined tasks
135
 * @return Task\Task
136
 * @throws \InvalidArgumentException
137
 */
138
function task($name, $body = null)
139
{
140 18
    $deployer = Deployer::get();
141
142 18
    if (empty($body)) {
143 1
        $task = $deployer->tasks->get($name);
144 1
        return $task;
145
    }
146
147 18
    if (is_callable($body)) {
148 16
        $task = new T($name, $body);
149 17
    } elseif (is_array($body)) {
150 15
        $task = new GroupTask($name, $body);
151 10
    } elseif (is_string($body)) {
152
        $task = new T($name, function () use ($body) {
153 1
            cd('{{release_path}}');
154 1
            run($body);
155 10
        });
156
    } else {
157
        throw new \InvalidArgumentException('Task should be an closure or array of other tasks.');
158
    }
159
160 18
    $deployer->tasks->set($name, $task);
161
162 18
    if (!empty(desc())) {
163 10
        $task->desc(desc());
164 10
        desc(''); // Clear title.
165
    }
166
167 18
    return $task;
168
}
169
170
/**
171
 * Call that task before specified task runs.
172
 *
173
 * @param string $it The task before $that should be run.
174
 * @param string $that The task to be run.
175
 */
176
function before($it, $that)
177
{
178 1
    $deployer = Deployer::get();
179 1
    $beforeTask = $deployer->tasks->get($it);
180
181 1
    $beforeTask->addBefore($that);
182 1
}
183
184
/**
185
 * Call that task after specified task runs.
186
 *
187
 * @param string $it The task after $that should be run.
188
 * @param string $that The task to be run.
189
 */
190
function after($it, $that)
191
{
192 1
    $deployer = Deployer::get();
193 1
    $afterTask = $deployer->tasks->get($it);
194
195 1
    $afterTask->addAfter($that);
196 1
}
197
198
/**
199
 * Setup which task run on failure of first.
200
 *
201
 * @param string $it The task which need to fail so $that should be run.
202
 * @param string $that The task to be run.
203
 */
204
function fail($it, $that)
205
{
206 10
    $deployer = Deployer::get();
207 10
    $deployer->fail->set($it, $that);
208 10
}
209
210
/**
211
 * Add users arguments.
212
 *
213
 * Note what Deployer already has one argument: "stage".
214
 *
215
 * @param string $name
216
 * @param int $mode
217
 * @param string $description
218
 * @param mixed $default
219
 */
220
function argument($name, $mode = null, $description = '', $default = null)
221
{
222
    Deployer::get()->getConsole()->getUserDefinition()->addArgument(
223
        new InputArgument($name, $mode, $description, $default)
224
    );
225
}
226
227
/**
228
 * Add users options.
229
 *
230
 * @param string $name
231
 * @param string $shortcut
232
 * @param int $mode
233
 * @param string $description
234
 * @param mixed $default
235
 */
236
function option($name, $shortcut = null, $mode = null, $description = '', $default = null)
237
{
238 10
    Deployer::get()->getConsole()->getUserDefinition()->addOption(
239 10
        new InputOption($name, $shortcut, $mode, $description, $default)
240
    );
241 10
}
242
243
/**
244
 * Change the current working directory.
245
 *
246
 * @param string $path
247
 */
248
function cd($path)
249
{
250
    try {
251 7
        set('working_path', parse($path));
252
    } catch (RuntimeException $e) {
253
        throw new \Exception('Unable to change directory into "'. $path .'"', 0, $e);
254
    }
255 7
}
256
257
/**
258
 * Execute a callback within a specific directory and revert back to the initial working directory.
259
 *
260
 * @param string $path
261
 * @param callable $callback
262
 */
263
function within($path, $callback)
264
{
265
    $lastWorkingPath = get('working_path', '');
266
    try {
267
        set('working_path', parse($path));
268
        $callback();
269
    } finally {
270
        set('working_path', $lastWorkingPath);
271
    }
272
}
273
274
/**
275
 * Run command.
276
 *
277
 * @param string $command
278
 * @param array $options
279
 * @return string
280
 */
281
function run($command, $options = [])
282
{
283 9
    $host = Context::get()->getHost();
284 9
    $hostname = $host->getHostname();
285
286 9
    $command = parse($command);
287 9
    $workingPath = get('working_path', '');
288
289 9
    if (!empty($workingPath)) {
290 7
        $command = "cd $workingPath && ($command)";
291
    }
292
293 9
    $env = get('env', []) + ($options['env'] ?? []);
294 9
    if (!empty($env)) {
295 1
        $env = array_to_string($env);
296 1
        $command = "export $env; $command";
297
    }
298
299 9
    if ($host instanceof Localhost) {
300 9
        $process = Deployer::get()->processRunner;
301 9
        $output = $process->run($hostname, $command, $options);
302
    } else {
303
        $client = Deployer::get()->sshClient;
304
        $output = $client->run($host, $command, $options);
305
    }
306
307 9
    return rtrim($output);
308
}
309
310
/**
311
 * Execute commands on local machine
312
 *
313
 * @param string $command Command to run locally.
314
 * @param array $options
315
 * @return string Output of command.
316
 */
317
function runLocally($command, $options = [])
318
{
319 2
    $process = Deployer::get()->processRunner;
320 2
    $hostname = 'localhost';
321 2
    $command = parse($command);
322
323 2
    $env = get('env', []) + ($options['env'] ?? []);
324 2
    if (!empty($env)) {
325 1
        $env = array_to_string($env);
326 1
        $command = "export $env; $command";
327
    }
328
329 2
    $output = $process->run($hostname, $command, $options);
330
331 2
    return rtrim($output);
332
}
333
334
/**
335
 * Run test command.
336
 * Example:
337
 *
338
 *     test('[ -d {{release_path}} ]')
339
 *
340
 * @param string $command
341
 * @return bool
342
 */
343
function test($command)
344
{
345 7
    return run("if $command; then echo 'true'; fi") === 'true';
346
}
347
348
/**
349
 * Run test command locally.
350
 * Example:
351
 *
352
 *     testLocally('[ -d {{local_release_path}} ]')
353
 *
354
 * @param string $command
355
 * @return bool
356
 */
357
function testLocally($command)
358
{
359
    return runLocally("if $command; then echo 'true'; fi") === 'true';
360
}
361
362
/**
363
 * Iterate other hosts, allowing to call run func in callback.
364
 *
365
 * @experimental
366
 * @param Host|Host[] $hosts
367
 * @param callable $callback
368
 */
369
function on($hosts, callable $callback)
370
{
371 5
    $input = Context::has() ? input() : null;
372 5
    $output = Context::has() ? output() : null;
373
374 5
    if (!is_array($hosts) && !($hosts instanceof \Traversable)) {
375
        $hosts = [$hosts];
376
    }
377
378 5
    foreach ($hosts as $host) {
379 5
        if ($host instanceof Host) {
380 5
            Context::push(new Context($host, $input, $output));
381
            try {
382 5
                $callback($host);
383 5
            } finally {
384 5
                Context::pop();
385
            }
386
        } else {
387 5
            throw new \InvalidArgumentException("Function on can iterate only on Host instances.");
388
        }
389
    }
390 5
}
391
392
/**
393
 * Return hosts based on roles.
394
 *
395
 * @experimental
396
 * @param string[] $roles
397
 * @return Host[]
398
 */
399
function roles(...$roles)
400
{
401 1
    return Deployer::get()->hostSelector->getByRoles($roles);
402
}
403
404
/**
405
 * Run task
406
 *
407
 * @experimental
408
 * @param string $task
409
 */
410
function invoke($task)
411
{
412 2
    $hosts = [Context::get()->getHost()];
413 2
    $tasks = Deployer::get()->scriptManager->getTasks($task, $hosts);
414
415 2
    $executor = Deployer::get()->seriesExecutor;
416 2
    $executor->run($tasks, $hosts);
417 2
}
418
419
/**
420
 * Upload file or directory to host
421
 *
422
 * @param string $source
423
 * @param string $destination
424
 * @param array $config
425
 */
426
function upload($source, $destination, array $config = [])
427
{
428
    $rsync = Deployer::get()->rsync;
429
    $host = Context::get()->getHost();
430
    $source = parse($source);
431
    $destination = parse($destination);
432
433
    if ($host instanceof Localhost) {
434
        $rsync->call($host->getHostname(), $source, $destination, $config);
435
    } else {
436
        $sshArguments = $host->getSshArguments()->getCliArguments();
437
        if (empty($sshArguments) === false) {
438
            if (!isset($config['options']) || !is_array($config['options'])) {
439
                $config['options'] = [];
440
            }
441
            $config['options'][] = "-e 'ssh $sshArguments'";
442
        }
443
        $rsync->call($host->getHostname(), $source, "$host:$destination", $config);
444
    }
445
}
446
447
/**
448
 * Download file or directory from host
449
 *
450
 * @param string $destination
451
 * @param string $source
452
 * @param array $config
453
 */
454
function download($source, $destination, array $config = [])
455
{
456
    $rsync = Deployer::get()->rsync;
457
    $host = Context::get()->getHost();
458
    $source = parse($source);
459
    $destination = parse($destination);
460
461
    if ($host instanceof Localhost) {
462
        $rsync->call($host->getHostname(), $source, $destination, $config);
463
    } else {
464
        if (!isset($config['options']) || !is_array($config['options'])) {
465
            $config['options'] = [];
466
        }
467
468
        $sshArguments = $host->getSshArguments()->getCliArguments();
469
        if (empty($sshArguments) === false) {
470
            $config['options'][] = "-e 'ssh $sshArguments'";
471
        }
472
473
        if ($host->has("become")) {
474
            $config['options'][]  = "--rsync-path='sudo -H -u " . $host->get('become') . " rsync'";
475
        }
476
477
        $rsync->call($host->getHostname(), "$host:$source", $destination, $config);
478
    }
479
}
480
481
/**
482
 * Writes a message to the output and adds a newline at the end.
483
 * @param string|array $message
484
 * @param int $options
485
 */
486
function writeln($message, $options = 0)
487
{
488 10
    output()->writeln(parse($message), $options);
489 10
}
490
491
/**
492
 * Writes a message to the output.
493
 * @param string $message
494
 * @param int $options
495
 */
496
function write($message, $options = 0)
497
{
498
    output()->write(parse($message), $options);
499
}
500
501
/**
502
 * Setup configuration option.
503
 *
504
 * @param string $name
505
 * @param mixed $value
506
 */
507
function set($name, $value)
508
{
509 11
    if (!Context::has()) {
510 11
        Deployer::setDefault($name, $value);
511
    } else {
512 7
        Context::get()->getConfig()->set($name, $value);
513
    }
514 11
}
515
516
/**
517
 * Merge new config params to existing config array.
518
 *
519
 * @param string $name
520
 * @param array $array
521
 */
522
function add($name, $array)
523
{
524 1
    if (!Context::has()) {
525
        Deployer::addDefault($name, $array);
526
    } else {
527 1
        Context::get()->getConfig()->add($name, $array);
528
    }
529 1
}
530
531
/**
532
 * Get configuration value.
533
 *
534
 * @param string $name
535
 * @param mixed|null $default
536
 * @return mixed
537
 */
538
function get($name, $default = null)
539
{
540 11
    if (!Context::has()) {
541
        return Deployer::getDefault($name, $default);
542
    } else {
543 11
        return Context::get()->getConfig()->get($name, $default);
544
    }
545
}
546
547
/**
548
 * Check if there is such configuration option.
549
 *
550
 * @param string $name
551
 * @return boolean
552
 */
553
function has($name)
554
{
555 5
    if (!Context::has()) {
556
        return Deployer::hasDefault($name);
557
    } else {
558 5
        return Context::get()->getConfig()->has($name);
559
    }
560
}
561
562
/**
563
 * @param string $message
564
 * @param string|null $default
565
 * @param string[]|null $suggestedChoices
566
 * @return string
567
 * @codeCoverageIgnore
568
 */
569
function ask($message, $default = null, $suggestedChoices = null)
570
{
571
    Context::required(__FUNCTION__);
572
573
    if (($suggestedChoices !== null) && (empty($suggestedChoices))) {
574
        throw new \InvalidArgumentException('Suggested choices should not be empty');
575
    }
576
577
    if (isQuiet()) {
578
        return $default;
579
    }
580
581
    $helper = Deployer::get()->getHelper('question');
582
583
    $message = "<question>$message" . (($default === null) ? "" : " [$default]") . "</question> ";
584
585
    $question = new Question($message, $default);
586
587
    if (empty($suggestedChoices) === false) {
588
        $question->setAutocompleterValues($suggestedChoices);
589
    }
590
591
    return $helper->ask(input(), output(), $question);
592
}
593
594
/**
595
 * @param string $message
596
 * @param string[] $availableChoices
597
 * @param string|null $default
598
 * @param bool|false $multiselect
599
 * @return array
600
 * @codeCoverageIgnore
601
 */
602
function askChoice($message, array $availableChoices, $default = null, $multiselect = false)
603
{
604
    Context::required(__FUNCTION__);
605
606
    if (empty($availableChoices)) {
607
        throw new \InvalidArgumentException('Available choices should not be empty');
608
    }
609
610
    if ($default !== null && !array_key_exists($default, $availableChoices)) {
611
        throw new \InvalidArgumentException('Default choice is not available');
612
    }
613
614
    if (isQuiet()) {
615
        if ($default === null) {
616
            $default = key($availableChoices);
617
        }
618
        return [$default => $availableChoices[$default]];
619
    }
620
621
    $helper = Deployer::get()->getHelper('question');
622
623
    $message = "<question>$message" . (($default === null) ? "" : " [$default]") . "</question> ";
624
625
    $question = new ChoiceQuestion($message, $availableChoices, $default);
626
    $question->setMultiselect($multiselect);
627
628
    return $helper->ask(input(), output(), $question);
629
}
630
631
/**
632
 * @param string $message
633
 * @param bool $default
634
 * @return bool
635
 * @codeCoverageIgnore
636
 */
637
function askConfirmation($message, $default = false)
638
{
639
    Context::required(__FUNCTION__);
640
641
    if (isQuiet()) {
642
        return $default;
643
    }
644
645
    $helper = Deployer::get()->getHelper('question');
646
647
    $yesOrNo = $default ? 'Y/n' : 'y/N';
648
    $message = "<question>$message [$yesOrNo]</question> ";
649
650
    $question = new ConfirmationQuestion($message, $default);
651
652
    return $helper->ask(input(), output(), $question);
653
}
654
655
/**
656
 * @param string $message
657
 * @return string
658
 * @codeCoverageIgnore
659
 */
660
function askHiddenResponse($message)
661
{
662
    Context::required(__FUNCTION__);
663
664
    if (isQuiet()) {
665
        return '';
666
    }
667
668
    $helper = Deployer::get()->getHelper('question');
669
670
    $message = "<question>$message</question> ";
671
672
    $question = new Question($message);
673
    $question->setHidden(true);
674
    $question->setHiddenFallback(false);
675
676
    return $helper->ask(input(), output(), $question);
677
}
678
679
/**
680
 * @return InputInterface
681
 */
682
function input()
683
{
684 8
    return Context::get()->getInput();
685
}
686
687
688
/**
689
 * @return OutputInterface
690
 */
691
function output()
692
{
693 14
    return Context::get()->getOutput();
694
}
695
696
/**
697
 * @return bool
698
 */
699
function isQuiet()
700
{
701 3
    return OutputInterface::VERBOSITY_QUIET === output()->getVerbosity();
702
}
703
704
705
/**
706
 * @return bool
707
 */
708
function isVerbose()
709
{
710 1
    return OutputInterface::VERBOSITY_VERBOSE <= output()->getVerbosity();
711
}
712
713
714
/**
715
 * @return bool
716
 */
717
function isVeryVerbose()
718
{
719
    return OutputInterface::VERBOSITY_VERY_VERBOSE <= output()->getVerbosity();
720
}
721
722
723
/**
724
 * @return bool
725
 */
726
function isDebug()
727
{
728
    return OutputInterface::VERBOSITY_DEBUG <= output()->getVerbosity();
729
}
730
731
/**
732
 * Check if command exists
733
 *
734
 * @param string $command
735
 * @return bool
736
 */
737
function commandExist($command)
738
{
739 3
    return test("hash $command 2>/dev/null");
740
}
741
742
function commandSupportsOption($command, $option)
743
{
744 6
    return test("[[ $(man $command 2>&1 || $command -h 2>&1 || $command --help 2>&1) =~ '$option' ]]");
745
}
746
747
/**
748
 * Parse set values.
749
 *
750
 * @param string $value
751
 * @return string
752
 */
753
function parse($value)
754
{
755 14
    return Context::get()->getConfig()->parse($value);
756
}
757
758
function locateBinaryPath($name)
759
{
760 3
    $nameEscaped = escapeshellarg($name);
761
762
    // Try `command`, should cover all Bourne-like shells
763
    // Try `which`, should cover most other cases
764
    // Fallback to `type` command, if the rest fails
765 3
    $path = run("command -v $nameEscaped || which $nameEscaped || type -p $nameEscaped");
766 3
    if ($path) {
767
        // Deal with issue when `type -p` outputs something like `type -ap` in some implementations
768 3
        return trim(str_replace("$name is", "", $path));
769
    }
770
771
    throw new \RuntimeException("Can't locate [$nameEscaped] - neither of [command|which|type] commands are available");
772
}
773