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