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