Passed
Push — main ( fc9dd0...ba819d )
by Michiel
14:43
created

SymlinkTask::setRelative()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
ccs 0
cts 2
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 2
1
<?php
2
/**
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the LGPL. For more information please see
17
 * <http://phing.info>.
18
 */
19
20
namespace Phing\Task\System;
21
22
use Phing\Exception\BuildException;
23
use Phing\Io\FileSystem;
24
use Phing\Io\File;
25
use Phing\Project;
26
use Phing\Task;
27
use Phing\Type\FileSet;
28
29
/**
30
 * Generates symlinks based on a target / link combination.
31
 * Can also symlink contents of a directory, individually
32
 *
33
 * Single target symlink example:
34
 * <code>
35
 *     <symlink target="/some/shared/file" link="${project.basedir}/htdocs/my_file" />
36
 * </code>
37
 *
38
 * Symlink entire contents of directory
39
 *
40
 * This will go through the contents of "/my/shared/library/*"
41
 * and create a symlink for each entry into ${project.basedir}/library/
42
 * <code>
43
 *     <symlink link="${project.basedir}/library">
44
 *         <fileset dir="/my/shared/library">
45
 *             <include name="*" />
46
 *         </fileset>
47
 *     </symlink>
48
 * </code>
49
 *
50
 * @author  Andrei Serdeliuc <[email protected]>
51
 */
