/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ /* * Since the initial version of this file was developed on the clock on * an NSF grant I should say the following boilerplate: * * This material is based upon work supported by the National Science * Foundaton under Grant No. EIA-0196404. Any opinions, findings, and * conclusions or recommendations expressed in this material are those * of the author and do not necessarily reflect the views of the * National Science Foundation. */ package org.apache.tools.ant.taskdefs.optional.unix; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; import java.util.HashSet; import java.util.Hashtable; import java.util.Iterator; import java.util.Properties; import java.util.Vector; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.DirectoryScanner; import org.apache.tools.ant.Project; import org.apache.tools.ant.dispatch.DispatchTask; import org.apache.tools.ant.dispatch.DispatchUtils; import org.apache.tools.ant.taskdefs.Execute; import org.apache.tools.ant.taskdefs.LogOutputStream; import org.apache.tools.ant.types.FileSet; import org.apache.tools.ant.util.FileUtils; import org.apache.tools.ant.util.SymbolicLinkUtils; /** * Creates, Deletes, Records and Restores Symlinks. * *

This task performs several related operations. In the most trivial * and default usage, it creates a link specified in the link attribute to * a resource specified in the resource attribute. The second usage of this * task is to traverse a directory structure specified by a fileset, * and write a properties file in each included directory describing the * links found in that directory. The third usage is to traverse a * directory structure specified by a fileset, looking for properties files * (also specified as included in the fileset) and recreate the links * that have been previously recorded for each directory. Finally, it can be * used to remove a symlink without deleting the associated resource. * *

Usage examples: * *

Make a link named "foo" to a resource named * "bar.foo" in subdir: *

 * <symlink link="${dir.top}/foo" resource="${dir.top}/subdir/bar.foo"/>
 * 
* *

Record all links in subdir and its descendants in files named * "dir.links": *

 * <symlink action="record" linkfilename="dir.links">
 *    <fileset dir="${dir.top}" includes="subdir/**" />
 * </symlink>
 * 
* *

Recreate the links recorded in the previous example: *

 * <symlink action="recreate">
 *    <fileset dir="${dir.top}" includes="subdir/**/dir.links" />
 * </symlink>
 * 
* *

Delete a link named "foo" to a resource named * "bar.foo" in subdir: *

 * <symlink action="delete" link="${dir.top}/foo"/>
 * 
* *

