Completed
Push — master ( c612e7...f3e1c8 )
by Christian
06:23
created

InstallTask::moveFile()   C

Complexity

Conditions 8
Paths 8

Size

Total Lines 52
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 52
rs 6.8493
c 1
b 0
f 0
cc 8
eloc 32
nc 8
nop 4

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * This file is part of tenside/core.
5
 *
6
 * (c) Christian Schiffler <[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
 * This project is provided in good faith and hope to be usable by anyone.
12
 *
13
 * @package    tenside/core
14
 * @author     Christian Schiffler <[email protected]>
15
 * @copyright  2015 Christian Schiffler <[email protected]>
16
 * @license    https://github.com/tenside/core/blob/master/LICENSE MIT
17
 * @link       https://github.com/tenside/core
18
 * @filesource
19
 */
20
21
namespace Tenside\Core\Task\Composer;
22
23
use Composer\Command\CreateProjectCommand;
24
use Composer\IO\IOInterface;
25
use Symfony\Component\Console\Input\ArrayInput;
26
use Symfony\Component\Finder\Finder;
27
use Symfony\Component\Finder\SplFileInfo;
28
use Tenside\Core\Task\Task;
29
use Tenside\Core\Task\TaskOutput;
30
31
/**
32
 * This class holds the information for an upgrade of some or all packages.
33
 */
34
class InstallTask extends Task
35
{
36
    /**
37
     * Constant for the package name key.
38
     */
39
    const SETTING_PACKAGE = 'package';
40
41
    /**
42
     * Constant for the package version key.
43
     */
44
    const SETTING_VERSION = 'version';
45
46
    /**
47
     * Constant for the destination dir key.
48
     */
49
    const SETTING_DESTINATION_DIR = 'dest-dir';
50
51
    /**
52
     * Constant for the repository url.
53
     */
54
    const SETTING_REPOSITORY_URL = 'repository-url';
55
56
    /**
57
     * The temporary directory to use.
58
     *
59
     * @var string
60
     */
61
    private $tempDir;
62
63
    /**
64
     * The environment variable storage.
65
     *
66
     * @var string|null
67
     */
68
    private $previousEnvVariable;
69
70
    /**
71
     * The previous working directory.
72
     *
73
     * @var string
74
     */
75
    private $previousWorkingDir;
76
77
    /**
78
     * The list of folders to remove after the installation was complete.
79
     *
80
     * @var string[]
81
     */
82
    private $folders;
83
84
    /**
85
     * {@inheritDoc}
86
     */
87
    public function getType()
88
    {
89
        return 'install';
90
    }
91
92
    /**
93
     * {@inheritDoc}
94
     *
95
     * @throws \RuntimeException When the project directory is not empty or when the installation was not successful.
96
     */
97
    public function doPerform()
98
    {
99
        if (!$this->mayInstall()) {
100
            throw new \RuntimeException('Project directory not empty.');
101
        }
102
103
        // Will throw exception upon error.
104
        $this->prepareTmpDir();
105
106
        $this->preserveEnvironment();
107
108
        try {
109
            $this->fetchProject();
110
            $this->moveFiles();
111
        } catch (\Exception $exception) {
112
            $this->restoreEnvironment();
113
            throw new \RuntimeException('Project could not be created.', 1, $exception);
114
        } finally {
115
            $this->restoreEnvironment();
116
        }
117
        // FIXME: delete recursively?
0 ignored issues
show
Coding Style introduced by
Comment refers to a FIXME task "delete recursively?"
Loading history...
118
        rmdir($this->tempDir);
119
    }
120
121
    /**
122
     * Prepare a temporary directory.
123
     *
124
     * @return void
125
     *
126
     * @throws \RuntimeException When an error occurred.
127
     */
128
    private function prepareTmpDir()
129
    {
130
        $tempDir = $this->file->get(self::SETTING_DESTINATION_DIR) . DIRECTORY_SEPARATOR . uniqid('install-');
131
132
        // If the temporary folder could not be created, error out.
133
        if (!mkdir($tempDir, 0700)) {
134
            throw new \RuntimeException('Could not create the temporary directory');
135
        }
136
137
        $this->tempDir = $tempDir;
138
    }
139
140
    /**
141
     * Fetch the project into the given directory.
142
     *
143
     * @return void
144
     *
145
     * @throws \RuntimeException When an error occurred.
146
     */
147
    private function fetchProject()
148
    {
149
        $arguments = [
150
            'package'   => $this->file->get(self::SETTING_PACKAGE),
151
            'directory' => $this->tempDir,
152
        ];
153
154
        if ($version = $this->file->get(self::SETTING_VERSION)) {
155
            $arguments['version'] = $version;
156
        }
157
158
        if ($repository = $this->file->get(self::SETTING_REPOSITORY_URL)) {
159
            $arguments['--repository-url'] = $repository;
160
        }
161
162
        $command = new CreateProjectCommand();
163
        $input   = new ArrayInput($arguments);
164
        $input->setInteractive(false);
165
        $command->setIO($this->getIO());
166
167
        try {
168
            if (0 !== ($statusCode = $command->run($input, new TaskOutput($this)))) {
169
                throw new \RuntimeException('Command exit code was non zero ' . $statusCode);
170
            }
171
        } catch (\Exception $exception) {
172
            throw new \RuntimeException($exception->getMessage(), $exception->getCode(), $exception);
173
        }
174
    }
175
176
    /**
177
     * Move the installed files to their intended destination.
178
     *
179
     * @return void
180
     */
181
    private function moveFiles()
182
    {
183
        // Ensure we have the file permissions not in cache as new files were installed.
184
        clearstatcache();
185
        // Now move all the files over.
186
        $destinationDir = $this->file->get(self::SETTING_DESTINATION_DIR);
187
        $ioHandler      = $this->getIO();
188
        $logging        = $ioHandler->isVeryVerbose();
189
        $this->folders  = [];
190
        foreach (Finder::create()->in($this->tempDir)->ignoreDotFiles(false)->ignoreVCS(false) as $file) {
191
            $this->moveFile($file, $destinationDir, $logging, $ioHandler);
192
        }
193
194
        foreach (array_reverse($this->folders) as $folder) {
195
            if ($logging) {
196
                $ioHandler->write(sprintf('remove directory %s', $folder));
197
            }
198
            rmdir($folder);
199
        }
200
    }
201
202
    /**
203
     * Move a single file or folder.
204
     *
205
     * @param SplFileInfo $file      The file to move.
206
     *
207
     * @param string      $targetDir The destination directory.
208
     *
209
     * @param bool        $logging   Flag determining if actions shall get logged.
210
     *
211
     * @param IOInterface $ioHandler The io handler to log to.
212
     *
213
     * @return void
214
     *
215
     * @throws \RuntimeException When an unknown file type has been encountered.
216
     */
217
    private function moveFile(SplFileInfo $file, $targetDir, $logging, $ioHandler)
218
    {
219
        $pathName        = $file->getPathname();
220
        $destinationFile = str_replace($this->tempDir, $targetDir, $pathName);
221
222
        // Symlink must(!) be handled first as the isDir() and isFile() checks return true for symlinks.
223
        if ($file->isLink()) {
224
            $target = $file->getLinkTarget();
225
            if ($logging) {
226
                $ioHandler->write(sprintf('link %s to %s', $target, $destinationFile));
227
            }
228
            symlink($target, $destinationFile);
229
            unlink($pathName);
230
231
            return;
232
        }
233
234
        if ($file->isDir()) {
235
            $permissions     = substr(decoct(fileperms($pathName)), 1);
236
            $this->folders[] = $pathName;
237
            if (!is_dir($destinationFile)) {
238
                if ($logging) {
239
                    $ioHandler->write(sprintf('mkdir %s (permissions: %s)', $pathName, $permissions));
240
                }
241
                mkdir($destinationFile, octdec($permissions), true);
242
            }
243
244
            return;
245
        }
246
247
        if ($file->isFile()) {
248
            $permissions = substr(decoct(fileperms($pathName)), 1);
249
            if ($logging) {
250
                $ioHandler->write(
251
                    sprintf('move %s to %s (permissions: %s)', $pathName, $destinationFile, $permissions)
252
                );
253
            }
254
            copy($pathName, $destinationFile);
255
            chmod($destinationFile, octdec($permissions));
256
            unlink($pathName);
257
258
            return;
259
        }
260
261
        throw new \RuntimeException(
262
            sprintf(
263
                'Unknown file of type %s encountered for %s',
264
                filetype($pathName),
265
                $pathName
266
            )
267
        );
268
    }
269
270
    /**
271
     * Check if we may install into the destination directory.
272
     *
273
     * @return bool
274
     */
275
    private function mayInstall()
276
    {
277
        $destinationDir = $this->file->get(self::SETTING_DESTINATION_DIR) . DIRECTORY_SEPARATOR;
278
279
        return !(file_exists($destinationDir . 'composer.json'));
280
    }
281
282
    /**
283
     * Save the current environment variable and working directory.
284
     *
285
     * @return void
286
     */
287
    private function preserveEnvironment()
288
    {
289
        $this->previousEnvVariable = getenv('COMPOSER');
290
        $this->previousWorkingDir  = getcwd();
291
        // Clear any potential overriding env variable.
292
        putenv('COMPOSER=');
293
    }
294
295
    /**
296
     * Restore the current environment variable and working directory.
297
     *
298
     * @return void
299
     */
300
    private function restoreEnvironment()
301
    {
302
        putenv('COMPOSER=' . $this->previousEnvVariable);
303
        chdir($this->previousWorkingDir);
304
    }
305
}
306