52
class SymlinkTask extends Task
53
{
54
    /**
55
     * What we're symlinking from
56
     *
57
     * (default value: null)
58
     *
59
     * @var string
60
     */
61
    private $linkTarget = null;
62
63
    /**
64
     * Symlink location
65
     *
66
     * (default value: null)
67
     *
68
     * @var string
69
     */
70
    private $link = null;
71
72
    /**
73
     * Collection of filesets
74
     * Used when linking contents of a directory
75
     *
76
     * (default value: array())
77
     *
78
     * @var array
79
     */
80
    private $filesets = [];
81
82
    /**
83
     * Whether to override the symlink if it exists but points
84
     * to a different location
85
     *
86
     * (default value: false)
87
     *
88
     * @var boolean
89
     */
90
    private $overwrite = false;
91
92
    /**
93
     * Whether to create relative symlinks
94
     *
95
     * @var boolean
96
     */
97
    private $relative = false;
98
99
    /**
100
     * setter for linkTarget
101
     *
102
     * @param string $linkTarget
103
     * @return void
104
     */
105 15
    public function setTarget($linkTarget)
106
    {
107 15
        $this->linkTarget = $linkTarget;
108 15
    }
109
110
    /**
111
     * setter for _link
112
     *
113
     * @param string $link
114
     * @return void
115
     */
116 15
    public function setLink($link)
117
    {
118 15
        $this->link = $link;
119 15
    }
120
121
    /**
122
     * creator for _filesets
123
     *
124
     * @return FileSet
125
     */
126
    public function createFileset()
127
    {
128
        $num = array_push($this->filesets, new FileSet());
129
130
        return $this->filesets[$num - 1];
131
    }
132
133
    /**
134
     * setter for _overwrite
135
     *
136
     * @param boolean $overwrite
137
     * @return void
138
     */
139 3
    public function setOverwrite($overwrite)
140
    {
141 3
        $this->overwrite = $overwrite;
142 3
    }
143
144
    /**
145
     * @param boolean $relative
146
     */
147
    public function setRelative($relative)
148
    {
149
        $this->relative = $relative;
150
    }
151
152
    /**
153
     * getter for linkTarget
154
     *
155
     * @return string
156
     * @throws BuildException
157
     */
158 15
    public function getTarget()
159
    {
160 15
        if ($this->linkTarget === null) {
161
            throw new BuildException('Target not set');
162
        }
163
164 15
        return $this->linkTarget;
165
    }
166
167
    /**
168
     * getter for _link
169
     *
170
     * @return string
171
     * @throws BuildException
172
     */
173 15
    public function getLink()
174
    {
175 15
        if ($this->link === null) {
176
            throw new BuildException('Link not set');
177
        }
178
179 15
        return $this->link;
180
    }
181
182
    /**
183
     * getter for _filesets
184
     *
185
     * @return array
186
     */
187 15
    public function getFilesets()
188
    {
189 15
        return $this->filesets;
190
    }
191
192
    /**
193
     * getter for _overwrite
194
     *
195
     * @return boolean
196
     */
197 4
    public function getOverwrite()
198
    {
199 4
        return $this->overwrite;
200
    }
201
202
    /**
203
     * @return boolean
204
     */
205 15
    public function isRelative()
206
    {
207 15
        return $this->relative;
208
    }
209
210
    /**
211
     * Given an existing path, convert it to a path relative to a given starting path.
212
     *
213
     * @param string $endPath Absolute path of target
214
     * @param string $startPath Absolute path where traversal begins
215
     *
216
     * @return string Path of target relative to starting path
217
     */
218
    public function makePathRelative($endPath, $startPath)
219
    {
220
        // Normalize separators on Windows
221
        if ('\\' === DIRECTORY_SEPARATOR) {
222
            $endPath = str_replace('\\', '/', $endPath);
223
            $startPath = str_replace('\\', '/', $startPath);
224
        }
225
226
        // Split the paths into arrays
227
        $startPathArr = explode('/', trim($startPath, '/'));
228
        $endPathArr = explode('/', trim($endPath, '/'));
229
230
        // Find for which directory the common path stops
231
        $index = 0;
232
        while (isset($startPathArr[$index]) && isset($endPathArr[$index]) && $startPathArr[$index] === $endPathArr[$index]) {
233
            ++$index;
234
        }
235
236
        // Determine how deep the start path is relative to the common path (ie, "web/bundles" = 2 levels)
237
        $depth = count($startPathArr) - $index;
238
239
        // Repeated "../" for each level need to reach the common path
240
        $traverser = str_repeat('../', $depth);
241
242
        $endPathRemainder = implode('/', array_slice($endPathArr, $index));
243
244
        // Construct $endPath from traversing to the common path, then to the remaining $endPath
245
        $relativePath = $traverser . ('' !== $endPathRemainder ? $endPathRemainder . '/' : '');
246
247
        return '' === $relativePath ? './' : $relativePath;
248
    }
249
250
    /**
251
     * Generates an array of directories / files to be linked
252
     * If _filesets is empty, returns getTarget()
253
     *
254
     * @return array|string
255
     * @throws BuildException
256
     */
257 15
    protected function getMap()
258
    {
259 15
        $fileSets = $this->getFilesets();
260
261
        // No filesets set
262
        // We're assuming single file / directory
263 15
        if (empty($fileSets)) {
264 15
            return $this->getTarget();
265
        }
266
267
        $targets = [];
268
269
        foreach ($fileSets as $fs) {
270
            if (!($fs instanceof FileSet)) {
271
                continue;
272
            }
273
274
            // We need a directory to store the links
275
            if (!is_dir($this->getLink())) {
276
                throw new BuildException('Link must be an existing directory when using fileset');
277
            }
278
279
            $fromDir = $fs->getDir($this->getProject())->getAbsolutePath();
280
281
            if (!is_dir($fromDir)) {
282
                $this->log('Directory doesn\'t exist: ' . $fromDir, Project::MSG_WARN);
283
                continue;
284
            }
285
286
            $fsTargets = [];
287
288
            $ds = $fs->getDirectoryScanner($this->getProject());
289
290
            $fsTargets = array_merge(
291
                $fsTargets,
292
                $ds->getIncludedDirectories(),
293
                $ds->getIncludedFiles()
294
            );
295
296
            // Add each target to the map
297
            foreach ($fsTargets as $target) {
298
                if (!empty($target)) {
299
                    $targets[$target] = $fromDir . DIRECTORY_SEPARATOR . $target;
300
                }
301
            }
302
        }
303
304
        return $targets;
305
    }
306
307
    /**
308
     * Main entry point for task
309
     *
310
     * @return bool
311
     */
312 15
    public function main()
313
    {
314 15
        $map = $this->getMap();
315
316
        // Single file symlink
317 15
        if (is_string($map)) {
0 ignored issues
show
introduced by
The condition is_string($map) is always true.
Loading history...
318 15
            return $this->symlink($map, $this->getLink());
319
        }
320
321
        // Multiple symlinks
322
        foreach ($map as $name => $targetPath) {
323
            $this->symlink($targetPath, $this->getLink() . DIRECTORY_SEPARATOR . $name);
324
        }
325
326
        return true;
327
    }
328
329
    /**
330
     * Create the actual link
331
     *
332
     * @param string $target
333
     * @param string $link
334
     * @return bool
335
     */
336 15
    protected function symlink($target, $link)
337
    {
338 15
        $fs = FileSystem::getFileSystem();
339
340 15
        if ($this->isRelative()) {
341
            $link = (new File($link))->getAbsolutePath();
342
            $target = rtrim($this->makePathRelative($target, dirname($link)), '/');
343
        }
344
345 15
        if (is_link($link) && @readlink($link) == $target) {
346 1
            $this->log('Link exists: ' . $link, Project::MSG_INFO);
347
348 1
            return true;
349
        }
350
351 15
        if (file_exists($link) || is_link($link)) {
352 4
            if (!$this->getOverwrite()) {
353 1
                $this->log('Not overwriting existing link ' . $link, Project::MSG_ERR);
354
355 1
                return false;
356
            }
357
358 3
            if (is_link($link) || is_file($link)) {
359 2
                $fs->unlink($link);
360 2
                $this->log('Link removed: ' . $link, Project::MSG_INFO);
361
            } else {
362 1
                $fs->rmdir($link, true);
363 1
                $this->log('Directory removed: ' . $link, Project::MSG_INFO);
364
            }
365
        }
366
367 15
        $this->log('Linking: ' . $target . ' to ' . $link, Project::MSG_INFO);
368
369 15
        $fs->symlink($target, $link);
370
371 15
        return true;
372
    }
373
}
374