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
019
020package org.apache.hadoop.fs;
021
022import com.google.common.annotations.VisibleForTesting;
023
024import java.io.BufferedOutputStream;
025import java.io.DataOutput;
026import java.io.EOFException;
027import java.io.File;
028import java.io.FileInputStream;
029import java.io.FileNotFoundException;
030import java.io.FileOutputStream;
031import java.io.IOException;
032import java.io.OutputStream;
033import java.io.FileDescriptor;
034import java.net.URI;
035import java.nio.ByteBuffer;
036import java.nio.file.Files;
037import java.nio.file.NoSuchFileException;
038import java.nio.file.attribute.BasicFileAttributes;
039import java.nio.file.attribute.BasicFileAttributeView;
040import java.nio.file.attribute.FileTime;
041import java.util.Arrays;
042import java.util.EnumSet;
043import java.util.StringTokenizer;
044
045import org.apache.hadoop.classification.InterfaceAudience;
046import org.apache.hadoop.classification.InterfaceStability;
047import org.apache.hadoop.conf.Configuration;
048import org.apache.hadoop.fs.permission.FsPermission;
049import org.apache.hadoop.io.IOUtils;
050import org.apache.hadoop.io.nativeio.NativeIO;
051import org.apache.hadoop.util.Progressable;
052import org.apache.hadoop.util.Shell;
053import org.apache.hadoop.util.StringUtils;
054
055/****************************************************************
056 * Implement the FileSystem API for the raw local filesystem.
057 *
058 *****************************************************************/
059@InterfaceAudience.Public
060@InterfaceStability.Stable
061public class RawLocalFileSystem extends FileSystem {
062  static final URI NAME = URI.create("file:///");
063  private Path workingDir;
064  // Temporary workaround for HADOOP-9652.
065  private static boolean useDeprecatedFileStatus = true;
066
067  private FsPermission umask;
068
069  @VisibleForTesting
070  public static void useStatIfAvailable() {
071    useDeprecatedFileStatus = !Stat.isAvailable();
072  }
073  
074  public RawLocalFileSystem() {
075    workingDir = getInitialWorkingDirectory();
076  }
077  
078  private Path makeAbsolute(Path f) {
079    if (f.isAbsolute()) {
080      return f;
081    } else {
082      return new Path(workingDir, f);
083    }
084  }
085  
086  /** Convert a path to a File. */
087  public File pathToFile(Path path) {
088    checkPath(path);
089    if (!path.isAbsolute()) {
090      path = new Path(getWorkingDirectory(), path);
091    }
092    return new File(path.toUri().getPath());
093  }
094
095  @Override
096  public URI getUri() { return NAME; }
097  
098  @Override
099  public void initialize(URI uri, Configuration conf) throws IOException {
100    super.initialize(uri, conf);
101    setConf(conf);
102    umask = FsPermission.getUMask(conf);
103  }
104  
105  /*******************************************************
106   * For open()'s FSInputStream.
107   *******************************************************/
108  class LocalFSFileInputStream extends FSInputStream implements HasFileDescriptor {
109    private FileInputStream fis;
110    private long position;
111
112    public LocalFSFileInputStream(Path f) throws IOException {
113      fis = new FileInputStream(pathToFile(f));
114    }
115    
116    @Override
117    public void seek(long pos) throws IOException {
118      if (pos < 0) {
119        throw new EOFException(
120          FSExceptionMessages.NEGATIVE_SEEK);
121      }
122      fis.getChannel().position(pos);
123      this.position = pos;
124    }
125    
126    @Override
127    public long getPos() throws IOException {
128      return this.position;
129    }
130    
131    @Override
132    public boolean seekToNewSource(long targetPos) throws IOException {
133      return false;
134    }
135    
136    /*
137     * Just forward to the fis
138     */
139    @Override
140    public int available() throws IOException { return fis.available(); }
141    @Override
142    public void close() throws IOException { fis.close(); }
143    @Override
144    public boolean markSupported() { return false; }
145    
146    @Override
147    public int read() throws IOException {
148      try {
149        int value = fis.read();
150        if (value >= 0) {
151          this.position++;
152          statistics.incrementBytesRead(1);
153        }
154        return value;
155      } catch (IOException e) {                 // unexpected exception
156        throw new FSError(e);                   // assume native fs error
157      }
158    }
159    
160    @Override
161    public int read(byte[] b, int off, int len) throws IOException {
162      try {
163        int value = fis.read(b, off, len);
164        if (value > 0) {
165          this.position += value;
166          statistics.incrementBytesRead(value);
167        }
168        return value;
169      } catch (IOException e) {                 // unexpected exception
170        throw new FSError(e);                   // assume native fs error
171      }
172    }
173    
174    @Override
175    public int read(long position, byte[] b, int off, int len)
176      throws IOException {
177      ByteBuffer bb = ByteBuffer.wrap(b, off, len);
178      try {
179        int value = fis.getChannel().read(bb, position);
180        if (value > 0) {
181          statistics.incrementBytesRead(value);
182        }
183        return value;
184      } catch (IOException e) {
185        throw new FSError(e);
186      }
187    }
188    
189    @Override
190    public long skip(long n) throws IOException {
191      long value = fis.skip(n);
192      if (value > 0) {
193        this.position += value;
194      }
195      return value;
196    }
197
198    @Override
199    public FileDescriptor getFileDescriptor() throws IOException {
200      return fis.getFD();
201    }
202  }
203  
204  @Override
205  public FSDataInputStream open(Path f, int bufferSize) throws IOException {
206    if (!exists(f)) {
207      throw new FileNotFoundException(f.toString());
208    }
209    return new FSDataInputStream(new BufferedFSInputStream(
210        new LocalFSFileInputStream(f), bufferSize));
211  }
212  
213  /*********************************************************
214   * For create()'s FSOutputStream.
215   *********************************************************/
216  class LocalFSFileOutputStream extends OutputStream {
217    private FileOutputStream fos;
218    
219    private LocalFSFileOutputStream(Path f, boolean append,
220        FsPermission permission) throws IOException {
221      File file = pathToFile(f);
222      if (!append && permission == null) {
223        permission = FsPermission.getFileDefault();
224      }
225      if (permission == null) {
226        this.fos = new FileOutputStream(file, append);
227      } else {
228        permission = permission.applyUMask(umask);
229        if (Shell.WINDOWS && NativeIO.isAvailable()) {
230          this.fos = NativeIO.Windows.createFileOutputStreamWithMode(file,
231              append, permission.toShort());
232        } else {
233          this.fos = new FileOutputStream(file, append);
234          boolean success = false;
235          try {
236            setPermission(f, permission);
237            success = true;
238          } finally {
239            if (!success) {
240              IOUtils.cleanup(LOG, this.fos);
241            }
242          }
243        }
244      }
245    }
246    
247    /*
248     * Just forward to the fos
249     */
250    @Override
251    public void close() throws IOException { fos.close(); }
252    @Override
253    public void flush() throws IOException { fos.flush(); }
254    @Override
255    public void write(byte[] b, int off, int len) throws IOException {
256      try {
257        fos.write(b, off, len);
258      } catch (IOException e) {                // unexpected exception
259        throw new FSError(e);                  // assume native fs error
260      }
261    }
262    
263    @Override
264    public void write(int b) throws IOException {
265      try {
266        fos.write(b);
267      } catch (IOException e) {              // unexpected exception
268        throw new FSError(e);                // assume native fs error
269      }
270    }
271  }
272
273  @Override
274  public FSDataOutputStream append(Path f, int bufferSize,
275      Progressable progress) throws IOException {
276    if (!exists(f)) {
277      throw new FileNotFoundException("File " + f + " not found");
278    }
279    FileStatus status = getFileStatus(f);
280    if (status.isDirectory()) {
281      throw new IOException("Cannot append to a diretory (=" + f + " )");
282    }
283    return new FSDataOutputStream(new BufferedOutputStream(
284        createOutputStreamWithMode(f, true, null), bufferSize), statistics,
285        status.getLen());
286  }
287
288  @Override
289  public FSDataOutputStream create(Path f, boolean overwrite, int bufferSize,
290    short replication, long blockSize, Progressable progress)
291    throws IOException {
292    return create(f, overwrite, true, bufferSize, replication, blockSize,
293        progress, null);
294  }
295
296  private FSDataOutputStream create(Path f, boolean overwrite,
297      boolean createParent, int bufferSize, short replication, long blockSize,
298      Progressable progress, FsPermission permission) throws IOException {
299    if (exists(f) && !overwrite) {
300      throw new FileAlreadyExistsException("File already exists: " + f);
301    }
302    Path parent = f.getParent();
303    if (parent != null && !mkdirs(parent)) {
304      throw new IOException("Mkdirs failed to create " + parent.toString());
305    }
306    return new FSDataOutputStream(new BufferedOutputStream(
307        createOutputStreamWithMode(f, false, permission), bufferSize),
308        statistics);
309  }
310  
311  protected OutputStream createOutputStream(Path f, boolean append) 
312      throws IOException {
313    return createOutputStreamWithMode(f, append, null);
314  }
315
316  protected OutputStream createOutputStreamWithMode(Path f, boolean append,
317      FsPermission permission) throws IOException {
318    return new LocalFSFileOutputStream(f, append, permission);
319  }
320  
321  @Override
322  @Deprecated
323  public FSDataOutputStream createNonRecursive(Path f, FsPermission permission,
324      EnumSet<CreateFlag> flags, int bufferSize, short replication, long blockSize,
325      Progressable progress) throws IOException {
326    if (exists(f) && !flags.contains(CreateFlag.OVERWRITE)) {
327      throw new FileAlreadyExistsException("File already exists: " + f);
328    }
329    return new FSDataOutputStream(new BufferedOutputStream(
330        createOutputStreamWithMode(f, false, permission), bufferSize),
331            statistics);
332  }
333
334  @Override
335  public FSDataOutputStream create(Path f, FsPermission permission,
336    boolean overwrite, int bufferSize, short replication, long blockSize,
337    Progressable progress) throws IOException {
338
339    FSDataOutputStream out = create(f, overwrite, true, bufferSize, replication,
340        blockSize, progress, permission);
341    return out;
342  }
343
344  @Override
345  public FSDataOutputStream createNonRecursive(Path f, FsPermission permission,
346      boolean overwrite,
347      int bufferSize, short replication, long blockSize,
348      Progressable progress) throws IOException {
349    FSDataOutputStream out = create(f, overwrite, false, bufferSize, replication,
350        blockSize, progress, permission);
351    return out;
352  }
353
354  @Override
355  public boolean rename(Path src, Path dst) throws IOException {
356    // Attempt rename using Java API.
357    File srcFile = pathToFile(src);
358    File dstFile = pathToFile(dst);
359    if (srcFile.renameTo(dstFile)) {
360      return true;
361    }
362
363    // Else try POSIX style rename on Windows only
364    if (Shell.WINDOWS &&
365        handleEmptyDstDirectoryOnWindows(src, srcFile, dst, dstFile)) {
366      return true;
367    }
368
369    // The fallback behavior accomplishes the rename by a full copy.
370    if (LOG.isDebugEnabled()) {
371      LOG.debug("Falling through to a copy of " + src + " to " + dst);
372    }
373    return FileUtil.copy(this, src, this, dst, true, getConf());
374  }
375
376  @VisibleForTesting
377  public final boolean handleEmptyDstDirectoryOnWindows(Path src, File srcFile,
378      Path dst, File dstFile) throws IOException {
379
380    // Enforce POSIX rename behavior that a source directory replaces an
381    // existing destination if the destination is an empty directory. On most
382    // platforms, this is already handled by the Java API call above. Some
383    // platforms (notably Windows) do not provide this behavior, so the Java API
384    // call renameTo(dstFile) fails. Delete destination and attempt rename
385    // again.
386    if (this.exists(dst)) {
387      FileStatus sdst = this.getFileStatus(dst);
388      if (sdst.isDirectory() && dstFile.list().length == 0) {
389        if (LOG.isDebugEnabled()) {
390          LOG.debug("Deleting empty destination and renaming " + src + " to " +
391            dst);
392        }
393        if (this.delete(dst, false) && srcFile.renameTo(dstFile)) {
394          return true;
395        }
396      }
397    }
398    return false;
399  }
400
401  @Override
402  public boolean truncate(Path f, final long newLength) throws IOException {
403    FileStatus status = getFileStatus(f);
404    if(status == null) {
405      throw new FileNotFoundException("File " + f + " not found");
406    }
407    if(status.isDirectory()) {
408      throw new IOException("Cannot truncate a directory (=" + f + ")");
409    }
410    long oldLength = status.getLen();
411    if(newLength > oldLength) {
412      throw new IllegalArgumentException(
413          "Cannot truncate to a larger file size. Current size: " + oldLength +
414          ", truncate size: " + newLength + ".");
415    }
416    try (FileOutputStream out = new FileOutputStream(pathToFile(f), true)) {
417      try {
418        out.getChannel().truncate(newLength);
419      } catch(IOException e) {
420        throw new FSError(e);
421      }
422    }
423    return true;
424  }
425  
426  /**
427   * Delete the given path to a file or directory.
428   * @param p the path to delete
429   * @param recursive to delete sub-directories
430   * @return true if the file or directory and all its contents were deleted
431   * @throws IOException if p is non-empty and recursive is false 
432   */
433  @Override
434  public boolean delete(Path p, boolean recursive) throws IOException {
435    File f = pathToFile(p);
436    if (!f.exists()) {
437      //no path, return false "nothing to delete"
438      return false;
439    }
440    if (f.isFile()) {
441      return f.delete();
442    } else if (!recursive && f.isDirectory() && 
443        (FileUtil.listFiles(f).length != 0)) {
444      throw new IOException("Directory " + f.toString() + " is not empty");
445    }
446    return FileUtil.fullyDelete(f);
447  }
448 
449  /**
450   * {@inheritDoc}
451   *
452   * (<b>Note</b>: Returned list is not sorted in any given order,
453   * due to reliance on Java's {@link File#list()} API.)
454   */
455  @Override
456  public FileStatus[] listStatus(Path f) throws IOException {
457    File localf = pathToFile(f);
458    FileStatus[] results;
459
460    if (!localf.exists()) {
461      throw new FileNotFoundException("File " + f + " does not exist");
462    }
463
464    if (localf.isDirectory()) {
465      String[] names = localf.list();
466      if (names == null) {
467        return null;
468      }
469      results = new FileStatus[names.length];
470      int j = 0;
471      for (int i = 0; i < names.length; i++) {
472        try {
473          // Assemble the path using the Path 3 arg constructor to make sure
474          // paths with colon are properly resolved on Linux
475          results[j] = getFileStatus(new Path(f, new Path(null, null,
476                                                          names[i])));
477          j++;
478        } catch (FileNotFoundException e) {
479          // ignore the files not found since the dir list may have have
480          // changed since the names[] list was generated.
481        }
482      }
483      if (j == names.length) {
484        return results;
485      }
486      return Arrays.copyOf(results, j);
487    }
488
489    if (!useDeprecatedFileStatus) {
490      return new FileStatus[] { getFileStatus(f) };
491    }
492    return new FileStatus[] {
493        new DeprecatedRawLocalFileStatus(localf,
494        getDefaultBlockSize(f), this) };
495  }
496  
497  protected boolean mkOneDir(File p2f) throws IOException {
498    return mkOneDirWithMode(new Path(p2f.getAbsolutePath()), p2f, null);
499  }
500
501  protected boolean mkOneDirWithMode(Path p, File p2f, FsPermission permission)
502      throws IOException {
503    if (permission == null) {
504      permission = FsPermission.getDirDefault();
505    }
506    permission = permission.applyUMask(umask);
507    if (Shell.WINDOWS && NativeIO.isAvailable()) {
508      try {
509        NativeIO.Windows.createDirectoryWithMode(p2f, permission.toShort());
510        return true;
511      } catch (IOException e) {
512        if (LOG.isDebugEnabled()) {
513          LOG.debug(String.format(
514              "NativeIO.createDirectoryWithMode error, path = %s, mode = %o",
515              p2f, permission.toShort()), e);
516        }
517        return false;
518      }
519    } else {
520      boolean b = p2f.mkdir();
521      if (b) {
522        setPermission(p, permission);
523      }
524      return b;
525    }
526  }
527
528  /**
529   * Creates the specified directory hierarchy. Does not
530   * treat existence as an error.
531   */
532  @Override
533  public boolean mkdirs(Path f) throws IOException {
534    return mkdirsWithOptionalPermission(f, null);
535  }
536
537  @Override
538  public boolean mkdirs(Path f, FsPermission permission) throws IOException {
539    return mkdirsWithOptionalPermission(f, permission);
540  }
541
542  private boolean mkdirsWithOptionalPermission(Path f, FsPermission permission)
543      throws IOException {
544    if(f == null) {
545      throw new IllegalArgumentException("mkdirs path arg is null");
546    }
547    Path parent = f.getParent();
548    File p2f = pathToFile(f);
549    File parent2f = null;
550    if(parent != null) {
551      parent2f = pathToFile(parent);
552      if(parent2f != null && parent2f.exists() && !parent2f.isDirectory()) {
553        throw new ParentNotDirectoryException("Parent path is not a directory: "
554            + parent);
555      }
556    }
557    if (p2f.exists() && !p2f.isDirectory()) {
558      throw new FileNotFoundException("Destination exists" +
559              " and is not a directory: " + p2f.getCanonicalPath());
560    }
561    return (parent == null || parent2f.exists() || mkdirs(parent)) &&
562      (mkOneDirWithMode(f, p2f, permission) || p2f.isDirectory());
563  }
564  
565  
566  @Override
567  public Path getHomeDirectory() {
568    return this.makeQualified(new Path(System.getProperty("user.home")));
569  }
570
571  /**
572   * Set the working directory to the given directory.
573   */
574  @Override
575  public void setWorkingDirectory(Path newDir) {
576    workingDir = makeAbsolute(newDir);
577    checkPath(workingDir);
578  }
579  
580  @Override
581  public Path getWorkingDirectory() {
582    return workingDir;
583  }
584  
585  @Override
586  protected Path getInitialWorkingDirectory() {
587    return this.makeQualified(new Path(System.getProperty("user.dir")));
588  }
589
590  @Override
591  public FsStatus getStatus(Path p) throws IOException {
592    File partition = pathToFile(p == null ? new Path("/") : p);
593    //File provides getUsableSpace() and getFreeSpace()
594    //File provides no API to obtain used space, assume used = total - free
595    return new FsStatus(partition.getTotalSpace(), 
596      partition.getTotalSpace() - partition.getFreeSpace(),
597      partition.getFreeSpace());
598  }
599  
600  // In the case of the local filesystem, we can just rename the file.
601  @Override
602  public void moveFromLocalFile(Path src, Path dst) throws IOException {
603    rename(src, dst);
604  }
605  
606  // We can write output directly to the final location
607  @Override
608  public Path startLocalOutput(Path fsOutputFile, Path tmpLocalFile)
609    throws IOException {
610    return fsOutputFile;
611  }
612  
613  // It's in the right place - nothing to do.
614  @Override
615  public void completeLocalOutput(Path fsWorkingFile, Path tmpLocalFile)
616    throws IOException {
617  }
618  
619  @Override
620  public void close() throws IOException {
621    super.close();
622  }
623  
624  @Override
625  public String toString() {
626    return "LocalFS";
627  }
628  
629  @Override
630  public FileStatus getFileStatus(Path f) throws IOException {
631    return getFileLinkStatusInternal(f, true);
632  }
633
634  @Deprecated
635  private FileStatus deprecatedGetFileStatus(Path f) throws IOException {
636    File path = pathToFile(f);
637    if (path.exists()) {
638      return new DeprecatedRawLocalFileStatus(pathToFile(f),
639          getDefaultBlockSize(f), this);
640    } else {
641      throw new FileNotFoundException("File " + f + " does not exist");
642    }
643  }
644
645  @Deprecated
646  static class DeprecatedRawLocalFileStatus extends FileStatus {
647    /* We can add extra fields here. It breaks at least CopyFiles.FilePair().
648     * We recognize if the information is already loaded by check if
649     * onwer.equals("").
650     */
651    private boolean isPermissionLoaded() {
652      return !super.getOwner().isEmpty(); 
653    }
654
655    private static long getLastAccessTime(File f) throws IOException {
656      long accessTime;
657      try {
658        accessTime = Files.readAttributes(f.toPath(),
659            BasicFileAttributes.class).lastAccessTime().toMillis();
660      } catch (NoSuchFileException e) {
661        throw new FileNotFoundException("File " + f + " does not exist");
662      }
663      return accessTime;
664    }
665
666    DeprecatedRawLocalFileStatus(File f, long defaultBlockSize, FileSystem fs)
667      throws IOException {
668      super(f.length(), f.isDirectory(), 1, defaultBlockSize,
669          f.lastModified(), getLastAccessTime(f),
670          null, null, null,
671          new Path(f.getPath()).makeQualified(fs.getUri(),
672            fs.getWorkingDirectory()));
673    }
674    
675    @Override
676    public FsPermission getPermission() {
677      if (!isPermissionLoaded()) {
678        loadPermissionInfo();
679      }
680      return super.getPermission();
681    }
682
683    @Override
684    public String getOwner() {
685      if (!isPermissionLoaded()) {
686        loadPermissionInfo();
687      }
688      return super.getOwner();
689    }
690
691    @Override
692    public String getGroup() {
693      if (!isPermissionLoaded()) {
694        loadPermissionInfo();
695      }
696      return super.getGroup();
697    }
698
699    /// loads permissions, owner, and group from `ls -ld`
700    private void loadPermissionInfo() {
701      IOException e = null;
702      try {
703        String output = FileUtil.execCommand(new File(getPath().toUri()), 
704            Shell.getGetPermissionCommand());
705        StringTokenizer t =
706            new StringTokenizer(output, Shell.TOKEN_SEPARATOR_REGEX);
707        //expected format
708        //-rw-------    1 username groupname ...
709        String permission = t.nextToken();
710        if (permission.length() > FsPermission.MAX_PERMISSION_LENGTH) {
711          //files with ACLs might have a '+'
712          permission = permission.substring(0,
713            FsPermission.MAX_PERMISSION_LENGTH);
714        }
715        setPermission(FsPermission.valueOf(permission));
716        t.nextToken();
717
718        String owner = t.nextToken();
719        // If on windows domain, token format is DOMAIN\\user and we want to
720        // extract only the user name
721        if (Shell.WINDOWS) {
722          int i = owner.indexOf('\\');
723          if (i != -1)
724            owner = owner.substring(i + 1);
725        }
726        setOwner(owner);
727
728        setGroup(t.nextToken());
729      } catch (Shell.ExitCodeException ioe) {
730        if (ioe.getExitCode() != 1) {
731          e = ioe;
732        } else {
733          setPermission(null);
734          setOwner(null);
735          setGroup(null);
736        }
737      } catch (IOException ioe) {
738        e = ioe;
739      } finally {
740        if (e != null) {
741          throw new RuntimeException("Error while running command to get " +
742                                     "file permissions : " + 
743                                     StringUtils.stringifyException(e));
744        }
745      }
746    }
747
748    @Override
749    public void write(DataOutput out) throws IOException {
750      if (!isPermissionLoaded()) {
751        loadPermissionInfo();
752      }
753      super.write(out);
754    }
755  }
756
757  /**
758   * Use the command chown to set owner.
759   */
760  @Override
761  public void setOwner(Path p, String username, String groupname)
762    throws IOException {
763    FileUtil.setOwner(pathToFile(p), username, groupname);
764  }
765
766  /**
767   * Use the command chmod to set permission.
768   */
769  @Override
770  public void setPermission(Path p, FsPermission permission)
771    throws IOException {
772    if (NativeIO.isAvailable()) {
773      NativeIO.POSIX.chmod(pathToFile(p).getCanonicalPath(),
774                     permission.toShort());
775    } else {
776      String perm = String.format("%04o", permission.toShort());
777      Shell.execCommand(Shell.getSetPermissionCommand(perm, false,
778        FileUtil.makeShellPath(pathToFile(p), true)));
779    }
780  }
781 
782  /**
783   * Sets the {@link Path}'s last modified time and last access time to
784   * the given valid times.
785   *
786   * @param mtime the modification time to set (only if no less than zero).
787   * @param atime the access time to set (only if no less than zero).
788   * @throws IOException if setting the times fails.
789   */
790  @Override
791  public void setTimes(Path p, long mtime, long atime) throws IOException {
792    try {
793      BasicFileAttributeView view = Files.getFileAttributeView(
794          pathToFile(p).toPath(), BasicFileAttributeView.class);
795      FileTime fmtime = (mtime >= 0) ? FileTime.fromMillis(mtime) : null;
796      FileTime fatime = (atime >= 0) ? FileTime.fromMillis(atime) : null;
797      view.setTimes(fmtime, fatime, null);
798    } catch (NoSuchFileException e) {
799      throw new FileNotFoundException("File " + p + " does not exist");
800    }
801  }
802
803  @Override
804  public boolean supportsSymlinks() {
805    return true;
806  }
807
808  @SuppressWarnings("deprecation")
809  @Override
810  public void createSymlink(Path target, Path link, boolean createParent)
811      throws IOException {
812    if (!FileSystem.areSymlinksEnabled()) {
813      throw new UnsupportedOperationException("Symlinks not supported");
814    }
815    final String targetScheme = target.toUri().getScheme();
816    if (targetScheme != null && !"file".equals(targetScheme)) {
817      throw new IOException("Unable to create symlink to non-local file "+
818                            "system: "+target.toString());
819    }
820    if (createParent) {
821      mkdirs(link.getParent());
822    }
823
824    // NB: Use createSymbolicLink in java.nio.file.Path once available
825    int result = FileUtil.symLink(target.toString(),
826        makeAbsolute(link).toString());
827    if (result != 0) {
828      throw new IOException("Error " + result + " creating symlink " +
829          link + " to " + target);
830    }
831  }
832
833  /**
834   * Return a FileStatus representing the given path. If the path refers
835   * to a symlink return a FileStatus representing the link rather than
836   * the object the link refers to.
837   */
838  @Override
839  public FileStatus getFileLinkStatus(final Path f) throws IOException {
840    FileStatus fi = getFileLinkStatusInternal(f, false);
841    // getFileLinkStatus is supposed to return a symlink with a
842    // qualified path
843    if (fi.isSymlink()) {
844      Path targetQual = FSLinkResolver.qualifySymlinkTarget(this.getUri(),
845          fi.getPath(), fi.getSymlink());
846      fi.setSymlink(targetQual);
847    }
848    return fi;
849  }
850
851  /**
852   * Public {@link FileStatus} methods delegate to this function, which in turn
853   * either call the new {@link Stat} based implementation or the deprecated
854   * methods based on platform support.
855   * 
856   * @param f Path to stat
857   * @param dereference whether to dereference the final path component if a
858   *          symlink
859   * @return FileStatus of f
860   * @throws IOException
861   */
862  private FileStatus getFileLinkStatusInternal(final Path f,
863      boolean dereference) throws IOException {
864    if (!useDeprecatedFileStatus) {
865      return getNativeFileLinkStatus(f, dereference);
866    } else if (dereference) {
867      return deprecatedGetFileStatus(f);
868    } else {
869      return deprecatedGetFileLinkStatusInternal(f);
870    }
871  }
872
873  /**
874   * Deprecated. Remains for legacy support. Should be removed when {@link Stat}
875   * gains support for Windows and other operating systems.
876   */
877  @Deprecated
878  private FileStatus deprecatedGetFileLinkStatusInternal(final Path f)
879      throws IOException {
880    String target = FileUtil.readLink(new File(f.toString()));
881
882    try {
883      FileStatus fs = getFileStatus(f);
884      // If f refers to a regular file or directory
885      if (target.isEmpty()) {
886        return fs;
887      }
888      // Otherwise f refers to a symlink
889      return new FileStatus(fs.getLen(),
890          false,
891          fs.getReplication(),
892          fs.getBlockSize(),
893          fs.getModificationTime(),
894          fs.getAccessTime(),
895          fs.getPermission(),
896          fs.getOwner(),
897          fs.getGroup(),
898          new Path(target),
899          f);
900    } catch (FileNotFoundException e) {
901      /* The exists method in the File class returns false for dangling
902       * links so we can get a FileNotFoundException for links that exist.
903       * It's also possible that we raced with a delete of the link. Use
904       * the readBasicFileAttributes method in java.nio.file.attributes
905       * when available.
906       */
907      if (!target.isEmpty()) {
908        return new FileStatus(0, false, 0, 0, 0, 0, FsPermission.getDefault(),
909            "", "", new Path(target), f);
910      }
911      // f refers to a file or directory that does not exist
912      throw e;
913    }
914  }
915  /**
916   * Calls out to platform's native stat(1) implementation to get file metadata
917   * (permissions, user, group, atime, mtime, etc). This works around the lack
918   * of lstat(2) in Java 6.
919   * 
920   *  Currently, the {@link Stat} class used to do this only supports Linux
921   *  and FreeBSD, so the old {@link #deprecatedGetFileLinkStatusInternal(Path)}
922   *  implementation (deprecated) remains further OS support is added.
923   *
924   * @param f File to stat
925   * @param dereference whether to dereference symlinks
926   * @return FileStatus of f
927   * @throws IOException
928   */
929  private FileStatus getNativeFileLinkStatus(final Path f,
930      boolean dereference) throws IOException {
931    checkPath(f);
932    Stat stat = new Stat(f, getDefaultBlockSize(f), dereference, this);
933    FileStatus status = stat.getFileStatus();
934    return status;
935  }
936
937  @Override
938  public Path getLinkTarget(Path f) throws IOException {
939    FileStatus fi = getFileLinkStatusInternal(f, false);
940    // return an unqualified symlink target
941    return fi.getSymlink();
942  }
943}