001/**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018
019package org.apache.hadoop.fs;
020
021import java.io.BufferedReader;
022import java.io.File;
023import java.io.FileNotFoundException;
024import java.io.IOException;
025import java.io.StringReader;
026
027import org.apache.hadoop.io.IOUtils;
028import org.apache.hadoop.util.Shell;
029import org.apache.hadoop.util.Shell.ExitCodeException;
030import org.apache.hadoop.util.Shell.ShellCommandExecutor;
031
032import com.google.common.annotations.VisibleForTesting;
033
034import static java.nio.file.Files.createLink;
035
036/**
037 * Class for creating hardlinks.
038 * Supports Unix/Linux, Windows via winutils , and Mac OS X.
039 * 
040 * The HardLink class was formerly a static inner class of FSUtil,
041 * and the methods provided were blatantly non-thread-safe.
042 * To enable volume-parallel Update snapshots, we now provide static 
043 * threadsafe methods that allocate new buffer string arrays
044 * upon each call.  We also provide an API to hardlink all files in a
045 * directory with a single command, which is up to 128 times more 
046 * efficient - and minimizes the impact of the extra buffer creations.
047 */
048public class HardLink { 
049
050  private static HardLinkCommandGetter getHardLinkCommand;
051  
052  public final LinkStats linkStats; //not static
053  
054  //initialize the command "getters" statically, so can use their 
055  //methods without instantiating the HardLink object
056  static { 
057    if (Shell.WINDOWS) {
058      // Windows
059      getHardLinkCommand = new HardLinkCGWin();
060    } else {
061      // Unix or Linux
062      getHardLinkCommand = new HardLinkCGUnix();
063      //override getLinkCountCommand for the particular Unix variant
064      //Linux is already set as the default - {"stat","-c%h", null}
065      if (Shell.MAC || Shell.FREEBSD) {
066        String[] linkCountCmdTemplate = {"/usr/bin/stat","-f%l", null};
067        HardLinkCGUnix.setLinkCountCmdTemplate(linkCountCmdTemplate);
068      } else if (Shell.SOLARIS) {
069        String[] linkCountCmdTemplate = {"ls","-l", null};
070        HardLinkCGUnix.setLinkCountCmdTemplate(linkCountCmdTemplate);        
071      }
072    }
073  }
074
075  public HardLink() {
076    linkStats = new LinkStats();
077  }
078  
079  /**
080   * This abstract class bridges the OS-dependent implementations of the 
081   * needed functionality for querying link counts.
082   * The particular implementation class is chosen during 
083   * static initialization phase of the HardLink class.
084   * The "getter" methods construct shell command strings.
085   */
086  private static abstract class HardLinkCommandGetter {
087    /**
088     * Get the command string to query the hardlink count of a file
089     */
090    abstract String[] linkCount(File file) throws IOException;
091  }
092  
093  /**
094   * Implementation of HardLinkCommandGetter class for Unix
095   */
096  private static class HardLinkCGUnix extends HardLinkCommandGetter {
097    private static String[] getLinkCountCommand = {"stat","-c%h", null};
098    private static synchronized 
099    void setLinkCountCmdTemplate(String[] template) {
100      //May update this for specific unix variants, 
101      //after static initialization phase
102      getLinkCountCommand = template;
103    }
104
105    /*
106     * @see org.apache.hadoop.fs.HardLink.HardLinkCommandGetter#linkCount(java.io.File)
107     */
108    @Override
109    String[] linkCount(File file) 
110    throws IOException {
111      String[] buf = new String[getLinkCountCommand.length];
112      System.arraycopy(getLinkCountCommand, 0, buf, 0, 
113                       getLinkCountCommand.length);
114      buf[getLinkCountCommand.length - 1] = FileUtil.makeShellPath(file, true);
115      return buf;
116    }
117  }
118  
119  /**
120   * Implementation of HardLinkCommandGetter class for Windows
121   */
122  @VisibleForTesting
123  static class HardLinkCGWin extends HardLinkCommandGetter {
124
125    /**
126     * Build the windows link command. This must not
127     * use an exception-raising reference to WINUTILS, as
128     * some tests examine the command.
129     */
130    @SuppressWarnings("deprecation")
131    static String[] getLinkCountCommand = {
132        Shell.WINUTILS, "hardlink", "stat", null};
133
134    /*
135     * @see org.apache.hadoop.fs.HardLink.HardLinkCommandGetter#linkCount(java.io.File)
136     */
137    @Override
138    String[] linkCount(File file) throws IOException {
139      // trigger the check for winutils
140      Shell.getWinUtilsFile();
141      String[] buf = new String[getLinkCountCommand.length];
142      System.arraycopy(getLinkCountCommand, 0, buf, 0, 
143                       getLinkCountCommand.length);
144      buf[getLinkCountCommand.length - 1] = file.getCanonicalPath();
145      return buf;
146    }
147  }
148
149  /*
150   * ****************************************************
151   * Complexity is above.  User-visible functionality is below
152   * ****************************************************
153   */
154
155  /**
156   * Creates a hardlink 
157   * @param file - existing source file
158   * @param linkName - desired target link file
159   */
160  public static void createHardLink(File file, File linkName) 
161      throws IOException {
162    if (file == null) {
163      throw new IOException(
164          "invalid arguments to createHardLink: source file is null");
165    }
166    if (linkName == null) {
167      throw new IOException(
168          "invalid arguments to createHardLink: link name is null");
169    }
170    createLink(linkName.toPath(), file.toPath());
171  }
172
173  /**
174   * Creates hardlinks from multiple existing files within one parent
175   * directory, into one target directory.
176   * @param parentDir - directory containing source files
177   * @param fileBaseNames - list of path-less file names, as returned by 
178   *                        parentDir.list()
179   * @param linkDir - where the hardlinks should be put. It must already exist.
180   */
181  public static void createHardLinkMult(File parentDir, String[] fileBaseNames,
182      File linkDir) throws IOException {
183    if (parentDir == null) {
184      throw new IOException(
185          "invalid arguments to createHardLinkMult: parent directory is null");
186    }
187    if (linkDir == null) {
188      throw new IOException(
189          "invalid arguments to createHardLinkMult: link directory is null");
190    }
191    if (fileBaseNames == null) {
192      throw new IOException(
193          "invalid arguments to createHardLinkMult: "
194          + "filename list can be empty but not null");
195    }
196    if (!linkDir.exists()) {
197      throw new FileNotFoundException(linkDir + " not found.");
198    }
199    for (String name : fileBaseNames) {
200      createLink(linkDir.toPath().resolve(name),
201                 parentDir.toPath().resolve(name));
202    }
203  }
204
205   /**
206   * Retrieves the number of links to the specified file.
207   */
208  public static int getLinkCount(File fileName) throws IOException {
209    if (fileName == null) {
210      throw new IOException(
211          "invalid argument to getLinkCount: file name is null");
212    }
213    if (!fileName.exists()) {
214      throw new FileNotFoundException(fileName + " not found.");
215    }
216
217    // construct and execute shell command
218    String[] cmd = getHardLinkCommand.linkCount(fileName);
219    String inpMsg = null;
220    String errMsg = null;
221    int exitValue = -1;
222    BufferedReader in = null;
223
224    ShellCommandExecutor shexec = new ShellCommandExecutor(cmd);
225    try {
226      shexec.execute();
227      in = new BufferedReader(new StringReader(shexec.getOutput()));
228      inpMsg = in.readLine();
229      exitValue = shexec.getExitCode();
230      if (inpMsg == null || exitValue != 0) {
231        throw createIOException(fileName, inpMsg, errMsg, exitValue, null);
232      }
233      if (Shell.SOLARIS) {
234        String[] result = inpMsg.split("\\s+");
235        return Integer.parseInt(result[1]);
236      } else {
237        return Integer.parseInt(inpMsg);
238      }
239    } catch (ExitCodeException e) {
240      inpMsg = shexec.getOutput();
241      errMsg = e.getMessage();
242      exitValue = e.getExitCode();
243      throw createIOException(fileName, inpMsg, errMsg, exitValue, e);
244    } catch (NumberFormatException e) {
245      throw createIOException(fileName, inpMsg, errMsg, exitValue, e);
246    } finally {
247      IOUtils.closeStream(in);
248    }
249  }
250  
251  /* Create an IOException for failing to get link count. */
252  private static IOException createIOException(File f, String message,
253      String error, int exitvalue, Exception cause) {
254
255    final String s = "Failed to get link count on file " + f
256        + ": message=" + message
257        + "; error=" + error
258        + "; exit value=" + exitvalue;
259    return (cause == null) ? new IOException(s) : new IOException(s, cause);
260  }
261  
262  
263  /**
264   * HardLink statistics counters and methods.
265   * Not multi-thread safe, obviously.
266   * Init is called during HardLink instantiation, above.
267   * 
268   * These are intended for use by knowledgeable clients, not internally, 
269   * because many of the internal methods are static and can't update these
270   * per-instance counters.
271   */
272  public static class LinkStats {
273    public int countDirs = 0; 
274    public int countSingleLinks = 0; 
275    public int countMultLinks = 0; 
276    public int countFilesMultLinks = 0; 
277    public int countEmptyDirs = 0; 
278    public int countPhysicalFileCopies = 0;
279  
280    public void clear() {
281      countDirs = 0; 
282      countSingleLinks = 0; 
283      countMultLinks = 0; 
284      countFilesMultLinks = 0; 
285      countEmptyDirs = 0; 
286      countPhysicalFileCopies = 0;
287    }
288    
289    public String report() {
290      return "HardLinkStats: " + countDirs + " Directories, including " 
291      + countEmptyDirs + " Empty Directories, " 
292      + countSingleLinks 
293      + " single Link operations, " + countMultLinks 
294      + " multi-Link operations, linking " + countFilesMultLinks 
295      + " files, total " + (countSingleLinks + countFilesMultLinks) 
296      + " linkable files.  Also physically copied " 
297      + countPhysicalFileCopies + " other files.";
298    }
299  }
300}
301