View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.vfs2.provider.sftp;
18  
19  import java.io.IOException;
20  import java.io.InputStream;
21  import java.io.OutputStream;
22  import java.util.ArrayList;
23  import java.util.Iterator;
24  import java.util.Vector;
25  
26  import org.apache.commons.vfs2.FileNotFoundException;
27  import org.apache.commons.vfs2.FileObject;
28  import org.apache.commons.vfs2.FileSystemException;
29  import org.apache.commons.vfs2.FileType;
30  import org.apache.commons.vfs2.NameScope;
31  import org.apache.commons.vfs2.RandomAccessContent;
32  import org.apache.commons.vfs2.VFS;
33  import org.apache.commons.vfs2.provider.AbstractFileName;
34  import org.apache.commons.vfs2.provider.AbstractFileObject;
35  import org.apache.commons.vfs2.provider.UriParser;
36  import org.apache.commons.vfs2.util.FileObjectUtils;
37  import org.apache.commons.vfs2.util.MonitorInputStream;
38  import org.apache.commons.vfs2.util.MonitorOutputStream;
39  import org.apache.commons.vfs2.util.PosixPermissions;
40  import org.apache.commons.vfs2.util.RandomAccessMode;
41  
42  import com.jcraft.jsch.ChannelSftp;
43  import com.jcraft.jsch.ChannelSftp.LsEntry;
44  import com.jcraft.jsch.SftpATTRS;
45  import com.jcraft.jsch.SftpException;
46  
47  /**
48   * An SFTP file.
49   */
50  public class SftpFileObject extends AbstractFileObject<SftpFileSystem>
51  {
52      private static final long MOD_TIME_FACTOR = 1000L;
53  
54      private SftpATTRS attrs;
55      private final String relPath;
56  
57      private boolean inRefresh;
58  
59      protected SftpFileObject(final AbstractFileName name,
60              final SftpFileSystem fileSystem) throws FileSystemException
61      {
62          super(name, fileSystem);
63          relPath = UriParser.decode(fileSystem.getRootName().getRelativeName(
64                  name));
65      }
66  
67      /** @since 2.0 */
68      @Override
69      protected void doDetach() throws Exception
70      {
71          attrs = null;
72      }
73  
74      /**
75       * @throws FileSystemException if error occurs.
76       * @since 2.0
77       */
78      @Override
79      public void refresh() throws FileSystemException
80      {
81          if (!inRefresh)
82          {
83              try
84              {
85                  inRefresh = true;
86                  super.refresh();
87                  try
88                  {
89                      attrs = null;
90                      getType();
91                  }
92                  catch (final IOException e)
93                  {
94                      throw new FileSystemException(e);
95                  }
96              }
97              finally
98              {
99                  inRefresh = false;
100             }
101         }
102     }
103 
104     /**
105      * Determines the type of this file, returns null if the file does not
106      * exist.
107      */
108     @Override
109     protected FileType doGetType() throws Exception
110     {
111         if (attrs == null)
112         {
113             statSelf();
114         }
115 
116         if (attrs == null)
117         {
118             return FileType.IMAGINARY;
119         }
120 
121         if ((attrs.getFlags() & SftpATTRS.SSH_FILEXFER_ATTR_PERMISSIONS) == 0)
122         {
123             throw new FileSystemException(
124                     "vfs.provider.sftp/unknown-permissions.error");
125         }
126         if (attrs.isDir())
127         {
128             return FileType.FOLDER;
129         }
130         else
131         {
132             return FileType.FILE;
133         }
134     }
135 
136     /**
137      * Called when the type or content of this file changes.
138      */
139     @Override
140     protected void onChange() throws Exception
141     {
142         statSelf();
143     }
144 
145     /**
146      * Fetches file attributes from server.
147      *
148      * @throws IOException
149      */
150     private void statSelf() throws IOException
151     {
152         ChannelSftp channel = getAbstractFileSystem().getChannel();
153         try
154         {
155             setStat(channel.stat(relPath));
156         }
157         catch (final SftpException e)
158         {
159             try
160             {
161                 // maybe the channel has some problems, so recreate the channel and retry
162                 if (e.id != ChannelSftp.SSH_FX_NO_SUCH_FILE)
163                 {
164                     channel.disconnect();
165                     channel = getAbstractFileSystem().getChannel();
166                     setStat(channel.stat(relPath));
167                 }
168                 else
169                 {
170                     // Really does not exist
171                     attrs = null;
172                 }
173             }
174             catch (final SftpException innerEx)
175             {
176                 // TODO - not strictly true, but jsch 0.1.2 does not give us
177                 // enough info in the exception. Should be using:
178                 // if ( e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE )
179                 // However, sometimes the exception has the correct id, and
180                 // sometimes
181                 // it does not. Need to look into why.
182 
183                 // Does not exist
184                 attrs = null;
185             }
186         }
187         finally
188         {
189             getAbstractFileSystem().putChannel(channel);
190         }
191     }
192 
193     /**
194      * Set attrs from listChildrenResolved
195      */
196     private void setStat(final SftpATTRS attrs)
197     {
198         this.attrs = attrs;
199     }
200 
201     /**
202      * Creates this file as a folder.
203      */
204     @Override
205     protected void doCreateFolder() throws Exception
206     {
207         final ChannelSftp channel = getAbstractFileSystem().getChannel();
208         try
209         {
210             channel.mkdir(relPath);
211         }
212         finally
213         {
214             getAbstractFileSystem().putChannel(channel);
215         }
216     }
217 
218     @Override
219     protected long doGetLastModifiedTime() throws Exception
220     {
221         if (attrs == null
222                 || (attrs.getFlags() & SftpATTRS.SSH_FILEXFER_ATTR_ACMODTIME) == 0)
223         {
224             throw new FileSystemException(
225                     "vfs.provider.sftp/unknown-modtime.error");
226         }
227         return attrs.getMTime() * MOD_TIME_FACTOR;
228     }
229 
230     /**
231      * Sets the last modified time of this file. Is only called if
232      * {@link #doGetType} does not return {@link FileType#IMAGINARY}.
233      *
234      * @param modtime
235      *            is modification time in milliseconds. SFTP protocol can send
236      *            times with nanosecond precision but at the moment jsch send
237      *            them with second precision.
238      */
239     @Override
240     protected boolean doSetLastModifiedTime(final long modtime) throws Exception
241     {
242         final int newMTime = (int) (modtime / MOD_TIME_FACTOR);
243         attrs.setACMODTIME(attrs.getATime(), newMTime);
244         flushStat();
245         return true;
246     }
247 
248     private void flushStat() throws IOException, SftpException
249     {
250         final ChannelSftp channel = getAbstractFileSystem().getChannel();
251         try
252         {
253             channel.setStat(relPath, attrs);
254         }
255         finally
256         {
257             getAbstractFileSystem().putChannel(channel);
258         }
259     }
260 
261     /**
262      * Deletes the file.
263      */
264     @Override
265     protected void doDelete() throws Exception
266     {
267         final ChannelSftp channel = getAbstractFileSystem().getChannel();
268         try
269         {
270             if (isFile())
271             {
272                 channel.rm(relPath);
273             }
274             else
275             {
276                 channel.rmdir(relPath);
277             }
278         }
279         finally
280         {
281             getAbstractFileSystem().putChannel(channel);
282         }
283     }
284 
285     /**
286      * Rename the file.
287      */
288     @Override
289     protected void doRename(final FileObject newFile) throws Exception
290     {
291         final ChannelSftp channel = getAbstractFileSystem().getChannel();
292         try
293         {
294             final SftpFileObject newSftpFileObject = (SftpFileObject) FileObjectUtils.getAbstractFileObject(newFile);
295             channel.rename(relPath, newSftpFileObject.relPath);
296         }
297         finally
298         {
299             getAbstractFileSystem().putChannel(channel);
300         }
301     }
302 
303     /**
304      * Returns the POSIX type permissions of the file.
305      *
306      * @param checkIds {@code true} if user and group ID should be checked (needed for some access rights checks)
307      * @return A PosixPermission object
308      * @throws Exception If an error occurs
309      * @since 2.1
310      */
311     protected PosixPermissions getPermissions(final boolean checkIds) throws Exception
312     {
313         statSelf();
314         boolean isInGroup = false;
315         if (checkIds)
316         {
317             for (final int groupId : getAbstractFileSystem().getGroupsIds())
318             {
319                 if (groupId == attrs.getGId())
320                 {
321                     isInGroup = true;
322                     break;
323                 }
324             }
325         }
326         final boolean isOwner = checkIds ?  attrs.getUId() == getAbstractFileSystem().getUId() : false;
327         final PosixPermissions permissions = new PosixPermissions(attrs.getPermissions(), isOwner, isInGroup);
328 
329         return permissions;
330     }
331 
332     @Override
333     protected boolean doIsReadable() throws Exception
334     {
335         return getPermissions(true).isReadable();
336     }
337 
338     @Override
339     protected boolean doSetReadable(final boolean readable, final boolean ownerOnly) throws Exception
340     {
341         final PosixPermissions permissions = getPermissions(false);
342         final int newPermissions = permissions.makeReadable(readable, ownerOnly);
343         if (newPermissions == permissions.getPermissions())
344         {
345             return true;
346         }
347 
348         attrs.setPERMISSIONS(newPermissions);
349         flushStat();
350 
351         return true;
352     }
353 
354     @Override
355     protected boolean doIsWriteable() throws Exception
356     {
357         return getPermissions(true).isWritable();
358     }
359 
360     @Override
361     protected boolean doSetWritable(final boolean writable, final boolean ownerOnly) throws Exception
362     {
363         final PosixPermissions permissions = getPermissions(false);
364         final int newPermissions = permissions.makeWritable(writable, ownerOnly);
365         if (newPermissions == permissions.getPermissions())
366         {
367             return true;
368         }
369 
370         attrs.setPERMISSIONS(newPermissions);
371         flushStat();
372 
373         return true;
374     }
375 
376     @Override
377     protected boolean doIsExecutable() throws Exception
378     {
379         return getPermissions(true).isExecutable();
380     }
381 
382 
383     @Override
384     protected boolean doSetExecutable(final boolean executable, final boolean ownerOnly) throws Exception
385     {
386         final PosixPermissions permissions = getPermissions(false);
387         final int newPermissions = permissions.makeExecutable(executable, ownerOnly);
388         if (newPermissions == permissions.getPermissions())
389         {
390             return true;
391         }
392 
393         attrs.setPERMISSIONS(newPermissions);
394         flushStat();
395 
396         return true;
397     }
398 
399     /**
400      * Lists the children of this file.
401      */
402     @Override
403     protected FileObject[] doListChildrenResolved() throws Exception
404     {
405         // should not require a round-trip because type is already set.
406         if (this.isFile())
407         {
408             return null;
409         }
410         // List the contents of the folder
411         Vector<?> vector = null;
412         final ChannelSftp channel = getAbstractFileSystem().getChannel();
413 
414         try
415         {
416             // try the direct way to list the directory on the server to avoid too many roundtrips
417             vector = channel.ls(relPath);
418         }
419         catch (final SftpException e)
420         {
421             String workingDirectory = null;
422             try
423             {
424                 if (relPath != null)
425                 {
426                     workingDirectory = channel.pwd();
427                     channel.cd(relPath);
428                 }
429             }
430             catch (final SftpException ex)
431             {
432                 // VFS-210: seems not to be a directory
433                 return null;
434             }
435 
436             SftpException lsEx = null;
437             try
438             {
439                 vector = channel.ls(".");
440             }
441             catch (final SftpException ex)
442             {
443                 lsEx = ex;
444             }
445             finally
446             {
447                 try
448                 {
449                     if (relPath != null)
450                     {
451                         channel.cd(workingDirectory);
452                     }
453                 }
454                 catch (final SftpException xe)
455                 {
456                     throw new FileSystemException("vfs.provider.sftp/change-work-directory-back.error",
457                                                   workingDirectory, lsEx);
458                 }
459             }
460 
461             if (lsEx != null)
462             {
463                 throw lsEx;
464             }
465         }
466         finally
467         {
468             getAbstractFileSystem().putChannel(channel);
469         }
470         if (vector == null)
471         {
472             throw new FileSystemException(
473                     "vfs.provider.sftp/list-children.error");
474         }
475 
476         // Extract the child names
477         final ArrayList<FileObject> children = new ArrayList<FileObject>();
478         for (@SuppressWarnings("unchecked") // OK because ChannelSftp.ls() is documented to return Vector<LsEntry>
479         final
480             Iterator<LsEntry> iterator = (Iterator<LsEntry>) vector.iterator(); iterator.hasNext();)
481         {
482             final LsEntry stat = iterator.next();
483 
484             String name = stat.getFilename();
485             if (VFS.isUriStyle() && stat.getAttrs().isDir()
486                     && name.charAt(name.length() - 1) != '/')
487             {
488                 name = name + "/";
489             }
490 
491             if (name.equals(".") || name.equals("..") || name.equals("./")
492                     || name.equals("../"))
493             {
494                 continue;
495             }
496 
497             final FileObject fo =
498                 getFileSystem()
499                     .resolveFile(
500                             getFileSystem().getFileSystemManager().resolveName(
501                                     getName(), UriParser.encode(name),
502                                     NameScope.CHILD));
503 
504             ((SftpFileObject) FileObjectUtils.getAbstractFileObject(fo)).setStat(stat.getAttrs());
505 
506             children.add(fo);
507         }
508 
509         return children.toArray(new FileObject[children.size()]);
510     }
511 
512     /**
513      * Lists the children of this file.
514      */
515     @Override
516     protected String[] doListChildren() throws Exception
517     {
518         // use doListChildrenResolved for performance
519         return null;
520     }
521 
522     /**
523      * Returns the size of the file content (in bytes).
524      */
525     @Override
526     protected long doGetContentSize() throws Exception
527     {
528         if (attrs == null
529                 || (attrs.getFlags() & SftpATTRS.SSH_FILEXFER_ATTR_SIZE) == 0)
530         {
531             throw new FileSystemException(
532                     "vfs.provider.sftp/unknown-size.error");
533         }
534         return attrs.getSize();
535     }
536 
537     @Override
538     protected RandomAccessContent doGetRandomAccessContent(
539             final RandomAccessMode mode) throws Exception
540     {
541         return new SftpRandomAccessContent(this, mode);
542     }
543 
544     /**
545      * Creates an input stream to read the file content from.
546      * The input stream is starting at the given position in the file.
547      */
548     InputStream getInputStream(final long filePointer) throws IOException
549     {
550         final ChannelSftp channel = getAbstractFileSystem().getChannel();
551         // Using InputStream directly from the channel
552         // is much faster than the memory method.
553         try
554         {
555             final InputStream is = channel.get(getName().getPathDecoded(), null, filePointer);
556             return new SftpInputStream(channel, is);
557         }
558         catch (final SftpException e)
559         {
560             getAbstractFileSystem().putChannel(channel);
561             throw new FileSystemException(e);
562         }
563     }
564 
565     /**
566      * Creates an input stream to read the file content from.
567      */
568     @Override
569     protected InputStream doGetInputStream() throws Exception
570     {
571         // VFS-113: avoid npe
572         synchronized (getAbstractFileSystem())
573         {
574             final ChannelSftp channel = getAbstractFileSystem().getChannel();
575             try
576             {
577                 // return channel.get(getName().getPath());
578                 // hmmm - using the in memory method is soooo much faster ...
579 
580                 // TODO - Don't read the entire file into memory. Use the
581                 // stream-based methods on ChannelSftp once they work properly
582 
583                 /*
584                 final ByteArrayOutputStream outstr = new ByteArrayOutputStream();
585                 channel.get(relPath, outstr);
586                 outstr.close();
587                 return new ByteArrayInputStream(outstr.toByteArray());
588                 */
589 
590                 InputStream is;
591                 try
592                 {
593                     // VFS-210: sftp allows to gather an input stream even from a directory and will
594                     // fail on first read. So we need to check the type anyway
595                     if (!getType().hasContent())
596                     {
597                         throw new FileSystemException("vfs.provider/read-not-file.error", getName());
598                     }
599 
600                     is = channel.get(relPath);
601                 }
602                 catch (final SftpException e)
603                 {
604                     if (e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE)
605                     {
606                         throw new FileNotFoundException(getName());
607                     }
608 
609                     throw new FileSystemException(e);
610                 }
611 
612                 return new SftpInputStream(channel, is);
613 
614             }
615             finally
616             {
617 //              getAbstractFileSystem().putChannel(channel);
618             }
619         }
620     }
621 
622     /**
623      * Creates an output stream to write the file content to.
624      */
625     @Override
626     protected OutputStream doGetOutputStream(final boolean bAppend) throws Exception
627     {
628         // TODO - Don't write the entire file into memory. Use the stream-based
629         // methods on ChannelSftp once the work properly
630         /*
631         final ChannelSftp channel = getAbstractFileSystem().getChannel();
632         return new SftpOutputStream(channel);
633         */
634 
635         final ChannelSftp channel = getAbstractFileSystem().getChannel();
636         return new SftpOutputStream(channel, channel.put(relPath));
637     }
638 
639     /**
640      * An InputStream that monitors for end-of-file.
641      */
642     private class SftpInputStream extends MonitorInputStream
643     {
644         private final ChannelSftp channel;
645 
646         public SftpInputStream(final ChannelSftp channel, final InputStream in)
647         {
648             super(in);
649             this.channel = channel;
650         }
651 
652         /**
653          * Called after the stream has been closed.
654          */
655         @Override
656         protected void onClose() throws IOException
657         {
658             getAbstractFileSystem().putChannel(channel);
659         }
660     }
661 
662     /**
663      * An OutputStream that wraps an sftp OutputStream, and closes the channel
664      * when the stream is closed.
665      */
666     private class SftpOutputStream extends MonitorOutputStream
667     {
668         private final ChannelSftp channel;
669 
670         public SftpOutputStream(final ChannelSftp channel, final OutputStream out)
671         {
672             super(out);
673             this.channel = channel;
674         }
675 
676         /**
677          * Called after this stream is closed.
678          */
679         @Override
680         protected void onClose() throws IOException
681         {
682             getAbstractFileSystem().putChannel(channel);
683         }
684     }
685 
686 }