1<?php
2
3/*
4 *  $Id: 6efb50d5b7cb94f2f22db6e876010e718aa25b22 $
5 *
6 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
7 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
8 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
9 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
10 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
11 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
12 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
13 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
14 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
15 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
16 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
17 *
18 * This software consists of voluntary contributions made by many individuals
19 * and is licensed under the LGPL. For more information please see
20 * <http://phing.info>.
21 */
22
23require_once "phing/Task.php";
24
25/**
26 * Generates symlinks based on a target / link combination.
27 * Can also symlink contents of a directory, individually
28 *
29 * Single target symlink example:
30 * <code>
31 *     <symlink target="/some/shared/file" link="${project.basedir}/htdocs/my_file" />
32 * </code>
33 *
34 * Symlink entire contents of directory
35 *
36 * This will go through the contents of "/my/shared/library/*"
37 * and create a symlink for each entry into ${project.basedir}/library/
38 * <code>
39 *     <symlink link="${project.basedir}/library">
40 *         <fileset dir="/my/shared/library">
41 *             <include name="*" />
42 *         </fileset>
43 *     </symlink>
44 * </code>
45 *
46 * @author Andrei Serdeliuc <andrei@serdeliuc.ro>
47 * @extends Task
48 * @version $ID$
49 * @package phing.tasks.ext
50 */
51class SymlinkTask extends Task
52{
53    /**
54     * What we're symlinking from
55     *
56     * (default value: null)
57     *
58     * @var string
59     * @access private
60     */
61    private $_target = null;
62
63    /**
64     * Symlink location
65     *
66     * (default value: null)
67     *
68     * @var string
69     * @access private
70     */
71    private $_link = null;
72
73    /**
74     * Collection of filesets
75     * Used when linking contents of a directory
76     *
77     * (default value: array())
78     *
79     * @var array
80     * @access private
81     */
82    private $_filesets = array();
83
84    /**
85     * Whether to override the symlink if it exists but points
86     * to a different location
87     *
88     * (default value: false)
89     *
90     * @var boolean
91     * @access private
92     */
93    private $_overwrite = false;
94
95    /**
96     * setter for _target
97     *
98     * @access public
99     * @param string $target
100     * @return void
101     */
102    public function setTarget($target)
103    {
104        $this->_target = $target;
105    }
106
107    /**
108     * setter for _link
109     *
110     * @access public
111     * @param string $link
112     * @return void
113     */
114    public function setLink($link)
115    {
116        $this->_link = $link;
117    }
118
119    /**
120     * creator for _filesets
121     *
122     * @access public
123     * @return FileSet
124     */
125    public function createFileset()
126    {
127        $num = array_push($this->_filesets, new FileSet());
128        return $this->_filesets[$num-1];
129    }
130
131    /**
132     * setter for _overwrite
133     *
134     * @access public
135     * @param boolean $overwrite
136     * @return void
137     */
138    public function setOverwrite($overwrite)
139    {
140        $this->_overwrite = $overwrite;
141    }
142
143    /**
144     * getter for _target
145     *
146     * @access public
147     * @return string
148     */
149    public function getTarget()
150    {
151        if($this->_target === null) {
152            throw new BuildException('Target not set');
153        }
154
155        return $this->_target;
156    }
157
158    /**
159     * getter for _link
160     *
161     * @access public
162     * @return string
163     */
164    public function getLink()
165    {
166        if($this->_link === null) {
167            throw new BuildException('Link not set');
168        }
169
170        return $this->_link;
171    }
172
173    /**
174     * getter for _filesets
175     *
176     * @access public
177     * @return array
178     */
179    public function getFilesets()
180    {
181        return $this->_filesets;
182    }
183
184    /**
185     * getter for _overwrite
186     *
187     * @access public
188     * @return boolean
189     */
190    public function getOverwrite()
191    {
192        return $this->_overwrite;
193    }
194
195    /**
196     * Generates an array of directories / files to be linked
197     * If _filesets is empty, returns getTarget()
198     *
199     * @access protected
200     * @return array|string
201     */
202    protected function getMap()
203    {
204        $fileSets = $this->getFilesets();
205
206        // No filesets set
207        // We're assuming single file / directory
208        if(empty($fileSets)) {
209            return $this->getTarget();
210        }
211
212        $targets = array();
213
214        foreach($fileSets as $fs) {
215            if(!($fs instanceof FileSet)) {
216                continue;
217            }
218
219            // We need a directory to store the links
220            if(!is_dir($this->getLink())) {
221                throw new BuildException('Link must be an existing directory when using fileset');
222            }
223
224            $fromDir = $fs->getDir($this->getProject())->getAbsolutePath();
225
226            if(!is_dir($fromDir)) {
227                $this->log('Directory doesn\'t exist: ' . $fromDir, Project::MSG_WARN);
228                continue;
229            }
230
231            $fsTargets = array();
232
233            $ds = $fs->getDirectoryScanner($this->getProject());
234
235            $fsTargets = array_merge(
236                $fsTargets,
237                $ds->getIncludedDirectories(),
238                $ds->getIncludedFiles()
239            );
240
241            // Add each target to the map
242            foreach($fsTargets as $target) {
243                if(!empty($target)) {
244                    $targets[$target] = $fromDir . DIRECTORY_SEPARATOR . $target;
245                }
246            }
247        }
248
249        return $targets;
250    }
251
252    /**
253     * Main entry point for task
254     *
255     * @access public
256     * @return bool
257     */
258    public function main()
259    {
260        $map = $this->getMap();
261
262        // Single file symlink
263        if(is_string($map)) {
264            return $this->symlink($map, $this->getLink());
265        }
266
267        // Multiple symlinks
268        foreach($map as $name => $targetPath) {
269            $this->symlink($targetPath, $this->getLink() . DIRECTORY_SEPARATOR . $name);
270        }
271
272        return true;
273    }
274
275    /**
276     * Create the actual link
277     *
278     * @access protected
279     * @param string $target
280     * @param string $link
281     * @return bool
282     */
283    protected function symlink($target, $link)
284    {
285        $fs = FileSystem::getFileSystem();
286
287        if (is_link($link) && readlink($link) == $target) {
288            $this->log('Link exists: ' . $link, Project::MSG_INFO);
289            return true;
290        } elseif (file_exists($link)) {
291            if (!$this->getOverwrite()) {
292                $this->log('Not overwriting existing link ' . $link, Project::MSG_ERR);
293                return false;
294            }
295
296            if (is_link($link) || is_file($link)) {
297                $fs->unlink($link);
298                $this->log('Link removed: ' . $link, Project::MSG_INFO);
299            } else {
300                $fs->rmdir($link, true);
301                $this->log('Directory removed: ' . $link, Project::MSG_INFO);
302            }
303        }
304
305        $this->log('Linking: ' . $target . ' to ' . $link, Project::MSG_INFO);
306
307        return $fs->symlink($target, $link);
308    }
309}
310