LIMITATIONS: Because Java has no direct support for * handling symlinks this task divines them by comparing canonical and * absolute paths. On non-unix systems this may cause false positives. * Furthermore, any operating system on which the command * ln -s link resource is not a valid command on the command line * will not be able to use action="delete", action="single" * or action="recreate", but action="record" should still * work. Finally, the lack of support for symlinks in Java means that all links * are recorded as links to the canonical resource name. * Therefore the link: link --> subdir/dir/../foo.bar will be * recorded as link=subdir/foo.bar and restored as * link --> subdir/foo.bar. * */ public class Symlink extends DispatchTask { private static final FileUtils FILE_UTILS = FileUtils.getFileUtils(); private static final SymbolicLinkUtils SYMLINK_UTILS = SymbolicLinkUtils.getSymbolicLinkUtils(); private String resource; private String link; private Vector fileSets = new Vector(); private String linkFileName; private boolean overwrite; private boolean failonerror; private boolean executing = false; /** * Initialize the task. * @throws BuildException on error. */ @Override public void init() throws BuildException { super.init(); setDefaults(); } /** * The standard method for executing any task. * @throws BuildException on error. */ @Override public synchronized void execute() throws BuildException { if (executing) { throw new BuildException( "Infinite recursion detected in Symlink.execute()"); } try { executing = true; DispatchUtils.execute(this); } finally { executing = false; } } /** * Create a symlink. * @throws BuildException on error. * @since Ant 1.7 */ public void single() throws BuildException { try { if (resource == null) { handleError("Must define the resource to symlink to!"); return; } if (link == null) { handleError("Must define the link name for symlink!"); return; } doLink(resource, link); } finally { setDefaults(); } } /** * Delete a symlink. * @throws BuildException on error. * @since Ant 1.7 */ public void delete() throws BuildException { try { if (link == null) { handleError("Must define the link name for symlink!"); return; } log("Removing symlink: " + link); SYMLINK_UTILS.deleteSymbolicLink(FILE_UTILS .resolveFile(new File("."), link), this); } catch (FileNotFoundException fnfe) { handleError(fnfe.toString()); } catch (IOException ioe) { handleError(ioe.toString()); } finally { setDefaults(); } } /** * Restore symlinks. * @throws BuildException on error. * @since Ant 1.7 */ public void recreate() throws BuildException { try { if (fileSets.isEmpty()) { handleError("File set identifying link file(s) " + "required for action recreate"); return; } Properties links = loadLinks(fileSets); for (Iterator kitr = links.keySet().iterator(); kitr.hasNext();) { String lnk = (String) kitr.next(); String res = links.getProperty(lnk); // handle the case where lnk points to a directory (bug 25181) try { File test = new File(lnk); if (!SYMLINK_UTILS.isSymbolicLink(lnk)) { doLink(res, lnk); } else if (!test.getCanonicalPath().equals( new File(res).getCanonicalPath())) { SYMLINK_UTILS.deleteSymbolicLink(test, this); doLink(res, lnk); } // else lnk exists, do nothing } catch (IOException ioe) { handleError("IO exception while creating link"); } } } finally { setDefaults(); } } /** * Record symlinks. * @throws BuildException on error. * @since Ant 1.7 */ public void record() throws BuildException { try { if (fileSets.isEmpty()) { handleError("Fileset identifying links to record required"); return; } if (linkFileName == null) { handleError("Name of file to record links in required"); return; } // create a hashtable to group them by parent directory: Hashtable byDir = new Hashtable(); // get an Iterator of file objects representing links (canonical): for (Iterator litr = findLinks(fileSets).iterator(); litr.hasNext();) { File thisLink = (File) litr.next(); File parent = thisLink.getParentFile(); Vector v = (Vector) byDir.get(parent); if (v == null) { v = new Vector(); byDir.put(parent, v); } v.addElement(thisLink); } // write a Properties file in each directory: for (Iterator dirs = byDir.keySet().iterator(); dirs.hasNext();) { File dir = (File) dirs.next(); Vector linksInDir = (Vector) byDir.get(dir); Properties linksToStore = new Properties(); // fill up a Properties object with link and resource names: for (Iterator dlnk = linksInDir.iterator(); dlnk.hasNext();) { File lnk = (File) dlnk.next(); try { linksToStore.put(lnk.getName(), lnk.getCanonicalPath()); } catch (IOException ioe) { handleError("Couldn't get canonical name of parent link"); } } writePropertyFile(linksToStore, dir); } } finally { setDefaults(); } } /** * Return all variables to their default state for the next invocation. * @since Ant 1.7 */ private void setDefaults() { resource = null; link = null; linkFileName = null; failonerror = true; // default behavior is to fail on an error overwrite = false; // default behavior is to not overwrite setAction("single"); // default behavior is make a single link fileSets.clear(); } /** * Set overwrite mode. If set to false (default) * the task will not overwrite existing links, and may stop the build * if a link already exists depending on the setting of failonerror. * * @param owrite If true overwrite existing links. */ public void setOverwrite(boolean owrite) { this.overwrite = owrite; } /** * Set failonerror mode. If set to true (default) the entire build fails * upon error; otherwise the error is logged and the build will continue. * * @param foe If true throw BuildException on error, else log it. */ public void setFailOnError(boolean foe) { this.failonerror = foe; } /** * Set the action to be performed. May be "single", * "delete", "recreate" or "record". * * @param action The action to perform. */ @Override public void setAction(String action) { super.setAction(action); } /** * Set the name of the link. Used when action = "single". * * @param lnk The name for the link. */ public void setLink(String lnk) { this.link = lnk; } /** * Set the name of the resource to which a link should be created. * Used when action = "single". * * @param src The resource to be linked. */ public void setResource(String src) { this.resource = src; } /** * Set the name of the file to which links will be written. * Used when action = "record". * * @param lf The name of the file to write links to. */ public void setLinkfilename(String lf) { this.linkFileName = lf; } /** * Add a fileset to this task. * * @param set The fileset to add. */ public void addFileset(FileSet set) { fileSets.addElement(set); } /** * Delete a symlink (without deleting the associated resource). * *

This is a convenience method that simply invokes * deleteSymlink(java.io.File). * * @param path A string containing the path of the symlink to delete. * * @throws FileNotFoundException When the path results in a * File that doesn't exist. * @throws IOException If calls to File.rename * or File.delete fail. * @deprecated use * org.apache.tools.ant.util.SymbolicLinkUtils#deleteSymbolicLink * instead */ @Deprecated public static void deleteSymlink(String path) throws IOException, FileNotFoundException { SYMLINK_UTILS.deleteSymbolicLink(new File(path), null); } /** * Delete a symlink (without deleting the associated resource). * *

This is a utility method that removes a unix symlink without removing * the resource that the symlink points to. If it is accidentally invoked * on a real file, the real file will not be harmed.

* *

This method works by * getting the canonical path of the link, using the canonical path to * rename the resource (breaking the link) and then deleting the link. * The resource is then returned to its original name inside a finally * block to ensure that the resource is unharmed even in the event of * an exception.

* *

Since Ant 1.8.0 this method will try to delete the File object if * it reports it wouldn't exist (as symlinks pointing nowhere usually do). * Prior version would throw a FileNotFoundException in that case.

* * @param linkfil A File object of the symlink to delete. * * @throws IOException If calls to File.rename, * File.delete or * File.getCanonicalPath * fail. * @deprecated use * org.apache.tools.ant.util.SymbolicLinkUtils#deleteSymbolicLink * instead */ @Deprecated public static void deleteSymlink(File linkfil) throws IOException { SYMLINK_UTILS.deleteSymbolicLink(linkfil, null); } /** * Write a properties file. This method uses Properties.store * and thus may throw exceptions that occur while writing the file. * * @param properties The properties object to be written. * @param dir The directory for which we are writing the links. * @throws BuildException if the property file could not be written */ private void writePropertyFile(Properties properties, File dir) throws BuildException { BufferedOutputStream bos = null; try { bos = new BufferedOutputStream( new FileOutputStream(new File(dir, linkFileName))); properties.store(bos, "Symlinks from " + dir); } catch (IOException ioe) { throw new BuildException(ioe, getLocation()); } finally { FileUtils.close(bos); } } /** * Handle errors based on the setting of failonerror. * * @param msg The message to log, or include in the * BuildException. * @throws BuildException with the message if failonerror=true */ private void handleError(String msg) { if (failonerror) { throw new BuildException(msg); } log(msg); } /** * Conduct the actual construction of a link. * *

The link is constructed by calling Execute.runCommand. * * @param res The path of the resource we are linking to. * @param lnk The name of the link we wish to make. * @throws BuildException when things go wrong */ private void doLink(String res, String lnk) throws BuildException { File linkfil = new File(lnk); String options = "-s"; if (overwrite) { options += "f"; if (linkfil.exists()) { try { SYMLINK_UTILS.deleteSymbolicLink(linkfil, this); } catch (FileNotFoundException fnfe) { log("Symlink disappeared before it was deleted: " + lnk); } catch (IOException ioe) { log("Unable to overwrite preexisting link or file: " + lnk, ioe, Project.MSG_INFO); } } } String[] cmd = new String[] {"ln", options, res, lnk}; try { Execute.runCommand(this, cmd); } catch (BuildException failedToExecute) { if (failonerror) { throw failedToExecute; } else { //log at the info level, and keep going. log(failedToExecute.getMessage(), failedToExecute, Project.MSG_INFO); } } } /** * Find all the links in all supplied filesets. * *

This method is invoked when the action attribute is * "record". This means that filesets are interpreted * as the directories in which links may be found. * * @param v The filesets specified by the user. * @return A HashSet of File objects containing the * links (with canonical parent directories). */ private HashSet findLinks(Vector v) { HashSet result = new HashSet(); final int size = v.size(); for (int i = 0; i < size; i++) { FileSet fs = (FileSet) v.get(i); DirectoryScanner ds = fs.getDirectoryScanner(getProject()); String[][] fnd = new String[][] {ds.getIncludedFiles(), ds.getIncludedDirectories()}; File dir = fs.getDir(getProject()); for (int j = 0; j < fnd.length; j++) { for (int k = 0; k < fnd[j].length; k++) { try { File f = new File(dir, fnd[j][k]); File pf = f.getParentFile(); String name = f.getName(); if (SYMLINK_UTILS.isSymbolicLink(pf, name)) { result.add(new File(pf.getCanonicalFile(), name)); } } catch (IOException e) { handleError("IOException: " + fnd[j][k] + " omitted"); } } } } return result; } /** * Load links from properties files included in one or more FileSets. * *

This method is only invoked when the action attribute is set to * "recreate". The filesets passed in are assumed to specify the * names of the property files with the link information and the * subdirectories in which to look for them. * * @param v The FileSets for this task. * @return The links to be made. */ private Properties loadLinks(Vector v) { Properties finalList = new Properties(); // loop through the supplied file sets: final int size = v.size(); for (int i = 0; i < size; i++) { FileSet fs = (FileSet) v.elementAt(i); DirectoryScanner ds = new DirectoryScanner(); fs.setupDirectoryScanner(ds, getProject()); ds.setFollowSymlinks(false); ds.scan(); String[] incs = ds.getIncludedFiles(); File dir = fs.getDir(getProject()); // load included files as properties files: for (int j = 0; j < incs.length; j++) { File inc = new File(dir, incs[j]); File pf = inc.getParentFile(); Properties lnks = new Properties(); InputStream is = null; try { is = new BufferedInputStream(new FileInputStream(inc)); lnks.load(is); pf = pf.getCanonicalFile(); } catch (FileNotFoundException fnfe) { handleError("Unable to find " + incs[j] + "; skipping it."); continue; } catch (IOException ioe) { handleError("Unable to open " + incs[j] + " or its parent dir; skipping it."); continue; } finally { FileUtils.close(is); } lnks.list(new PrintStream( new LogOutputStream(this, Project.MSG_INFO))); // Write the contents to our master list of links // This method assumes that all links are defined in // terms of absolute paths, or paths relative to the // working directory: for (Iterator kitr = lnks.keySet().iterator(); kitr.hasNext();) { String key = (String) kitr.next(); finalList.put(new File(pf, key).getAbsolutePath(), lnks.getProperty(key)); } } } return finalList; } }