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    static String[] getLinkCountCommand = {
126        Shell.WINUTILS, "hardlink", "stat", null};
127
128    /*
129     * @see org.apache.hadoop.fs.HardLink.HardLinkCommandGetter#linkCount(java.io.File)
130     */
131    @Override
132    String[] linkCount(File file) throws IOException {
133      String[] buf = new String[getLinkCountCommand.length];
134      System.arraycopy(getLinkCountCommand, 0, buf, 0, 
135                       getLinkCountCommand.length);
136      buf[getLinkCountCommand.length - 1] = file.getCanonicalPath();
137      return buf;
138    }
139  }
140
141  /*
142   * ****************************************************
143   * Complexity is above.  User-visible functionality is below
144   * ****************************************************
145   */
146
147  /**
148   * Creates a hardlink 
149   * @param file - existing source file
150   * @param linkName - desired target link file
151   */
152  public static void createHardLink(File file, File linkName) 
153      throws IOException {
154    if (file == null) {
155      throw new IOException(
156          "invalid arguments to createHardLink: source file is null");
157    }
158    if (linkName == null) {
159      throw new IOException(
160          "invalid arguments to createHardLink: link name is null");
161    }
162    createLink(linkName.toPath(), file.toPath());
163  }
164
165  /**
166   * Creates hardlinks from multiple existing files within one parent
167   * directory, into one target directory.
168   * @param parentDir - directory containing source files
169   * @param fileBaseNames - list of path-less file names, as returned by 
170   *                        parentDir.list()
171   * @param linkDir - where the hardlinks should be put. It must already exist.
172   */
173  public static void createHardLinkMult(File parentDir, String[] fileBaseNames,
174      File linkDir) throws IOException {
175    if (parentDir == null) {
176      throw new IOException(
177          "invalid arguments to createHardLinkMult: parent directory is null");
178    }
179    if (linkDir == null) {
180      throw new IOException(
181          "invalid arguments to createHardLinkMult: link directory is null");
182    }
183    if (fileBaseNames == null) {
184      throw new IOException(
185          "invalid arguments to createHardLinkMult: "
186          + "filename list can be empty but not null");
187    }
188    if (!linkDir.exists()) {
189      throw new FileNotFoundException(linkDir + " not found.");
190    }
191    for (String name : fileBaseNames) {
192      createLink(linkDir.toPath().resolve(name),
193                 parentDir.toPath().resolve(name));
194    }
195  }
196
197   /**
198   * Retrieves the number of links to the specified file.
199   */
200  public static int getLinkCount(File fileName) throws IOException {
201    if (fileName == null) {
202      throw new IOException(
203          "invalid argument to getLinkCount: file name is null");
204    }
205    if (!fileName.exists()) {
206      throw new FileNotFoundException(fileName + " not found.");
207    }
208
209    // construct and execute shell command
210    String[] cmd = getHardLinkCommand.linkCount(fileName);
211    String inpMsg = null;
212    String errMsg = null;
213    int exitValue = -1;
214    BufferedReader in = null;
215
216    ShellCommandExecutor shexec = new ShellCommandExecutor(cmd);
217    try {
218      shexec.execute();
219      in = new BufferedReader(new StringReader(shexec.getOutput()));
220      inpMsg = in.readLine();
221      exitValue = shexec.getExitCode();
222      if (inpMsg == null || exitValue != 0) {
223        throw createIOException(fileName, inpMsg, errMsg, exitValue, null);
224      }
225      if (Shell.SOLARIS) {
226        String[] result = inpMsg.split("\\s+");
227        return Integer.parseInt(result[1]);
228      } else {
229        return Integer.parseInt(inpMsg);
230      }
231    } catch (ExitCodeException e) {
232      inpMsg = shexec.getOutput();
233      errMsg = e.getMessage();
234      exitValue = e.getExitCode();
235      throw createIOException(fileName, inpMsg, errMsg, exitValue, e);
236    } catch (NumberFormatException e) {
237      throw createIOException(fileName, inpMsg, errMsg, exitValue, e);
238    } finally {
239      IOUtils.closeStream(in);
240    }
241  }
242  
243  /* Create an IOException for failing to get link count. */
244  private static IOException createIOException(File f, String message,
245      String error, int exitvalue, Exception cause) {
246
247    final String s = "Failed to get link count on file " + f
248        + ": message=" + message
249        + "; error=" + error
250        + "; exit value=" + exitvalue;
251    return (cause == null) ? new IOException(s) : new IOException(s, cause);
252  }
253  
254  
255  /**
256   * HardLink statistics counters and methods.
257   * Not multi-thread safe, obviously.
258   * Init is called during HardLink instantiation, above.
259   * 
260   * These are intended for use by knowledgeable clients, not internally, 
261   * because many of the internal methods are static and can't update these
262   * per-instance counters.
263   */
264  public static class LinkStats {
265    public int countDirs = 0; 
266    public int countSingleLinks = 0; 
267    public int countMultLinks = 0; 
268    public int countFilesMultLinks = 0; 
269    public int countEmptyDirs = 0; 
270    public int countPhysicalFileCopies = 0;
271  
272    public void clear() {
273      countDirs = 0; 
274      countSingleLinks = 0; 
275      countMultLinks = 0; 
276      countFilesMultLinks = 0; 
277      countEmptyDirs = 0; 
278      countPhysicalFileCopies = 0;
279    }
280    
281    public String report() {
282      return "HardLinkStats: " + countDirs + " Directories, including " 
283      + countEmptyDirs + " Empty Directories, " 
284      + countSingleLinks 
285      + " single Link operations, " + countMultLinks 
286      + " multi-Link operations, linking " + countFilesMultLinks 
287      + " files, total " + (countSingleLinks + countFilesMultLinks) 
288      + " linkable files.  Also physically copied " 
289      + countPhysicalFileCopies + " other files.";
290    }
291  }
292}
293