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