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, final SftpFileSystem fileSystem) throws FileSystemException {
60          super(name, fileSystem);
61          relPath = UriParser.decode(fileSystem.getRootName().getRelativeName(name));
62      }
63  
64      /** @since 2.0 */
65      @Override
66      protected void doDetach() throws Exception {
67          attrs = null;
68      }
69  
70      /**
71       * Determines the type of this file, returns null if the file does not exist.
72       */
73      @Override
74      protected FileType doGetType() throws Exception {
75          if (attrs == null) {
76              statSelf();
77          }
78  
79          if (attrs == null) {
80              return FileType.IMAGINARY;
81          }
82  
83          if ((attrs.getFlags() & SftpATTRS.SSH_FILEXFER_ATTR_PERMISSIONS) == 0) {
84              throw new FileSystemException("vfs.provider.sftp/unknown-permissions.error");
85          }
86          if (attrs.isDir()) {
87              return FileType.FOLDER;
88          }
89          return FileType.FILE;
90      }
91  
92      /**
93       * Called when the type or content of this file changes.
94       */
95      @Override
96      protected void onChange() throws Exception {
97          statSelf();
98      }
99  
100     /**
101      * Fetches file attributes from server.
102      *
103      * @throws IOException
104      */
105     private void statSelf() throws IOException {
106         ChannelSftp channel = getAbstractFileSystem().getChannel();
107         try {
108             setStat(channel.stat(relPath));
109         } catch (final SftpException e) {
110             try {
111                 // maybe the channel has some problems, so recreate the channel and retry
112                 if (e.id != ChannelSftp.SSH_FX_NO_SUCH_FILE) {
113                     channel.disconnect();
114                     channel = getAbstractFileSystem().getChannel();
115                     setStat(channel.stat(relPath));
116                 } else {
117                     // Really does not exist
118                     attrs = null;
119                 }
120             } catch (final SftpException innerEx) {
121                 // TODO - not strictly true, but jsch 0.1.2 does not give us
122                 // enough info in the exception. Should be using:
123                 // if ( e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE )
124                 // However, sometimes the exception has the correct id, and
125                 // sometimes
126                 // it does not. Need to look into why.
127 
128                 // Does not exist
129                 attrs = null;
130             }
131         } finally {
132             getAbstractFileSystem().putChannel(channel);
133         }
134     }
135 
136     /**
137      * Sets attrs from listChildrenResolved
138      */
139     private void setStat(final SftpATTRS attrs) {
140         this.attrs = attrs;
141     }
142 
143     /**
144      * Creates this file as a folder.
145      */
146     @Override
147     protected void doCreateFolder() throws Exception {
148         final ChannelSftp channel = getAbstractFileSystem().getChannel();
149         try {
150             channel.mkdir(relPath);
151         } finally {
152             getAbstractFileSystem().putChannel(channel);
153         }
154     }
155 
156     @Override
157     protected long doGetLastModifiedTime() throws Exception {
158         if (attrs == null || (attrs.getFlags() & SftpATTRS.SSH_FILEXFER_ATTR_ACMODTIME) == 0) {
159             throw new FileSystemException("vfs.provider.sftp/unknown-modtime.error");
160         }
161         return attrs.getMTime() * MOD_TIME_FACTOR;
162     }
163 
164     /**
165      * Sets the last modified time of this file. Is only called if {@link #doGetType} does not return
166      * {@link FileType#IMAGINARY}.
167      *
168      * @param modtime is modification time in milliseconds. SFTP protocol can send times with nanosecond precision but
169      *            at the moment jsch send them with second precision.
170      */
171     @Override
172     protected boolean doSetLastModifiedTime(final long modtime) throws Exception {
173         final int newMTime = (int) (modtime / MOD_TIME_FACTOR);
174         attrs.setACMODTIME(attrs.getATime(), newMTime);
175         flushStat();
176         return true;
177     }
178 
179     private void flushStat() throws IOException, SftpException {
180         final ChannelSftp channel = getAbstractFileSystem().getChannel();
181         try {
182             channel.setStat(relPath, attrs);
183         } finally {
184             getAbstractFileSystem().putChannel(channel);
185         }
186     }
187 
188     /**
189      * Deletes the file.
190      */
191     @Override
192     protected void doDelete() throws Exception {
193         final ChannelSftp channel = getAbstractFileSystem().getChannel();
194         try {
195             if (isFile()) {
196                 channel.rm(relPath);
197             } else {
198                 channel.rmdir(relPath);
199             }
200         } finally {
201             getAbstractFileSystem().putChannel(channel);
202         }
203     }
204 
205     /**
206      * Renames the file.
207      */
208     @Override
209     protected void doRename(final FileObject newFile) throws Exception {
210         final ChannelSftp channel = getAbstractFileSystem().getChannel();
211         try {
212             final SftpFileObjectrg/apache/commons/vfs2/provider/sftp/SftpFileObject.html#SftpFileObject">SftpFileObject newSftpFileObject = (SftpFileObject) FileObjectUtils.getAbstractFileObject(newFile);
213             channel.rename(relPath, newSftpFileObject.relPath);
214         } finally {
215             getAbstractFileSystem().putChannel(channel);
216         }
217     }
218 
219     /**
220      * Returns the POSIX type permissions of the file.
221      *
222      * @param checkIds {@code true} if user and group ID should be checked (needed for some access rights checks)
223      * @return A PosixPermission object
224      * @throws Exception If an error occurs
225      * @since 2.1
226      */
227     protected PosixPermissions getPermissions(final boolean checkIds) throws Exception {
228         statSelf();
229         boolean isInGroup = false;
230         if (checkIds) {
231             if(getAbstractFileSystem().isExecDisabled()) {
232                 // Exec is disabled, so we won't be able to ascertain the current user's UID and GID.
233                 // Return "always-true" permissions as a workaround, knowing that the SFTP server won't
234                 // let us perform unauthorized actions anyway.
235                 return new UserIsOwnerPosixPermissions(attrs.getPermissions());
236             }
237 
238             for (final int groupId : getAbstractFileSystem().getGroupsIds()) {
239                 if (groupId == attrs.getGId()) {
240                     isInGroup = true;
241                     break;
242                 }
243             }
244         }
245         final boolean isOwner = checkIds ? attrs.getUId() == getAbstractFileSystem().getUId() : false;
246         return new PosixPermissions(attrs.getPermissions(), isOwner, isInGroup);
247     }
248 
249     @Override
250     protected boolean doIsReadable() throws Exception {
251         return getPermissions(true).isReadable();
252     }
253 
254     @Override
255     protected boolean doSetReadable(final boolean readable, final boolean ownerOnly) throws Exception {
256         final PosixPermissions permissions = getPermissions(false);
257         final int newPermissions = permissions.makeReadable(readable, ownerOnly);
258         if (newPermissions == permissions.getPermissions()) {
259             return true;
260         }
261 
262         attrs.setPERMISSIONS(newPermissions);
263         flushStat();
264 
265         return true;
266     }
267 
268     @Override
269     protected boolean doIsWriteable() throws Exception {
270         return getPermissions(true).isWritable();
271     }
272 
273     @Override
274     protected boolean doSetWritable(final boolean writable, final boolean ownerOnly) throws Exception {
275         final PosixPermissions permissions = getPermissions(false);
276         final int newPermissions = permissions.makeWritable(writable, ownerOnly);
277         if (newPermissions == permissions.getPermissions()) {
278             return true;
279         }
280 
281         attrs.setPERMISSIONS(newPermissions);
282         flushStat();
283 
284         return true;
285     }
286 
287     @Override
288     protected boolean doIsExecutable() throws Exception {
289         return getPermissions(true).isExecutable();
290     }
291 
292     @Override
293     protected boolean doSetExecutable(final boolean executable, final boolean ownerOnly) throws Exception {
294         final PosixPermissions permissions = getPermissions(false);
295         final int newPermissions = permissions.makeExecutable(executable, ownerOnly);
296         if (newPermissions == permissions.getPermissions()) {
297             return true;
298         }
299 
300         attrs.setPERMISSIONS(newPermissions);
301         flushStat();
302 
303         return true;
304     }
305 
306     /**
307      * Lists the children of this file.
308      */
309     @Override
310     protected FileObject[] doListChildrenResolved() throws Exception {
311         // should not require a round-trip because type is already set.
312         if (this.isFile()) {
313             return null;
314         }
315         // List the contents of the folder
316         Vector<?> vector = null;
317         final ChannelSftp channel = getAbstractFileSystem().getChannel();
318 
319         try {
320             // try the direct way to list the directory on the server to avoid too many roundtrips
321             vector = channel.ls(relPath);
322         } catch (final SftpException e) {
323             String workingDirectory = null;
324             try {
325                 if (relPath != null) {
326                     workingDirectory = channel.pwd();
327                     channel.cd(relPath);
328                 }
329             } catch (final SftpException ex) {
330                 // VFS-210: seems not to be a directory
331                 return null;
332             }
333 
334             SftpException lsEx = null;
335             try {
336                 vector = channel.ls(".");
337             } catch (final SftpException ex) {
338                 lsEx = ex;
339             } finally {
340                 try {
341                     if (relPath != null) {
342                         channel.cd(workingDirectory);
343                     }
344                 } catch (final SftpException xe) {
345                     throw new FileSystemException("vfs.provider.sftp/change-work-directory-back.error",
346                             workingDirectory, lsEx);
347                 }
348             }
349 
350             if (lsEx != null) {
351                 throw lsEx;
352             }
353         } finally {
354             getAbstractFileSystem().putChannel(channel);
355         }
356         FileSystemException.requireNonNull(vector, "vfs.provider.sftp/list-children.error");
357 
358         // Extract the child names
359         final ArrayList<FileObject> children = new ArrayList<>();
360         for (@SuppressWarnings("unchecked") // OK because ChannelSftp.ls() is documented to return Vector<LsEntry>
361         final Iterator<LsEntry> iterator = (Iterator<LsEntry>) vector.iterator(); iterator.hasNext();) {
362             final LsEntry stat = iterator.next();
363 
364             String name = stat.getFilename();
365             if (VFS.isUriStyle() && stat.getAttrs().isDir() && name.charAt(name.length() - 1) != '/') {
366                 name = name + "/";
367             }
368 
369             if (name.equals(".") || name.equals("..") || name.equals("./") || name.equals("../")) {
370                 continue;
371             }
372 
373             final FileObject fo = getFileSystem().resolveFile(getFileSystem().getFileSystemManager()
374                     .resolveName(getName(), UriParser.encode(name), NameScope.CHILD));
375 
376             ((SftpFileObject) FileObjectUtils.getAbstractFileObject(fo)).setStat(stat.getAttrs());
377 
378             children.add(fo);
379         }
380 
381         return children.toArray(new FileObject[children.size()]);
382     }
383 
384     /**
385      * Lists the children of this file.
386      */
387     @Override
388     protected String[] doListChildren() throws Exception {
389         // use doListChildrenResolved for performance
390         return null;
391     }
392 
393     /**
394      * Returns the size of the file content (in bytes).
395      */
396     @Override
397     protected long doGetContentSize() throws Exception {
398         if (attrs == null || (attrs.getFlags() & SftpATTRS.SSH_FILEXFER_ATTR_SIZE) == 0) {
399             throw new FileSystemException("vfs.provider.sftp/unknown-size.error");
400         }
401         return attrs.getSize();
402     }
403 
404     @Override
405     protected RandomAccessContent doGetRandomAccessContent(final RandomAccessMode mode) throws Exception {
406         return new SftpRandomAccessContent(this, mode);
407     }
408 
409     /**
410      * Creates an input stream to read the file content from. The input stream is starting at the given position in the
411      * file.
412      */
413     InputStream getInputStream(final long filePointer) throws IOException {
414         final ChannelSftp channel = getAbstractFileSystem().getChannel();
415         // Using InputStream directly from the channel
416         // is much faster than the memory method.
417         try {
418             return new SftpInputStream(channel, channel.get(getName().getPathDecoded(), null, filePointer));
419         } catch (final SftpException e) {
420             getAbstractFileSystem().putChannel(channel);
421             throw new FileSystemException(e);
422         }
423     }
424 
425     /**
426      * Creates an input stream to read the file content from.
427      */
428     @SuppressWarnings("resource")
429     @Override
430     protected InputStream doGetInputStream(final int bufferSize) throws Exception {
431         // VFS-113: avoid npe
432         synchronized (getAbstractFileSystem()) {
433             final ChannelSftp channel = getAbstractFileSystem().getChannel();
434             try {
435                 // return channel.get(getName().getPath());
436                 // hmmm - using the in memory method is soooo much faster ...
437 
438                 // TODO - Don't read the entire file into memory. Use the
439                 // stream-based methods on ChannelSftp once they work properly
440 
441                 /*
442                  * final ByteArrayOutputStream outstr = new ByteArrayOutputStream(); channel.get(relPath, outstr);
443                  * outstr.close(); return new ByteArrayInputStream(outstr.toByteArray());
444                  */
445 
446                 InputStream inputStream;
447                 try {
448                     // VFS-210: sftp allows to gather an input stream even from a directory and will
449                     // fail on first read. So we need to check the type anyway
450                     if (!getType().hasContent()) {
451                         throw new FileSystemException("vfs.provider/read-not-file.error", getName());
452                     }
453 
454                     inputStream = channel.get(relPath);
455                 } catch (final SftpException e) {
456                     if (e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) {
457                         throw new FileNotFoundException(getName());
458                     }
459 
460                     throw new FileSystemException(e);
461                 }
462 
463                 return new SftpInputStream(channel, inputStream, bufferSize);
464 
465             } finally {
466                 // getAbstractFileSystem().putChannel(channel);
467             }
468         }
469     }
470 
471     /**
472      * Creates an output stream to write the file content to.
473      */
474     @Override
475     protected OutputStream doGetOutputStream(final boolean bAppend) throws Exception {
476         // TODO - Don't write the entire file into memory. Use the stream-based
477         // methods on ChannelSftp once the work properly
478         /*
479          * final ChannelSftp channel = getAbstractFileSystem().getChannel(); return new SftpOutputStream(channel);
480          */
481 
482         final ChannelSftp channel = getAbstractFileSystem().getChannel();
483         return new SftpOutputStream(channel, channel.put(relPath, bAppend ? ChannelSftp.APPEND : ChannelSftp.OVERWRITE));
484     }
485 
486     /**
487      * An InputStream that monitors for end-of-file.
488      */
489     private class SftpInputStream extends MonitorInputStream {
490         private final ChannelSftp channel;
491 
492         public SftpInputStream(final ChannelSftp channel, final InputStream in) {
493             super(in);
494             this.channel = channel;
495         }
496 
497         public SftpInputStream(final ChannelSftp channel, final InputStream in, final int bufferSize) {
498             super(in, bufferSize);
499             this.channel = channel;
500         }
501 
502         /**
503          * Called after the stream has been closed.
504          */
505         @Override
506         protected void onClose() throws IOException {
507             getAbstractFileSystem().putChannel(channel);
508         }
509     }
510 
511     /**
512      * An OutputStream that wraps an sftp OutputStream, and closes the channel when the stream is closed.
513      */
514     private class SftpOutputStream extends MonitorOutputStream {
515         private final ChannelSftp channel;
516 
517         public SftpOutputStream(final ChannelSftp channel, final OutputStream out) {
518             super(out);
519             this.channel = channel;
520         }
521 
522         /**
523          * Called after this stream is closed.
524          */
525         @Override
526         protected void onClose() throws IOException {
527             getAbstractFileSystem().putChannel(channel);
528         }
529     }
530 
531 }