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