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