001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.vfs2.provider.sftp;
018
019import java.io.IOException;
020import java.io.InputStream;
021import java.io.OutputStream;
022import java.util.ArrayList;
023import java.util.Iterator;
024import java.util.Vector;
025
026import org.apache.commons.vfs2.FileNotFoundException;
027import org.apache.commons.vfs2.FileObject;
028import org.apache.commons.vfs2.FileSystemException;
029import org.apache.commons.vfs2.FileType;
030import org.apache.commons.vfs2.NameScope;
031import org.apache.commons.vfs2.RandomAccessContent;
032import org.apache.commons.vfs2.VFS;
033import org.apache.commons.vfs2.provider.AbstractFileName;
034import org.apache.commons.vfs2.provider.AbstractFileObject;
035import org.apache.commons.vfs2.provider.UriParser;
036import org.apache.commons.vfs2.util.FileObjectUtils;
037import org.apache.commons.vfs2.util.MonitorInputStream;
038import org.apache.commons.vfs2.util.MonitorOutputStream;
039import org.apache.commons.vfs2.util.PosixPermissions;
040import org.apache.commons.vfs2.util.RandomAccessMode;
041
042import com.jcraft.jsch.ChannelSftp;
043import com.jcraft.jsch.ChannelSftp.LsEntry;
044import com.jcraft.jsch.SftpATTRS;
045import com.jcraft.jsch.SftpException;
046
047/**
048 * An SFTP file.
049 */
050public class SftpFileObject extends AbstractFileObject<SftpFileSystem> {
051
052    /**
053     * An InputStream that monitors for end-of-file.
054     */
055    private final class SftpInputStream extends MonitorInputStream {
056        private final ChannelSftp channel;
057
058        SftpInputStream(final ChannelSftp channel, final InputStream in) {
059            super(in);
060            this.channel = channel;
061        }
062
063        SftpInputStream(final ChannelSftp channel, final InputStream in, final int bufferSize) {
064            super(in, bufferSize);
065            this.channel = channel;
066        }
067
068        /**
069         * Called after the stream has been closed.
070         */
071        @Override
072        protected void onClose() throws IOException {
073            putChannel(channel);
074        }
075    }
076
077    /**
078     * An OutputStream that wraps an sftp OutputStream, and closes the channel when the stream is closed.
079     */
080    private final class SftpOutputStream extends MonitorOutputStream {
081        private final ChannelSftp channel;
082
083        SftpOutputStream(final ChannelSftp channel, final OutputStream out) {
084            super(out);
085            this.channel = channel;
086        }
087
088        /**
089         * Called after this stream is closed.
090         */
091        @Override
092        protected void onClose() throws IOException {
093            putChannel(channel);
094        }
095    }
096    private static final long MOD_TIME_FACTOR = 1000L;
097
098    private SftpATTRS attrs;
099
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}