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