Completed
Push — master ( 8e56f4...9f919b )
by Christian
03:16
created

InstallTask::prepareCommand()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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