You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by lg...@apache.org on 2015/07/01 12:57:46 UTC

[05/10] mina-sshd git commit: [SSHD-509] Use targeted derived NamedFactory(ies) for the various generic parameters

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/a6e2bf9e/sshd-core/src/main/java/org/apache/sshd/server/sftp/SftpSubsystem.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/sftp/SftpSubsystem.java b/sshd-core/src/main/java/org/apache/sshd/server/sftp/SftpSubsystem.java
deleted file mode 100644
index d696a4f..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/server/sftp/SftpSubsystem.java
+++ /dev/null
@@ -1,2280 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.sshd.server.sftp;
-
-import static org.apache.sshd.common.sftp.SftpConstants.*;
-
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
-import java.io.EOFException;
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.nio.BufferUnderflowException;
-import java.nio.ByteBuffer;
-import java.nio.channels.FileChannel;
-import java.nio.channels.FileLock;
-import java.nio.channels.OverlappingFileLockException;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.AccessDeniedException;
-import java.nio.file.CopyOption;
-import java.nio.file.DirectoryNotEmptyException;
-import java.nio.file.DirectoryStream;
-import java.nio.file.FileAlreadyExistsException;
-import java.nio.file.FileSystem;
-import java.nio.file.FileSystems;
-import java.nio.file.Files;
-import java.nio.file.LinkOption;
-import java.nio.file.NoSuchFileException;
-import java.nio.file.OpenOption;
-import java.nio.file.Path;
-import java.nio.file.StandardCopyOption;
-import java.nio.file.StandardOpenOption;
-import java.nio.file.attribute.AclEntry;
-import java.nio.file.attribute.AclEntryFlag;
-import java.nio.file.attribute.AclEntryPermission;
-import java.nio.file.attribute.AclEntryType;
-import java.nio.file.attribute.FileAttribute;
-import java.nio.file.attribute.FileTime;
-import java.nio.file.attribute.GroupPrincipal;
-import java.nio.file.attribute.PosixFilePermission;
-import java.nio.file.attribute.PosixFilePermissions;
-import java.nio.file.attribute.UserPrincipal;
-import java.nio.file.attribute.UserPrincipalLookupService;
-import java.security.Principal;
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.EnumSet;
-import java.util.GregorianCalendar;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Set;
-import java.util.UUID;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
-
-import org.apache.sshd.common.FactoryManagerUtils;
-import org.apache.sshd.common.file.FileSystemAware;
-import org.apache.sshd.common.util.GenericUtils;
-import org.apache.sshd.common.util.OsUtils;
-import org.apache.sshd.common.util.SelectorUtils;
-import org.apache.sshd.common.util.buffer.Buffer;
-import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
-import org.apache.sshd.common.util.io.IoUtils;
-import org.apache.sshd.common.util.logging.AbstractLoggingBean;
-import org.apache.sshd.common.util.threads.ThreadUtils;
-import org.apache.sshd.server.Command;
-import org.apache.sshd.server.Environment;
-import org.apache.sshd.server.ExitCallback;
-import org.apache.sshd.server.SessionAware;
-import org.apache.sshd.server.session.ServerSession;
-
-/**
- * SFTP subsystem
- *
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public class SftpSubsystem extends AbstractLoggingBean implements Command, Runnable, SessionAware, FileSystemAware {
-
-    /**
-     * Properties key for the maximum of available open handles per session.
-     */
-    public static final String MAX_OPEN_HANDLES_PER_SESSION = "max-open-handles-per-session";
-
-    /**
-     * Force the use of a given sftp version
-     */
-    public static final String SFTP_VERSION = "sftp-version";
-
-    public static final int LOWER_SFTP_IMPL = SFTP_V3; // Working implementation from v3
-    public static final int HIGHER_SFTP_IMPL = SFTP_V6; //  .. up to
-    public static final String ALL_SFTP_IMPL;
-    public static final int  MAX_PACKET_LENGTH = 1024 * 16;
-
-    static {
-        StringBuilder sb = new StringBuilder(2 * (1 + (HIGHER_SFTP_IMPL - LOWER_SFTP_IMPL)));
-        for (int v = LOWER_SFTP_IMPL; v <= HIGHER_SFTP_IMPL; v++) {
-            if (sb.length() > 0) {
-                sb.append(',');
-            }
-            sb.append(v);
-        }
-        ALL_SFTP_IMPL = sb.toString();
-    }
-
-    private ExitCallback callback;
-    private InputStream in;
-    private OutputStream out;
-    private OutputStream err;
-    private Environment env;
-    private ServerSession session;
-    private boolean closed = false;
-	private ExecutorService executors;
-	private boolean shutdownExecutor;
-	private Future<?> pendingFuture;
-
-    private FileSystem fileSystem = FileSystems.getDefault();
-    private Path defaultDir = fileSystem.getPath(System.getProperty("user.dir"));
-
-    private int version;
-    private final Map<String, byte[]> extensions = new HashMap<>();
-    private final Map<String, Handle> handles = new HashMap<>();
-
-    private final UnsupportedAttributePolicy unsupportedAttributePolicy;
-
-    protected static abstract class Handle implements java.io.Closeable {
-        private Path file;
-
-        public Handle(Path file) {
-            this.file = file;
-        }
-
-        public Path getFile() {
-            return file;
-        }
-
-        @Override
-        public void close() throws IOException {
-            // ignored
-        }
-
-        @Override
-        public String toString() {
-            return Objects.toString(getFile());
-        }
-    }
-
-    protected static class DirectoryHandle extends Handle implements Iterator<Path> {
-        private boolean done;
-        // the directory should be read once at "open directory"
-        private DirectoryStream<Path> ds;
-        private Iterator<Path> fileList;
-
-        public DirectoryHandle(Path file) throws IOException {
-            super(file);
-            ds = Files.newDirectoryStream(file);
-            fileList = ds.iterator();
-        }
-
-        public boolean isDone() {
-            return done;
-        }
-
-        public void setDone(boolean done) {
-            this.done = done;
-        }
-
-        @Override
-        public boolean hasNext() {
-            return fileList.hasNext();
-        }
-
-        @Override
-        public Path next() {
-            return fileList.next();
-        }
-
-        @Override
-        public void remove() {
-            throw new UnsupportedOperationException("Not allowed to remove " + toString());
-        }
-
-        public void clearFileList() {
-            // allow the garbage collector to do the job
-            fileList = null;
-        }
-
-        @Override
-        public void close() throws IOException {
-            ds.close();
-        }
-    }
-
-    protected class FileHandle extends Handle {
-        private final FileChannel channel;
-        private long pos;
-        private final List<FileLock> locks = new ArrayList<>();
-
-        public FileHandle(Path file, int flags, int access, Map<String, Object> attrs) throws IOException {
-            super(file);
-            Set<OpenOption> options = new HashSet<>();
-            if ((access & ACE4_READ_DATA) != 0 || (access & ACE4_READ_ATTRIBUTES) != 0) {
-                options.add(StandardOpenOption.READ);
-            }
-            if ((access & ACE4_WRITE_DATA) != 0 || (access & ACE4_WRITE_ATTRIBUTES) != 0) {
-                options.add(StandardOpenOption.WRITE);
-            }
-            switch (flags & SSH_FXF_ACCESS_DISPOSITION) {
-            case SSH_FXF_CREATE_NEW:
-                options.add(StandardOpenOption.CREATE_NEW);
-                break;
-            case SSH_FXF_CREATE_TRUNCATE:
-                options.add(StandardOpenOption.CREATE);
-                options.add(StandardOpenOption.TRUNCATE_EXISTING);
-                break;
-            case SSH_FXF_OPEN_EXISTING:
-                break;
-            case SSH_FXF_OPEN_OR_CREATE:
-                options.add(StandardOpenOption.CREATE);
-                break;
-            case SSH_FXF_TRUNCATE_EXISTING:
-                options.add(StandardOpenOption.TRUNCATE_EXISTING);
-                break;
-            default:    // ignored
-            }
-            if ((flags & SSH_FXF_APPEND_DATA) != 0) {
-                options.add(StandardOpenOption.APPEND);
-            }
-            FileAttribute<?>[] attributes = new FileAttribute<?>[attrs.size()];
-            int index = 0;
-            for (Map.Entry<String, Object> attr : attrs.entrySet()) {
-                final String key = attr.getKey();
-                final Object val = attr.getValue();
-                attributes[index++] = new FileAttribute<Object>() {
-                    @Override
-                    public String name() {
-                        return key;
-                    }
-
-                    @Override
-                    public Object value() {
-                        return val;
-                    }
-                };
-            }
-            FileChannel channel;
-            try {
-                  channel = FileChannel.open(file, options, attributes);
-            } catch (UnsupportedOperationException e) {
-                channel = FileChannel.open(file, options);
-                setAttributes(file, attrs);
-            }
-            this.channel = channel;
-            this.pos = 0;
-        }
-
-        public int read(byte[] data, long offset) throws IOException {
-            return read(data, 0, data.length, offset);
-        }
-
-        public int read(byte[] data, int doff, int length, long offset) throws IOException {
-            if (pos != offset) {
-                channel.position(offset);
-                pos = offset;
-            }
-            int read = channel.read(ByteBuffer.wrap(data, doff, length));
-            pos += read;
-            return read;
-        }
-
-        public void write(byte[] data, long offset) throws IOException {
-            write(data, 0, data.length, offset);
-        }
-
-        public void write(byte[] data, int doff, int length, long offset) throws IOException {
-            if (pos != offset) {
-                channel.position(offset);
-                pos = offset;
-            }
-            channel.write(ByteBuffer.wrap(data, doff, length));
-            pos += length;
-        }
-
-        @Override
-        public void close() throws IOException {
-            channel.close();
-        }
-
-        public void lock(long offset, long length, int mask) throws IOException {
-            long size = length == 0 ? channel.size() - offset : length;
-            FileLock lock = channel.tryLock(offset, size, false);
-            synchronized (locks) {
-                locks.add(lock);
-            }
-        }
-
-        public boolean unlock(long offset, long length) throws IOException {
-            long size = length == 0 ? channel.size() - offset : length;
-            FileLock lock = null;
-            for (Iterator<FileLock> iterator = locks.iterator(); iterator.hasNext();) {
-                FileLock l = iterator.next();
-                if (l.position() == offset && l.size() == size) {
-                    iterator.remove();
-                    lock = l;
-                    break;
-                }
-            }
-            if (lock != null) {
-                lock.release();
-                return true;
-            }
-            return false;
-        }
-    }
-
-    /**
-     * @param executorService The {@link ExecutorService} to be used by
-     *                        the {@link SftpSubsystem} command when starting execution. If
-     *                        {@code null} then a single-threaded ad-hoc service is used.
-     * @param shutdownOnExit  If {@code true} the {@link ExecutorService#shutdownNow()}
-     *                        will be called when subsystem terminates - unless it is the ad-hoc
-     *                        service, which will be shutdown regardless
-     * @param policy The {@link UnsupportedAttributePolicy} to use if failed to access
-     * some local file attributes
-     * @see ThreadUtils#newSingleThreadExecutor(String)
-     */
-    public SftpSubsystem(ExecutorService executorService, boolean shutdownOnExit, UnsupportedAttributePolicy policy) {
-        if ((executors = executorService) == null) {
-            executors = ThreadUtils.newSingleThreadExecutor(getClass().getSimpleName());
-            shutdownExecutor = true;    // we always close the ad-hoc executor service
-        } else {
-            shutdownExecutor = shutdownOnExit;
-        }
-        
-        if ((unsupportedAttributePolicy=policy) == null) {
-            throw new IllegalArgumentException("No policy provided");
-        }
-    }
-
-    public final UnsupportedAttributePolicy getUnsupportedAttributePolicy() {
-        return unsupportedAttributePolicy;
-    }
-
-    @Override
-    public void setSession(ServerSession session) {
-        this.session = session;
-    }
-
-    @Override
-    public void setFileSystem(FileSystem fileSystem) {
-        if (fileSystem != this.fileSystem) {
-            this.fileSystem = fileSystem;
-            this.defaultDir = fileSystem.getRootDirectories().iterator().next();
-        }
-    }
-
-    @Override
-    public void setExitCallback(ExitCallback callback) {
-        this.callback = callback;
-    }
-
-    @Override
-    public void setInputStream(InputStream in) {
-        this.in = in;
-    }
-
-    @Override
-    public void setOutputStream(OutputStream out) {
-        this.out = out;
-    }
-
-    @Override
-    public void setErrorStream(OutputStream err) {
-        this.err = err;
-    }
-
-    @Override
-    public void start(Environment env) throws IOException {
-        this.env = env;
-        try {
-            pendingFuture = executors.submit(this);
-        } catch (RuntimeException e) {    // e.g., RejectedExecutionException
-            log.error("Failed (" + e.getClass().getSimpleName() + ") to start command: " + e.toString(), e);
-            throw new IOException(e);
-        }
-    }
-
-    @Override
-    public void run() {
-        DataInputStream dis = null;
-        try {
-            dis = new DataInputStream(in);
-            while (true) {
-                int length = dis.readInt();
-                if (length < 5) {
-                    throw new IllegalArgumentException("Bad length to read: " + length);
-                }
-                Buffer buffer = new ByteArrayBuffer(length + 4);
-                buffer.putInt(length);
-                int nb = length;
-                while (nb > 0) {
-                    int l = dis.read(buffer.array(), buffer.wpos(), nb);
-                    if (l < 0) {
-                        throw new IllegalArgumentException("Premature EOF while read length=" + length + " while remain=" + nb);
-                    }
-                    buffer.wpos(buffer.wpos() + l);
-                    nb -= l;
-                }
-                process(buffer);
-            }
-        } catch (Throwable t) {
-            if (!closed && !(t instanceof EOFException)) { // Ignore
-                log.error("Exception caught in SFTP subsystem", t);
-            }
-        } finally {
-            if (dis != null) {
-                try {
-                    dis.close();
-                } catch (IOException ioe) {
-                    log.error("Could not close DataInputStream", ioe);
-                }
-            }
-
-            if (handles != null) {
-                for (Map.Entry<String, Handle> entry : handles.entrySet()) {
-                    Handle handle = entry.getValue();
-                    try {
-                        handle.close();
-                    } catch (IOException ioe) {
-                        log.error("Could not close open handle: " + entry.getKey(), ioe);
-                    }
-                }
-            }
-            callback.onExit(0);
-        }
-    }
-
-    protected void process(Buffer buffer) throws IOException {
-        int length = buffer.getInt();
-        int type = buffer.getByte();
-        int id = buffer.getInt();
-        if (log.isDebugEnabled()) {
-            log.debug("process(length={}, type={}, id={})",
-                      new Object[] { Integer.valueOf(length), Integer.valueOf(type), Integer.valueOf(id) });
-        }
-
-        switch (type) {
-            case SSH_FXP_INIT:
-                doInit(buffer, id);
-                break;
-            case SSH_FXP_OPEN:
-                doOpen(buffer, id);
-                break;
-            case SSH_FXP_CLOSE:
-                doClose(buffer, id);
-                break;
-            case SSH_FXP_READ:
-                doRead(buffer, id);
-                break;
-            case SSH_FXP_WRITE:
-                doWrite(buffer, id);
-                break;
-            case SSH_FXP_LSTAT:
-                doLStat(buffer, id);
-                break;
-            case SSH_FXP_FSTAT:
-                doFStat(buffer, id);
-                break;
-            case SSH_FXP_SETSTAT:
-                doSetStat(buffer, id);
-                break;
-            case SSH_FXP_FSETSTAT:
-                doFSetStat(buffer, id);
-                break;
-            case SSH_FXP_OPENDIR:
-                doOpenDir(buffer, id);
-                break;
-            case SSH_FXP_READDIR:
-                doReadDir(buffer, id);
-                break;
-            case SSH_FXP_REMOVE:
-                doRemove(buffer, id);
-                break;
-            case SSH_FXP_MKDIR:
-                doMakeDirectory(buffer, id);
-                break;
-            case SSH_FXP_RMDIR:
-                doRemoveDirectory(buffer, id);
-                break;
-            case SSH_FXP_REALPATH:
-                doRealPath(buffer, id);
-                break;
-            case SSH_FXP_STAT:
-                doStat(buffer, id);
-                break;
-            case SSH_FXP_RENAME:
-                doRename(buffer, id);
-                break;
-            case SSH_FXP_READLINK:
-                doReadLink(buffer, id);
-                break;
-            case SSH_FXP_SYMLINK:
-                doSymLink(buffer, id);
-                break;
-            case SSH_FXP_LINK:
-                doLink(buffer, id);
-                break;
-            case SSH_FXP_BLOCK:
-                doBlock(buffer, id);
-                break;
-            case SSH_FXP_UNBLOCK:
-                doUnblock(buffer, id);
-                break;
-            case SSH_FXP_EXTENDED:
-                doExtended(buffer, id);
-                break;
-            default:
-                log.warn("Unknown command type received: {}", Integer.valueOf(type));
-                sendStatus(id, SSH_FX_OP_UNSUPPORTED, "Command " + type + " is unsupported or not implemented");
-        }
-    }
-
-    protected void doExtended(Buffer buffer, int id) throws IOException {
-        String extension = buffer.getString();
-        switch (extension) {
-        case "text-seek":
-            doTextSeek(buffer, id);
-            break;
-        case "version-select":
-            doVersionSelect(buffer, id);
-            break;
-        default:
-            log.info("Received unsupported SSH_FXP_EXTENDED({})", extension);
-            sendStatus(id, SSH_FX_OP_UNSUPPORTED, "Command SSH_FXP_EXTENDED(" + extension + ") is unsupported or not implemented");
-            break;
-        }
-    }
-
-    protected void doTextSeek(Buffer buffer, int id) throws IOException {
-        String handle = buffer.getString();
-        long line = buffer.getLong();
-        if (log.isDebugEnabled()) {
-            log.debug("Received SSH_FXP_EXTENDED(text-seek) (handle={}, line={})", handle, Long.valueOf(line));
-        }
-
-        // TODO : implement text-seek
-        sendStatus(id, SSH_FX_OP_UNSUPPORTED, "Command SSH_FXP_EXTENDED(text-seek) is unsupported or not implemented");
-    }
-
-    protected void doVersionSelect(Buffer buffer, int id) throws IOException {
-        String ver = buffer.getString();
-        if (log.isDebugEnabled()) {
-            log.debug("Received SSH_FXP_EXTENDED(version-select) (version={})", Integer.valueOf(version));
-        }
-        
-        if (GenericUtils.length(ver) == 1) {
-            char digit = ver.charAt(0);
-            if ((digit >= '0') && (digit <= '9')) {
-                int value = digit - '0';
-                String all = checkVersionCompatibility(id, value, SSH_FX_FAILURE);
-                if (GenericUtils.isEmpty(all)) {    // validation failed
-                    return;
-                }
-
-                version = value;
-                sendStatus(id, SSH_FX_OK, "");
-                return;
-            }
-        }
-
-        sendStatus(id, SSH_FX_FAILURE, "Unsupported version " + ver);
-    }
-
-    /**
-     * Checks if a proposed version is within supported range. <B>Note:</B>
-     * if the user forced a specific value via the {@link #SFTP_VERSION}
-     * property, then it is used to validate the proposed value
-     * @param id The SSH message ID to be used to send the failure message
-     * if required
-     * @param proposed The proposed version value
-     * @param failureOpcode The failure opcode to send if validation fails
-     * @return A {@link String} of comma separated values representing all
-     * the supported version - {@code null} if validation failed and an
-     * appropriate status message was sent
-     * @throws IOException If failed to send the failure status message
-     */
-    protected String checkVersionCompatibility(int id, int proposed, int failureOpcode) throws IOException {
-        int low = LOWER_SFTP_IMPL;
-        int hig = HIGHER_SFTP_IMPL;
-        String available = ALL_SFTP_IMPL;
-        // check if user wants to use a specific version
-        Integer sftpVersion = FactoryManagerUtils.getInteger(session, SFTP_VERSION);
-        if (sftpVersion != null) {
-            int forcedValue = sftpVersion.intValue();
-            if ((forcedValue < LOWER_SFTP_IMPL) || (forcedValue > HIGHER_SFTP_IMPL)) {
-                throw new IllegalStateException("Forced SFTP version (" + sftpVersion + ") not within supported values: " + available);
-            }
-            low = hig = sftpVersion.intValue();
-            available = sftpVersion.toString();
-        }
-
-        if (log.isTraceEnabled()) {
-            log.trace("checkVersionCompatibility(id={}) - proposed={}, available={}",
-                      new Object[] { Integer.valueOf(id), Integer.valueOf(proposed), available });
-        }
-
-        if ((proposed < low) || (proposed > hig)) {
-            sendStatus(id, failureOpcode, "Proposed version (" + proposed + ") not in supported range: " + available);
-            return null;
-        }
-
-        return available;
-    }
-
-    protected void doBlock(Buffer buffer, int id) throws IOException {
-        String handle = buffer.getString();
-        long offset = buffer.getLong();
-        long length = buffer.getLong();
-        int mask = buffer.getInt();
-        
-        if (log.isDebugEnabled()) {
-            log.debug("Received SSH_FXP_BLOCK (handle={}, offset={}, length={}, mask=0x{})",
-                      new Object[] { handle, Long.valueOf(offset), Long.valueOf(length), Integer.toHexString(mask) });
-        }
-
-        try {
-            Handle p = handles.get(handle);
-            if (!(p instanceof FileHandle)) {
-                sendStatus(id, SSH_FX_INVALID_HANDLE, handle);
-                return;
-            }
-            FileHandle fileHandle = (FileHandle) p;
-            fileHandle.lock(offset, length, mask);
-            sendStatus(id, SSH_FX_OK, "");
-        } catch (IOException | OverlappingFileLockException e) {
-            sendStatus(id, e);
-        }
-    }
-
-    protected void doUnblock(Buffer buffer, int id) throws IOException {
-        String handle = buffer.getString();
-        long offset = buffer.getLong();
-        long length = buffer.getLong();
-        if (log.isDebugEnabled()) {
-            log.debug("Received SSH_FXP_UNBLOCK (handle={}, offset={}, length={})",
-                      new Object[] { handle, Long.valueOf(offset), Long.valueOf(length) });
-        }
-
-        try {
-            Handle p = handles.get(handle);
-            if (!(p instanceof FileHandle)) {
-                sendStatus(id, SSH_FX_INVALID_HANDLE, handle);
-                return;
-            }
-            FileHandle fileHandle = (FileHandle) p;
-            boolean found = fileHandle.unlock(offset, length);
-            sendStatus(id, found ? SSH_FX_OK : SSH_FX_NO_MATCHING_BYTE_RANGE_LOCK, "");
-        } catch (IOException e) {
-            sendStatus(id, e);
-        }
-    }
-
-    protected void doLink(Buffer buffer, int id) throws IOException {
-        String targetpath = buffer.getString();
-        String linkpath = buffer.getString();
-        boolean symLink = buffer.getBoolean();
-        if (log.isDebugEnabled()) {
-            log.debug("Received SSH_FXP_LINK (linkpath={}, targetpath={}, symlink={})",
-                      new Object[] { linkpath, targetpath, Boolean.valueOf(symLink) });
-        }
-
-        try {
-            Path link = resolveFile(linkpath);
-            Path target = fileSystem.getPath(targetpath);
-            if (symLink) {
-                Files.createSymbolicLink(link, target);
-            } else {
-                Files.createLink(link, target);
-            }
-            sendStatus(id, SSH_FX_OK, "");
-        } catch (UnsupportedOperationException e) {
-            sendStatus(id, SSH_FX_OP_UNSUPPORTED, "Command SSH_FXP_SYMLINK is unsupported or not implemented");
-        } catch (IOException e) {
-            sendStatus(id, e);
-        }
-    }
-
-    protected void doSymLink(Buffer buffer, int id) throws IOException {
-        String targetpath = buffer.getString();
-        String linkpath = buffer.getString();
-        log.debug("Received SSH_FXP_SYMLINK (linkpath={}, targetpath={})", linkpath, targetpath);
-        try {
-            Path link = resolveFile(linkpath);
-            Path target = fileSystem.getPath(targetpath);
-            Files.createSymbolicLink(link, target);
-            sendStatus(id, SSH_FX_OK, "");
-        } catch (UnsupportedOperationException e) {
-            sendStatus(id, SSH_FX_OP_UNSUPPORTED, "Command SSH_FXP_SYMLINK is unsupported or not implemented");
-        } catch (IOException e) {
-            sendStatus(id, e);
-        }
-    }
-
-    protected void doReadLink(Buffer buffer, int id) throws IOException {
-        String path = buffer.getString();
-        log.debug("Received SSH_FXP_READLINK (path={})", path);
-        try {
-            Path f = resolveFile(path);
-            String l = Files.readSymbolicLink(f).toString();
-            sendLink(id, l);
-        } catch (UnsupportedOperationException e) {
-            sendStatus(id, SSH_FX_OP_UNSUPPORTED, "Command SSH_FXP_READLINK is unsupported or not implemented");
-        } catch (IOException e) {
-            sendStatus(id, e);
-        }
-    }
-
-    protected void doRename(Buffer buffer, int id) throws IOException {
-        String oldPath = buffer.getString();
-        String newPath = buffer.getString();
-        int flags = 0;
-        if (version >= SFTP_V5) {
-            flags = buffer.getInt();
-        }
-        if (log.isDebugEnabled()) {
-            log.debug("Received SSH_FXP_RENAME (oldPath={}, newPath={}, flags=0x{})",
-                       new Object[] { oldPath, newPath, Integer.toHexString(flags) });
-        }
-        try {
-            List<CopyOption> opts = new ArrayList<>();
-            if ((flags & SSH_FXP_RENAME_ATOMIC) != 0) {
-                opts.add(StandardCopyOption.ATOMIC_MOVE);
-            }
-            if ((flags & SSH_FXP_RENAME_OVERWRITE) != 0) {
-                opts.add(StandardCopyOption.REPLACE_EXISTING);
-            }
-            Path o = resolveFile(oldPath);
-            Path n = resolveFile(newPath);
-            Files.move(o, n, opts.toArray(new CopyOption[opts.size()]));
-            sendStatus(id, SSH_FX_OK, "");
-        } catch (IOException e) {
-            sendStatus(id, e);
-        }
-    }
-
-    protected void doStat(Buffer buffer, int id) throws IOException {
-        String path = buffer.getString();
-        int flags = SSH_FILEXFER_ATTR_ALL;
-        if (version >= SFTP_V4) {
-            flags = buffer.getInt();
-        }
-        if (log.isDebugEnabled()) {
-            log.debug("Received SSH_FXP_STAT (path={}, flags={})", path, "0x" + Integer.toHexString(flags));
-        }
-        try {
-            Path p = resolveFile(path);
-            sendAttrs(id, p, flags, true);
-        } catch (IOException e) {
-            sendStatus(id, e);
-        }
-    }
-
-    protected void doRealPath(Buffer buffer, int id) throws IOException {
-        String path = buffer.getString();
-        log.debug("Received SSH_FXP_REALPATH (path={})", path);
-        path = GenericUtils.trimToEmpty(path);
-        if (GenericUtils.isEmpty(path)) {
-            path = ".";
-        }
-
-        try {
-            if (version < SFTP_V6) {
-                Path f = resolveFile(path);
-                Path abs = f.toAbsolutePath();
-                Path p = abs.normalize();
-                Boolean status = IoUtils.checkFileExists(p, IoUtils.EMPTY_LINK_OPTIONS);
-                if (status == null) {
-                    p = handleUnknownRealPathStatus(path, abs, p);
-                } else if (!status.booleanValue()) {
-                    throw new FileNotFoundException(p.toString());
-                }
-                sendPath(id, p, Collections.<String, Object>emptyMap());
-            } else {
-                // Read control byte
-                int control = 0;
-                if (buffer.available() > 0) {
-                    control = buffer.getByte();
-                }
-                List<String> paths = new ArrayList<>();
-                while (buffer.available() > 0) {
-                    paths.add(buffer.getString());
-                }
-                // Resolve path
-                Path p = resolveFile(path);
-                for (String p2 : paths) {
-                    p = p.resolve(p2);
-                }
-                p = p.toAbsolutePath().normalize();
-
-                Map<String, Object> attrs = Collections.emptyMap();
-                if (control == SSH_FXP_REALPATH_STAT_IF) {
-                    try {
-                        attrs = getAttributes(p, false);
-                    } catch (IOException e) {
-                        // ignore
-                    }
-                } else if (control == SSH_FXP_REALPATH_STAT_ALWAYS) {
-                    attrs = getAttributes(p, false);
-                }
-                sendPath(id, p, attrs);
-            }
-        } catch (IOException e) {
-            sendStatus(id, e);
-        }
-    }
-
-    protected Path handleUnknownRealPathStatus(String path, Path absolute, Path normalized) throws IOException {
-        switch(unsupportedAttributePolicy) {
-            case Ignore:
-                break;
-            case Warn:
-                log.warn("handleUnknownRealPathStatus(" + path + ") abs=" + absolute + ", normal=" + normalized);
-                break;
-            case ThrowException:
-                throw new AccessDeniedException("Cannot determine existence status of real path: " + normalized);
-            
-            default:
-                log.warn("handleUnknownRealPathStatus(" + path + ") abs=" + absolute + ", normal=" + normalized
-                       + " - unknown policy: " + unsupportedAttributePolicy);
-        }
-        
-        return absolute;
-    }
-
-    protected void doRemoveDirectory(Buffer buffer, int id) throws IOException {
-        String path = buffer.getString();
-        log.debug("Received SSH_FXP_RMDIR (path={})", path);
-        // attrs
-        try {
-            Path p = resolveFile(path);
-            if (Files.isDirectory(p, IoUtils.getLinkOptions(false))) {
-                Files.delete(p);
-                sendStatus(id, SSH_FX_OK, "");
-            } else {
-                sendStatus(id, SSH_FX_NO_SUCH_FILE, p.toString());
-            }
-        } catch (IOException e) {
-            sendStatus(id, e);
-        }
-    }
-
-    protected void doMakeDirectory(Buffer buffer, int id) throws IOException {
-        String path = buffer.getString();
-        Map<String, Object> attrs = readAttrs(buffer);
-
-        log.debug("Received SSH_FXP_MKDIR (path={})", path);
-        // attrs
-        try {
-            Path            p = resolveFile(path);
-            LinkOption[]    options = IoUtils.getLinkOptions(false);
-            Boolean         status = IoUtils.checkFileExists(p, options);
-            if (status == null) {
-                throw new AccessDeniedException("Cannot make-directory existence for " + p);
-            }
-            if (status.booleanValue()) {
-                if (Files.isDirectory(p, options)) {
-                    sendStatus(id, SSH_FX_FILE_ALREADY_EXISTS, p.toString());
-                } else {
-                    sendStatus(id, SSH_FX_NO_SUCH_FILE, p.toString());
-                }
-            } else {
-                Files.createDirectory(p);
-                setAttributes(p, attrs);
-                sendStatus(id, SSH_FX_OK, "");
-            }
-        } catch (IOException e) {
-            sendStatus(id, e);
-        }
-    }
-
-    protected void doRemove(Buffer buffer, int id) throws IOException {
-        String path = buffer.getString();
-        log.debug("Received SSH_FXP_REMOVE (path={})", path);
-        try {
-            Path            p = resolveFile(path);
-            LinkOption[]    options = IoUtils.getLinkOptions(false);
-            Boolean         status = IoUtils.checkFileExists(p, options);
-            if (status == null) {
-                throw new AccessDeniedException("Cannot determine existence of remove candidate: " + p);
-            }
-            if (!status.booleanValue()) {
-                sendStatus(id, SSH_FX_NO_SUCH_FILE, p.toString());
-            } else if (Files.isDirectory(p, options)) {
-                sendStatus(id, SSH_FX_NO_SUCH_FILE, p.toString());
-            } else {
-                Files.delete(p);
-                sendStatus(id, SSH_FX_OK, "");
-            }
-        } catch (IOException e) {
-            sendStatus(id, e);
-        }
-    }
-
-    protected void doReadDir(Buffer buffer, int id) throws IOException {
-        String handle = buffer.getString();
-        log.debug("Received SSH_FXP_READDIR (handle={})", handle);
-        Handle p = handles.get(handle);
-        try {
-            if (!(p instanceof DirectoryHandle)) {
-                sendStatus(id, SSH_FX_INVALID_HANDLE, handle);
-                return;
-            }
-            
-            if (((DirectoryHandle) p).isDone()) {
-                sendStatus(id, SSH_FX_EOF, "", "");
-                return;
-            }
-
-            Path            file = p.getFile();
-            LinkOption[]    options = IoUtils.getLinkOptions(false);
-            Boolean         status = IoUtils.checkFileExists(file, options);
-            if (status == null) {
-                throw new AccessDeniedException("Cannot determine existence of read-dir for " + file);
-            }
-
-            if (!status.booleanValue()) {
-                sendStatus(id, SSH_FX_NO_SUCH_FILE, file.toString());
-            } else if (!Files.isDirectory(file, options)) {
-                sendStatus(id, SSH_FX_NOT_A_DIRECTORY, file.toString());
-            } else if (!Files.isReadable(file)) {
-                sendStatus(id, SSH_FX_PERMISSION_DENIED, file.toString());
-            } else {
-                DirectoryHandle dh = (DirectoryHandle) p;
-                if (dh.hasNext()) {
-                    // There is at least one file in the directory.
-                    // Send only a few files at a time to not create packets of a too
-                    // large size or have a timeout to occur.
-                    sendName(id, dh);
-                    if (!dh.hasNext()) {
-                        // if no more files to send
-                        dh.setDone(true);
-                        dh.clearFileList();
-                    }
-                } else {
-                    // empty directory
-                    dh.setDone(true);
-                    dh.clearFileList();
-                    sendStatus(id, SSH_FX_EOF, "", "");
-                }
-            }
-        } catch (IOException e) {
-            sendStatus(id, e);
-        }
-    }
-
-    protected void doOpenDir(Buffer buffer, int id) throws IOException {
-        String path = buffer.getString();
-        log.debug("Received SSH_FXP_OPENDIR (path={})", path);
-        try {
-            Path            p = resolveFile(path);
-            LinkOption[]    options = IoUtils.getLinkOptions(false);
-            Boolean         status = IoUtils.checkFileExists(p, options);
-            if (status == null) {
-                throw new AccessDeniedException("Cannot determine open-dir existence of " + p);
-            }
-
-            if (!status.booleanValue()) {
-                sendStatus(id, SSH_FX_NO_SUCH_FILE, path);
-            } else if (!Files.isDirectory(p, options)) {
-                sendStatus(id, SSH_FX_NOT_A_DIRECTORY, path);
-            } else if (!Files.isReadable(p)) {
-                sendStatus(id, SSH_FX_PERMISSION_DENIED, path);
-            } else {
-                String handle = UUID.randomUUID().toString();
-                handles.put(handle, new DirectoryHandle(p));
-                sendHandle(id, handle);
-            }
-        } catch (IOException e) {
-            sendStatus(id, e);
-        }
-    }
-
-    protected void doFSetStat(Buffer buffer, int id) throws IOException {
-        String handle = buffer.getString();
-        Map<String, Object> attrs = readAttrs(buffer);
-        log.debug("Received SSH_FXP_FSETSTAT (handle={}, attrs={})", handle, attrs);
-        try {
-            Handle p = handles.get(handle);
-            if (p == null) {
-                sendStatus(id, SSH_FX_INVALID_HANDLE, handle);
-            } else {
-                setAttributes(p.getFile(), attrs);
-                sendStatus(id, SSH_FX_OK, "");
-            }
-        } catch (IOException | UnsupportedOperationException e) {
-            sendStatus(id, e);
-        }
-    }
-
-    protected void doSetStat(Buffer buffer, int id) throws IOException {
-        String path = buffer.getString();
-        Map<String, Object> attrs = readAttrs(buffer);
-        log.debug("Received SSH_FXP_SETSTAT (path={}, attrs={})", path, attrs);
-        try {
-            Path p = resolveFile(path);
-            setAttributes(p, attrs);
-            sendStatus(id, SSH_FX_OK, "");
-        } catch (IOException | UnsupportedOperationException e) {
-            sendStatus(id, e);
-        }
-    }
-
-    protected void doFStat(Buffer buffer, int id) throws IOException {
-        String handle = buffer.getString();
-        int flags = SSH_FILEXFER_ATTR_ALL;
-        if (version >= SFTP_V4) {
-            flags = buffer.getInt();
-        }
-        if (log.isDebugEnabled()) {
-            log.debug("Received SSH_FXP_FSTAT (handle={}, flags={})", handle, "0x" + Integer.toHexString(flags));
-        }
-        try {
-            Handle p = handles.get(handle);
-            if (p == null) {
-                sendStatus(id, SSH_FX_INVALID_HANDLE, handle);
-            } else {
-                sendAttrs(id, p.getFile(), flags, true);
-            }
-        } catch (IOException e) {
-            sendStatus(id, e);
-        }
-    }
-
-    protected void doLStat(Buffer buffer, int id) throws IOException {
-        String path = buffer.getString();
-        int flags = SSH_FILEXFER_ATTR_ALL;
-        if (version >= SFTP_V4) {
-            flags = buffer.getInt();
-        }
-        if (log.isDebugEnabled()) {
-            log.debug("Received SSH_FXP_LSTAT (path={}, flags={})", path, "0x" + Integer.toHexString(flags));
-        }
-        try {
-            Path p = resolveFile(path);
-            sendAttrs(id, p, flags, false);
-        } catch (IOException e) {
-            sendStatus(id, e);
-        }
-    }
-
-    protected void doWrite(Buffer buffer, int id) throws IOException {
-        String handle = buffer.getString();
-        long offset = buffer.getLong();
-        int length = buffer.getInt();
-        if (length < 0) {
-            throw new IllegalStateException();
-        }
-        if (buffer.available() < length) {
-            throw new BufferUnderflowException();
-        }
-        byte[] data = buffer.array();
-        int doff = buffer.rpos();
-        if (log.isDebugEnabled()) {
-            log.debug("Received SSH_FXP_WRITE (handle={}, offset={}, data=byte[{}])",
-                      new Object[] { handle, Long.valueOf(offset), Integer.valueOf(length) });
-        }
-        try {
-            Handle p = handles.get(handle);
-            if (!(p instanceof FileHandle)) {
-                sendStatus(id, SSH_FX_INVALID_HANDLE, handle);
-            } else {
-                FileHandle fh = (FileHandle) p;
-                fh.write(data, doff, length, offset);
-                sendStatus(id, SSH_FX_OK, "");
-            }
-        } catch (IOException e) {
-            sendStatus(id, e);
-        }
-    }
-
-    protected void doRead(Buffer buffer, int id) throws IOException {
-        String handle = buffer.getString();
-        long offset = buffer.getLong();
-        int len = buffer.getInt();
-        if (log.isDebugEnabled()) {
-            log.debug("Received SSH_FXP_READ (handle={}, offset={}, length={})",
-                      new Object[]{handle, Long.valueOf(offset), Integer.valueOf(len) });
-        }
-        try {
-            Handle p = handles.get(handle);
-            if (!(p instanceof FileHandle)) {
-                sendStatus(id, SSH_FX_INVALID_HANDLE, handle);
-            } else {
-                FileHandle fh = (FileHandle) p;
-                Buffer buf = new ByteArrayBuffer(len + 9);
-                buf.putByte((byte) SSH_FXP_DATA);
-                buf.putInt(id);
-                int pos = buf.wpos();
-                buf.putInt(0);
-                len = fh.read(buf.array(), buf.wpos(), len, offset);
-                if (len >= 0) {
-                    buf.wpos(pos);
-                    buf.putInt(len);
-                    buf.wpos(pos + 4 + len);
-                    send(buf);
-                } else {
-                    sendStatus(id, SSH_FX_EOF, "");
-                }
-            }
-        } catch (IOException e) {
-            sendStatus(id, e);
-        }
-    }
-
-    protected void doClose(Buffer buffer, int id) throws IOException {
-        String handle = buffer.getString();
-        log.debug("Received SSH_FXP_CLOSE (handle={})", handle);
-        try {
-            Handle h = handles.get(handle);
-            if (h == null) {
-                sendStatus(id, SSH_FX_INVALID_HANDLE, handle, "");
-            } else {
-                handles.remove(handle);
-                h.close();
-                sendStatus(id, SSH_FX_OK, "", "");
-            }
-        } catch (IOException e) {
-            sendStatus(id, e);
-        }
-    }
-
-    protected void doOpen(Buffer buffer, int id) throws IOException {
-        int maxHandleCount = FactoryManagerUtils.getIntProperty(session, MAX_OPEN_HANDLES_PER_SESSION, Integer.MAX_VALUE);
-        if (handles.size() > maxHandleCount) {
-            sendStatus(id, SSH_FX_FAILURE, "Too many open handles");
-            return;
-        }
-
-        String path = buffer.getString();
-        int access = 0;
-        if (version >= SFTP_V5) {
-            access = buffer.getInt();
-        }
-        int pflags = buffer.getInt();
-        if (version < SFTP_V5) {
-            int flags = pflags;
-            pflags = 0;
-            switch (flags & (SSH_FXF_READ | SSH_FXF_WRITE)) {
-            case SSH_FXF_READ:
-                access |= ACE4_READ_DATA | ACE4_READ_ATTRIBUTES;
-                break;
-            case SSH_FXF_WRITE:
-                access |= ACE4_WRITE_DATA | ACE4_WRITE_ATTRIBUTES;
-                break;
-            default:
-                access |= ACE4_READ_DATA | ACE4_READ_ATTRIBUTES;
-                access |= ACE4_WRITE_DATA | ACE4_WRITE_ATTRIBUTES;
-                break;
-            }
-            if ((flags & SSH_FXF_APPEND) != 0) {
-                access |= ACE4_APPEND_DATA;
-                pflags |= SSH_FXF_APPEND_DATA | SSH_FXF_APPEND_DATA_ATOMIC;
-            }
-            if ((flags & SSH_FXF_CREAT) != 0) {
-                if ((flags & SSH_FXF_EXCL) != 0) {
-                    pflags |= SSH_FXF_CREATE_NEW;
-                } else if ((flags & SSH_FXF_TRUNC) != 0) {
-                    pflags |= SSH_FXF_CREATE_TRUNCATE;
-                } else {
-                    pflags |= SSH_FXF_OPEN_OR_CREATE;
-                }
-            } else {
-                if ((flags & SSH_FXF_TRUNC) != 0) {
-                    pflags |= SSH_FXF_TRUNCATE_EXISTING;
-                } else {
-                    pflags |= SSH_FXF_OPEN_EXISTING;
-                }
-            }
-        }
-        Map<String, Object> attrs = readAttrs(buffer);
-        if (log.isDebugEnabled()) {
-            log.debug("Received SSH_FXP_OPEN (path={}, access=0x{}, pflags=0x{}, attrs={})",
-                      new Object[]{path, Integer.toHexString(access), Integer.toHexString(pflags), attrs});
-        }
-        try {
-            Path file = resolveFile(path);
-            String handle = UUID.randomUUID().toString();
-            handles.put(handle, new FileHandle(file, pflags, access, attrs));
-            sendHandle(id, handle);
-        } catch (IOException e) {
-            sendStatus(id, e);
-        }
-    }
-
-    protected void doInit(Buffer buffer, int id) throws IOException {
-        if (log.isDebugEnabled()) {
-            log.debug("Received SSH_FXP_INIT (version={})", Integer.valueOf(id));
-        }
-
-        String all = checkVersionCompatibility(id, id, SSH_FX_OP_UNSUPPORTED);
-        if (GenericUtils.isEmpty(all)) { // i.e. validation failed
-            return;
-        }
-        version = id;
-        while (buffer.available() > 0) {
-            String name = buffer.getString();
-            byte[] data = buffer.getBytes();
-            extensions.put(name, data);
-        }
-
-        buffer.clear();
-        buffer.putByte((byte) SSH_FXP_VERSION);
-        buffer.putInt(version);
-
-        // newline
-        buffer.putString("newline");
-        buffer.putString(System.getProperty("line.separator"));
-
-        // versions
-        buffer.putString("versions");
-        buffer.putString(all);
-
-        // supported
-        buffer.putString("supported");
-        buffer.putInt(5 * 4); // length of 5 integers
-        // supported-attribute-mask
-        buffer.putInt(SSH_FILEXFER_ATTR_SIZE | SSH_FILEXFER_ATTR_PERMISSIONS
-                | SSH_FILEXFER_ATTR_ACCESSTIME | SSH_FILEXFER_ATTR_CREATETIME
-                | SSH_FILEXFER_ATTR_MODIFYTIME | SSH_FILEXFER_ATTR_OWNERGROUP
-                | SSH_FILEXFER_ATTR_BITS);
-        // TODO: supported-attribute-bits
-        buffer.putInt(0);
-        // supported-open-flags
-        buffer.putInt(SSH_FXF_READ | SSH_FXF_WRITE | SSH_FXF_APPEND
-                | SSH_FXF_CREAT | SSH_FXF_TRUNC | SSH_FXF_EXCL);
-        // TODO: supported-access-mask
-        buffer.putInt(0);
-        // max-read-size
-        buffer.putInt(0);
-
-        // supported2
-        buffer.putString("supported2");
-        buffer.putInt(8 * 4); // length of 7 integers + 2 shorts
-        // supported-attribute-mask
-        buffer.putInt(SSH_FILEXFER_ATTR_SIZE | SSH_FILEXFER_ATTR_PERMISSIONS
-                | SSH_FILEXFER_ATTR_ACCESSTIME | SSH_FILEXFER_ATTR_CREATETIME
-                | SSH_FILEXFER_ATTR_MODIFYTIME | SSH_FILEXFER_ATTR_OWNERGROUP
-                | SSH_FILEXFER_ATTR_BITS);
-        // TODO: supported-attribute-bits
-        buffer.putInt(0);
-        // supported-open-flags
-        buffer.putInt(SSH_FXF_ACCESS_DISPOSITION | SSH_FXF_APPEND_DATA);
-        // TODO: supported-access-mask
-        buffer.putInt(0);
-        // max-read-size
-        buffer.putInt(0);
-        // supported-open-block-vector
-        buffer.putShort(0);
-        // supported-block-vector
-        buffer.putShort(0);
-        // attrib-extension-count
-        buffer.putInt(0);
-        // extension-count
-        buffer.putInt(0);
-
-        /*
-        buffer.putString("acl-supported");
-        buffer.putInt(4);
-        // capabilities
-        buffer.putInt(0);
-        */
-
-        send(buffer);
-    }
-
-    protected void sendHandle(int id, String handle) throws IOException {
-        Buffer buffer = new ByteArrayBuffer();
-        buffer.putByte((byte) SSH_FXP_HANDLE);
-        buffer.putInt(id);
-        buffer.putString(handle);
-        send(buffer);
-    }
-
-    protected void sendAttrs(int id, Path file, int flags, boolean followLinks) throws IOException {
-        Buffer buffer = new ByteArrayBuffer();
-        buffer.putByte((byte) SSH_FXP_ATTRS);
-        buffer.putInt(id);
-        writeAttrs(buffer, file, flags, followLinks);
-        send(buffer);
-    }
-
-    protected void sendPath(int id, Path f, Map<String, Object> attrs) throws IOException {
-        Buffer buffer = new ByteArrayBuffer();
-        buffer.putByte((byte) SSH_FXP_NAME);
-        buffer.putInt(id);
-        buffer.putInt(1);
-
-        String originalPath = f.toString();
-        //in case we are running on Windows
-        String unixPath = originalPath.replace(File.separatorChar, '/');
-        //normalize the given path, use *nix style separator
-        String normalizedPath = SelectorUtils.normalizePath(unixPath, "/");
-        if (normalizedPath.length() == 0) {
-            normalizedPath = "/";
-        }
-        buffer.putString(normalizedPath, StandardCharsets.UTF_8);
-
-        if (version == SFTP_V3) {
-            f = resolveFile(normalizedPath);
-            buffer.putString(getLongName(f, attrs), StandardCharsets.UTF_8); // Format specified in the specs
-            buffer.putInt(0);
-        } else if (version >= SFTP_V4) {
-            writeAttrs(buffer, attrs);
-        } else {
-            throw new IllegalStateException("sendPath(" + f + ") unsupported version: " + version);
-        }
-        send(buffer);
-    }
-
-    protected void sendLink(int id, String link) throws IOException {
-        Buffer buffer = new ByteArrayBuffer();
-        buffer.putByte((byte) SSH_FXP_NAME);
-        buffer.putInt(id);
-        buffer.putInt(1);
-        //normalize the given path, use *nix style separator
-        buffer.putString(link);
-        buffer.putString(link);
-        buffer.putInt(0);
-        send(buffer);
-    }
-
-    protected void sendName(int id, Iterator<Path> files) throws IOException {
-        Buffer buffer = new ByteArrayBuffer();
-        buffer.putByte((byte) SSH_FXP_NAME);
-        buffer.putInt(id);
-        int wpos = buffer.wpos();
-        buffer.putInt(0);
-        int nb = 0;
-        while (files.hasNext() && (buffer.wpos() < MAX_PACKET_LENGTH)) {
-            Path    f = files.next();
-            String  shortName = getShortName(f);
-            buffer.putString(shortName, StandardCharsets.UTF_8);
-            if (version == SFTP_V3) {
-                String  longName = getLongName(f);
-                buffer.putString(longName, StandardCharsets.UTF_8); // Format specified in the specs
-                if (log.isTraceEnabled()) {
-                    log.trace("sendName(id=" + id + ")[" + nb + "] - " + shortName + " [" + longName + "]");
-                }
-            } else {
-                if (log.isTraceEnabled()) {
-                    log.trace("sendName(id=" + id + ")[" + nb + "] - " + shortName);
-                }
-            }
-            writeAttrs(buffer, f, SSH_FILEXFER_ATTR_ALL, false);
-            nb++;
-        }
-
-        int oldpos = buffer.wpos();
-        buffer.wpos(wpos);
-        buffer.putInt(nb);
-        buffer.wpos(oldpos);
-        send(buffer);
-    }
-
-    private String getLongName(Path f) throws IOException {
-        return getLongName(f, true);
-    }
-
-    private String getLongName(Path f, boolean sendAttrs) throws IOException {
-        Map<String, Object> attributes;
-        if (sendAttrs) {
-            attributes = getAttributes(f, false);
-        } else {
-            attributes = Collections.emptyMap();
-        }
-        return getLongName(f, attributes);
-    }
-
-    private String getLongName(Path f, Map<String, Object> attributes) throws IOException {
-        String username;
-        if (attributes.containsKey("owner")) {
-            username = attributes.get("owner").toString();
-        } else {
-            username = "owner";
-        }
-        if (username.length() > 8) {
-            username = username.substring(0, 8);
-        } else {
-            for (int i = username.length(); i < 8; i++) {
-                username = username + " ";
-            }
-        }
-        String group;
-        if (attributes.containsKey("group")) {
-            group = attributes.get("group").toString();
-        } else {
-            group = "group";
-        }
-        if (group.length() > 8) {
-            group = group.substring(0, 8);
-        } else {
-            for (int i = group.length(); i < 8; i++) {
-                group = group + " ";
-            }
-        }
-
-        Number length = (Number) attributes.get("size");
-        if (length == null) {
-            length = Long.valueOf(0L);
-        }
-        String lengthString = String.format("%1$8s", length);
-
-        Boolean isDirectory = (Boolean) attributes.get("isDirectory");
-        Boolean isLink = (Boolean) attributes.get("isSymbolicLink");
-        @SuppressWarnings("unchecked")
-        Set<PosixFilePermission> perms = (Set<PosixFilePermission>) attributes.get("permissions");
-        if (perms == null) {
-            perms = EnumSet.noneOf(PosixFilePermission.class);
-        }
-
-        StringBuilder sb = new StringBuilder();
-        sb.append((isDirectory != null && isDirectory.booleanValue()) ? "d" : (isLink != null && isLink.booleanValue()) ? "l" : "-");
-        sb.append(PosixFilePermissions.toString(perms));
-        sb.append("  ");
-        sb.append(attributes.containsKey("nlink") ? attributes.get("nlink") : "1");
-        sb.append(" ");
-        sb.append(username);
-        sb.append(" ");
-        sb.append(group);
-        sb.append(" ");
-        sb.append(lengthString);
-        sb.append(" ");
-        sb.append(getUnixDate((FileTime) attributes.get("lastModifiedTime")));
-        sb.append(" ");
-        sb.append(getShortName(f));
-
-        return sb.toString();
-    }
-
-    protected String getShortName(Path f) {
-        if (OsUtils.isUNIX()) {
-            Path    name=f.getFileName();
-            if (name == null) {
-                Path    p=resolveFile(".");
-                name = p.getFileName();
-            }
-            
-            return name.toString();
-        } else {    // need special handling for Windows root drives
-            Path    abs=f.toAbsolutePath().normalize();
-            int     count=abs.getNameCount();
-            /*
-             * According to the javadoc:
-             * 
-             *      The number of elements in the path, or 0 if this path only
-             *      represents a root component
-             */
-            if (count > 0) {
-                Path    name=abs.getFileName();
-                return name.toString();
-            } else {
-                return abs.toString().replace(File.separatorChar, '/');
-            }
-        }
-    }
-
-    protected int attributesToPermissions(boolean isReg, boolean isDir, boolean isLnk, Collection<PosixFilePermission> perms) {
-        int pf = 0;
-        if (perms != null) {
-            for (PosixFilePermission p : perms) {
-                switch (p) {
-                case OWNER_READ:
-                    pf |= S_IRUSR;
-                    break;
-                case OWNER_WRITE:
-                    pf |= S_IWUSR;
-                    break;
-                case OWNER_EXECUTE:
-                    pf |= S_IXUSR;
-                    break;
-                case GROUP_READ:
-                    pf |= S_IRGRP;
-                    break;
-                case GROUP_WRITE:
-                    pf |= S_IWGRP;
-                    break;
-                case GROUP_EXECUTE:
-                    pf |= S_IXGRP;
-                    break;
-                case OTHERS_READ:
-                    pf |= S_IROTH;
-                    break;
-                case OTHERS_WRITE:
-                    pf |= S_IWOTH;
-                    break;
-                case OTHERS_EXECUTE:
-                    pf |= S_IXOTH;
-                    break;
-                default: // ignored
-                }
-            }
-        }
-        pf |= isReg ? S_IFREG : 0;
-        pf |= isDir ? S_IFDIR : 0;
-        pf |= isLnk ? S_IFLNK : 0;
-        return pf;
-    }
-
-    protected void writeAttrs(Buffer buffer, Path file, int flags, boolean followLinks) throws IOException {
-        LinkOption[]    options = IoUtils.getLinkOptions(followLinks);
-        Boolean         status = IoUtils.checkFileExists(file, options);
-        Map<String, Object> attributes;
-        if (status == null) {
-            attributes = handleUnknownStatusFileAttributes(file, flags, followLinks);
-        } else if (!status.booleanValue()) {
-            throw new FileNotFoundException(file.toString());
-        } else {
-            attributes = getAttributes(file, flags, followLinks);
-        }
-
-        writeAttrs(buffer, attributes);
-    }
-
-    protected void writeAttrs(Buffer buffer, Map<String, Object> attributes) throws IOException {
-        boolean isReg = getBool((Boolean) attributes.get("isRegularFile"));
-        boolean isDir = getBool((Boolean) attributes.get("isDirectory"));
-        boolean isLnk = getBool((Boolean) attributes.get("isSymbolicLink"));
-        @SuppressWarnings("unchecked")
-        Collection<PosixFilePermission> perms = (Collection<PosixFilePermission>) attributes.get("permissions");
-        Number size = (Number) attributes.get("size");
-        FileTime lastModifiedTime = (FileTime) attributes.get("lastModifiedTime");
-        FileTime lastAccessTime = (FileTime) attributes.get("lastAccessTime");
-
-        if (version == SFTP_V3) {
-            int flags =
-                    ((isReg || isLnk) && (size != null) ? SSH_FILEXFER_ATTR_SIZE : 0) |
-                    (attributes.containsKey("uid") && attributes.containsKey("gid") ? SSH_FILEXFER_ATTR_UIDGID : 0) |
-                    ((perms != null) ? SSH_FILEXFER_ATTR_PERMISSIONS : 0) |
-                    (((lastModifiedTime != null) && (lastAccessTime != null)) ? SSH_FILEXFER_ATTR_ACMODTIME : 0);
-            buffer.putInt(flags);
-            if ((flags & SSH_FILEXFER_ATTR_SIZE) != 0) {
-                buffer.putLong(size.longValue());
-            }
-            if ((flags & SSH_FILEXFER_ATTR_UIDGID) != 0) {
-                buffer.putInt(((Number) attributes.get("uid")).intValue());
-                buffer.putInt(((Number) attributes.get("gid")).intValue());
-            }
-            if ((flags & SSH_FILEXFER_ATTR_PERMISSIONS) != 0) {
-                buffer.putInt(attributesToPermissions(isReg, isDir, isLnk, perms));
-            }
-            if ((flags & SSH_FILEXFER_ATTR_ACMODTIME) != 0) {
-                buffer.putInt(lastAccessTime.to(TimeUnit.SECONDS));
-                buffer.putInt(lastModifiedTime.to(TimeUnit.SECONDS));
-            }
-        } else if (version >= SFTP_V4) {
-            FileTime creationTime = (FileTime) attributes.get("creationTime");
-            int flags = (((isReg || isLnk) && (size != null)) ? SSH_FILEXFER_ATTR_SIZE : 0) |
-                        ((attributes.containsKey("owner") && attributes.containsKey("group")) ? SSH_FILEXFER_ATTR_OWNERGROUP : 0) |
-                        ((perms != null) ? SSH_FILEXFER_ATTR_PERMISSIONS : 0) |
-                        ((lastModifiedTime != null) ? SSH_FILEXFER_ATTR_MODIFYTIME : 0) |
-                        ((creationTime != null) ? SSH_FILEXFER_ATTR_CREATETIME : 0) |
-                        ((lastAccessTime != null) ? SSH_FILEXFER_ATTR_ACCESSTIME : 0);
-            buffer.putInt(flags);
-            buffer.putByte((byte) (isReg ? SSH_FILEXFER_TYPE_REGULAR :
-                    isDir ? SSH_FILEXFER_TYPE_DIRECTORY :
-                            isLnk ? SSH_FILEXFER_TYPE_SYMLINK :
-                                    SSH_FILEXFER_TYPE_UNKNOWN));
-            if ((flags & SSH_FILEXFER_ATTR_SIZE) != 0) {
-                buffer.putLong(size.longValue());
-            }
-            if ((flags & SSH_FILEXFER_ATTR_OWNERGROUP) != 0) {
-                buffer.putString(attributes.get("owner").toString(), StandardCharsets.UTF_8);
-                buffer.putString(attributes.get("group").toString(), StandardCharsets.UTF_8);
-            }
-            if ((flags & SSH_FILEXFER_ATTR_PERMISSIONS) != 0) {
-                buffer.putInt(attributesToPermissions(isReg, isDir, isLnk, perms));
-            }
-
-            if ((flags & SSH_FILEXFER_ATTR_ACCESSTIME) != 0) {
-                putFileTime(buffer, flags, lastAccessTime);
-            }
-
-            if ((flags & SSH_FILEXFER_ATTR_CREATETIME) != 0) {
-                putFileTime(buffer, flags, lastAccessTime);
-            }
-            if ((flags & SSH_FILEXFER_ATTR_MODIFYTIME) != 0) {
-                putFileTime(buffer, flags, lastModifiedTime);
-            }
-            // TODO: acls
-            // TODO: bits
-            // TODO: extended
-        }
-    }
-
-    protected void putFileTime(Buffer buffer, int flags, FileTime time) {
-        buffer.putLong(time.to(TimeUnit.SECONDS));
-        if ((flags & SSH_FILEXFER_ATTR_SUBSECOND_TIMES) != 0) {
-            long nanos = time.to(TimeUnit.NANOSECONDS);
-            nanos = nanos % TimeUnit.SECONDS.toNanos(1);
-            buffer.putInt((int) nanos);
-        }
-    }
-
-    protected boolean getBool(Boolean bool) {
-        return (bool != null) && bool.booleanValue();
-    }
-
-    protected Map<String, Object> getAttributes(Path file, boolean followLinks) throws IOException {
-        return getAttributes(file, SSH_FILEXFER_ATTR_ALL, followLinks);
-    }
-
-    public static final List<String>    DEFAULT_UNIX_VIEW=Collections.singletonList("unix:*");
-
-    protected Map<String, Object> handleUnknownStatusFileAttributes(Path file, int flags, boolean followLinks) throws IOException {
-        switch(unsupportedAttributePolicy) {
-            case Ignore:
-                break;
-            case ThrowException:
-                throw new AccessDeniedException("Cannot determine existence for attributes of " + file);
-            case Warn:
-                log.warn("handleUnknownStatusFileAttributes(" + file + ") cannot determine existence");
-                break;
-            default:
-                log.warn("handleUnknownStatusFileAttributes(" + file + ") unknown policy: " + unsupportedAttributePolicy);
-        }
-        
-        return getAttributes(file, flags, followLinks);
-    }
-
-    protected Map<String, Object> getAttributes(Path file, int flags, boolean followLinks) throws IOException {
-        FileSystem          fs=file.getFileSystem();
-        Collection<String>  supportedViews=fs.supportedFileAttributeViews();
-        LinkOption[]        opts=IoUtils.getLinkOptions(followLinks);
-        Map<String,Object>  attrs=new HashMap<>();
-        Collection<String>  views;
-
-        if (GenericUtils.isEmpty(supportedViews)) {
-            views = Collections.<String>emptyList();
-        } else if (supportedViews.contains("unix")) {
-            views = DEFAULT_UNIX_VIEW;
-        } else {
-            views = new ArrayList<String>(supportedViews.size());
-            for (String v : supportedViews) {
-                views.add(v + ":*");
-            }
-        }
-
-        for (String v : views) {
-            Map<String, Object> ta=readFileAttributes(file, v, opts);
-            attrs.putAll(ta);
-        }
-
-        // if did not get permissions from the supported views return a best approximation
-        if (!attrs.containsKey("permissions")) {
-            Set<PosixFilePermission> perms=IoUtils.getPermissionsFromFile(file.toFile());
-            attrs.put("permissions", perms);
-        }
-
-        return attrs;
-    }
-
-    protected Map<String, Object> readFileAttributes(Path file, String view, LinkOption ... opts) throws IOException {
-        try {
-            return Files.readAttributes(file, view, opts);
-        } catch(IOException e) {
-            return handleReadFileAttributesException(file, view, opts, e);
-        }
-    }
-
-    protected Map<String, Object> handleReadFileAttributesException(Path file, String view, LinkOption[] opts, IOException e) throws IOException {
-        switch(unsupportedAttributePolicy) {
-            case Ignore:
-                break;
-            case Warn:
-                log.warn("handleReadFileAttributesException(" + file + ")[" + view + "] " + e.getClass().getSimpleName() + ": " + e.getMessage());
-                break;
-            case ThrowException:
-                throw e;
-            default:
-                log.warn("handleReadFileAttributesException(" + file + ")[" + view + "]"
-                       + " Unknown policy (" + unsupportedAttributePolicy + ")"
-                       + " for " + e.getClass().getSimpleName() + ": " + e.getMessage());
-        }
-        
-        return Collections.emptyMap();
-    }
-
-    protected void setAttributes(Path file, Map<String, Object>  attributes) throws IOException {
-        Set<String> unsupported = new HashSet<>();
-        for (String attribute : attributes.keySet()) {
-            String view = null;
-            Object value = attributes.get(attribute);
-            switch (attribute) {
-            case "size": {
-                long newSize = ((Number) value).longValue();
-                try (FileChannel channel = FileChannel.open(file, StandardOpenOption.WRITE)) {
-                    channel.truncate(newSize);
-                }
-                continue;
-            }
-            case "uid":
-                view = "unix";
-                break;
-            case "gid":
-                view = "unix";
-                break;
-            case "owner":
-                view = "posix";
-                value = toUser(file, (UserPrincipal) value);
-                break;
-            case "group":
-                view = "posix";
-                value = toGroup(file, (GroupPrincipal) value);
-                break;
-            case "permissions":
-                if (OsUtils.isWin32()) {
-                    @SuppressWarnings("unchecked")
-                    Collection<PosixFilePermission> perms = (Collection<PosixFilePermission>) value;
-                    IoUtils.setPermissionsToFile(file.toFile(), perms);
-                    continue;
-                }
-                view = "posix";
-                break;
-
-            case "creationTime":
-                view = "basic";
-                break;
-            case "lastModifiedTime":
-                view = "basic";
-                break;
-            case "lastAccessTime":
-                view = "basic";
-                break;
-            default:    // ignored
-            }
-            if (view != null && value != null) {
-                try {
-                    Files.setAttribute(file, view + ":" + attribute, value, IoUtils.getLinkOptions(false));
-                } catch (UnsupportedOperationException e) {
-                    unsupported.add(attribute);
-                }
-            }
-        }
-        handleUnsupportedAttributes(unsupported);
-    }
-
-    protected void handleUnsupportedAttributes(Collection<String> attributes) {
-        if (!attributes.isEmpty()) {
-            StringBuilder sb = new StringBuilder();
-            for (String attr : attributes) {
-                if (sb.length() > 0) {
-                    sb.append(", ");
-                }
-                sb.append(attr);
-            }
-            switch (unsupportedAttributePolicy) {
-                case Ignore:
-                    break;
-                case Warn:
-                    log.warn("Unsupported attributes: " + sb.toString());
-                    break;
-                case ThrowException:
-                    throw new UnsupportedOperationException("Unsupported attributes: " + sb.toString());
-                default:
-                    log.warn("Unknown policy for attributes=" + sb.toString() + ": " + unsupportedAttributePolicy);
-            }
-        }
-    }
-
-    private GroupPrincipal toGroup(Path file, GroupPrincipal name) throws IOException {
-        String groupName = name.toString();
-        FileSystem fileSystem = file.getFileSystem();
-        UserPrincipalLookupService lookupService = fileSystem.getUserPrincipalLookupService();
-        try {
-            return lookupService.lookupPrincipalByGroupName(groupName);
-        } catch (IOException e) {
-            handleUserPrincipalLookupServiceException(GroupPrincipal.class, groupName, e);
-            return null;
-        }
-    }
-
-    private UserPrincipal toUser(Path file, UserPrincipal name) throws IOException {
-        String username = name.toString();
-        FileSystem fileSystem = file.getFileSystem();
-        UserPrincipalLookupService lookupService = fileSystem.getUserPrincipalLookupService();
-        try {
-            return lookupService.lookupPrincipalByName(username);
-        } catch (IOException e) {
-            handleUserPrincipalLookupServiceException(UserPrincipal.class, username, e);
-            return null;
-        }
-    }
-
-    protected void handleUserPrincipalLookupServiceException(Class<? extends Principal> principalType, String name, IOException e) throws IOException {
-        /* According to Javadoc:
-         * 
-         *      "Where an implementation does not support any notion of group
-         *      or user then this method always throws UserPrincipalNotFoundException."
-         */
-        switch (unsupportedAttributePolicy) {
-            case Ignore:
-                break;
-            case Warn:
-                log.warn("handleUserPrincipalLookupServiceException(" + principalType.getSimpleName() + "[" + name + "])"
-                       + " failed (" + e.getClass().getSimpleName() + "): " + e.getMessage());
-                break;
-            case ThrowException:
-                throw e;
-            default:
-                log.warn("Unknown policy for principal=" + principalType.getSimpleName() + "[" + name + "]: " + unsupportedAttributePolicy);
-        }
-    }
-
-    private Set<PosixFilePermission> permissionsToAttributes(int perms) {
-        Set<PosixFilePermission> p = new HashSet<>();
-        if ((perms & S_IRUSR) != 0) {
-            p.add(PosixFilePermission.OWNER_READ);
-        }
-        if ((perms & S_IWUSR) != 0) {
-            p.add(PosixFilePermission.OWNER_WRITE);
-        }
-        if ((perms & S_IXUSR) != 0) {
-            p.add(PosixFilePermission.OWNER_EXECUTE);
-        }
-        if ((perms & S_IRGRP) != 0) {
-            p.add(PosixFilePermission.GROUP_READ);
-        }
-        if ((perms & S_IWGRP) != 0) {
-            p.add(PosixFilePermission.GROUP_WRITE);
-        }
-        if ((perms & S_IXGRP) != 0) {
-            p.add(PosixFilePermission.GROUP_EXECUTE);
-        }
-        if ((perms & S_IROTH) != 0) {
-            p.add(PosixFilePermission.OTHERS_READ);
-        }
-        if ((perms & S_IWOTH) != 0) {
-            p.add(PosixFilePermission.OTHERS_WRITE);
-        }
-        if ((perms & S_IXOTH) != 0) {
-            p.add(PosixFilePermission.OTHERS_EXECUTE);
-        }
-        return p;
-    }
-
-    protected Map<String, Object> readAttrs(Buffer buffer) throws IOException {
-        Map<String, Object> attrs = new HashMap<>();
-        int flags = buffer.getInt();
-        if (version >= SFTP_V4) {
-            byte type = buffer.getByte();
-            switch (type) {
-            case SSH_FILEXFER_TYPE_REGULAR:
-                attrs.put("isRegular", Boolean.TRUE);
-                break;
-            case SSH_FILEXFER_TYPE_DIRECTORY:
-                attrs.put("isDirectory", Boolean.TRUE);
-                break;
-            case SSH_FILEXFER_TYPE_SYMLINK:
-                attrs.put("isSymbolicLink", Boolean.TRUE);
-                break;
-            case SSH_FILEXFER_TYPE_UNKNOWN:
-                attrs.put("isOther", Boolean.TRUE);
-                break;
-            default:    // ignored
-            }
-        }
-        if ((flags & SSH_FILEXFER_ATTR_SIZE) != 0) {
-            attrs.put("size", Long.valueOf(buffer.getLong()));
-        }
-        if ((flags & SSH_FILEXFER_ATTR_ALLOCATION_SIZE) != 0) {
-            attrs.put("allocationSize", Long.valueOf(buffer.getLong()));
-        }
-        if ((flags & SSH_FILEXFER_ATTR_UIDGID) != 0) {
-            attrs.put("uid", Integer.valueOf(buffer.getInt()));
-            attrs.put("gid", Integer.valueOf(buffer.getInt()));
-        }
-        if ((flags & SSH_FILEXFER_ATTR_OWNERGROUP) != 0) {
-            attrs.put("owner", new DefaultGroupPrincipal(buffer.getString()));
-            attrs.put("group", new DefaultGroupPrincipal(buffer.getString()));
-        }
-        if ((flags & SSH_FILEXFER_ATTR_PERMISSIONS) != 0) {
-            attrs.put("permissions", permissionsToAttributes(buffer.getInt()));
-        }
-        if (version == SFTP_V3) {
-            if ((flags & SSH_FILEXFER_ATTR_ACMODTIME) != 0) {
-                attrs.put("lastAccessTime", readTime(buffer, flags));
-                attrs.put("lastModifiedTime", readTime(buffer, flags));
-            }
-        } else if (version >= SFTP_V4) {
-            if ((flags & SSH_FILEXFER_ATTR_ACCESSTIME) != 0) {
-                attrs.put("lastAccessTime", readTime(buffer, flags));
-            }
-            if ((flags & SSH_FILEXFER_ATTR_CREATETIME) != 0) {
-                attrs.put("creationTime", readTime(buffer, flags));
-            }
-            if ((flags & SSH_FILEXFER_ATTR_MODIFYTIME) != 0) {
-                attrs.put("lastModifiedTime", readTime(buffer, flags));
-            }
-            if ((flags & SSH_FILEXFER_ATTR_CTIME) != 0) {
-                attrs.put("ctime", readTime(buffer, flags));
-            }
-        }
-        if ((flags & SSH_FILEXFER_ATTR_ACL) != 0) {
-            int count = buffer.getInt();
-            List<AclEntry> acls = new ArrayList<>();
-            for (int i = 0; i < count; i++) {
-                int aclType = buffer.getInt();
-                int aclFlag = buffer.getInt();
-                int aclMask = buffer.getInt();
-                String aclWho = buffer.getString();
-                acls.add(buildAclEntry(aclType, aclFlag, aclMask, aclWho));
-            }
-            attrs.put("acl", acls);
-        }
-        if ((flags & SSH_FILEXFER_ATTR_BITS) != 0) {
-            int bits = buffer.getInt();
-            int valid = 0xffffffff;
-            if (version >= SFTP_V6) {
-                valid = buffer.getInt();
-            }
-            // TODO: handle attrib bits
-        }
-        if ((flags & SSH_FILEXFER_ATTR_TEXT_HINT) != 0) {
-            boolean text = buffer.getBoolean();
-            // TODO: handle text
-        }
-        if ((flags & SSH_FILEXFER_ATTR_MIME_TYPE) != 0) {
-            String mimeType = buffer.getString();
-            // TODO: handle mime-type
-        }
-        if ((flags & SSH_FILEXFER_ATTR_LINK_COUNT) != 0) {
-            int nlink = buffer.getInt();
-            // TODO: handle link-count
-        }
-        if ((flags & SSH_FILEXFER_ATTR_UNTRANSLATED_NAME) != 0) {
-            String untranslated = buffer.getString();
-            // TODO: handle untranslated-name
-        }
-        if ((flags & SSH_FILEXFER_ATTR_EXTENDED) != 0) {
-            int count = buffer.getInt();
-            Map<String, String> extended = new HashMap<>();
-            for (int i = 0; i < count; i++) {
-                String key = buffer.getString();
-                String val = buffer.getString();
-                extended.put(key, val);
-            }
-            attrs.put("extended", extended);
-        }
-
-        return attrs;
-    }
-
-    private FileTime readTime(Buffer buffer, int flags) {
-        long secs = buffer.getLong();
-        long millis = secs * 1000;
-        if ((flags & SSH_FILEXFER_ATTR_SUBSECOND_TIMES) != 0) {
-            millis += buffer.getInt() / 1000000l;
-        }
-        return FileTime.from(millis, TimeUnit.MILLISECONDS);
-    }
-
-    private AclEntry buildAclEntry(int aclType, int aclFlag, int aclMask, final String aclWho) {
-        AclEntryType type;
-        switch (aclType) {
-        case ACE4_ACCESS_ALLOWED_ACE_TYPE:
-            type = AclEntryType.ALLOW;
-            break;
-        case ACE4_ACCESS_DENIED_ACE_TYPE:
-            type = AclEntryType.DENY;
-            break;
-        case ACE4_SYSTEM_AUDIT_ACE_TYPE:
-            type = AclEntryType.AUDIT;
-            break;
-        case ACE4_SYSTEM_ALARM_ACE_TYPE:
-            type = AclEntryType.AUDIT;
-            break;
-        default:
-            throw new IllegalStateException("Unknown acl type: " + aclType);
-        }
-        Set<AclEntryFlag> flags = new HashSet<>();
-        if ((aclFlag & ACE4_FILE_INHERIT_ACE) != 0) {
-            flags.add(AclEntryFlag.FILE_INHERIT);
-        }
-        if ((aclFlag & ACE4_DIRECTORY_INHERIT_ACE) != 0) {
-            flags.add(AclEntryFlag.DIRECTORY_INHERIT);
-        }
-        if ((aclFlag & ACE4_NO_PROPAGATE_INHERIT_ACE) != 0) {
-            flags.add(AclEntryFlag.NO_PROPAGATE_INHERIT);
-        }
-        if ((aclFlag & ACE4_INHERIT_ONLY_ACE) != 0) {
-            flags.add(AclEntryFlag.INHERIT_ONLY);
-        }
-        Set<AclEntryPermission> mask = new HashSet<>();
-        if ((aclMask & ACE4_READ_DATA) != 0) {
-            mask.add(AclEntryPermission.READ_DATA);
-        }
-        if ((aclMask & ACE4_LIST_DIRECTORY) != 0) {
-            mask.add(AclEntryPermission.LIST_DIRECTORY);
-        }
-        if ((aclMask & ACE4_WRITE_DATA) != 0) {
-            mask.add(AclEntryPermission.WRITE_DATA);
-        }
-        if ((aclMask & ACE4_ADD_FILE) != 0) {
-            mask.add(AclEntryPermission.ADD_FILE);
-        }
-        if ((aclMask & ACE4_APPEND_DATA) != 0) {
-            mask.add(AclEntryPermission.APPEND_DATA);
-        }
-        if ((aclMask & ACE4_ADD_SUBDIRECTORY) != 0) {
-            mask.add(AclEntryPermission.ADD_SUBDIRECTORY);
-        }
-        if ((aclMask & ACE4_READ_NAMED_ATTRS) != 0) {
-            mask.add(AclEntryPermission.READ_NAMED_ATTRS);
-        }
-        if ((aclMask & ACE4_WRITE_NAMED_ATTRS) != 0) {
-            mask.add(AclEntryPermission.WRITE_NAMED_ATTRS);
-        }
-        if ((aclMask & ACE4_EXECUTE) != 0) {
-            mask.add(AclEntryPermission.EXECUTE);
-        }
-        if ((aclMask & ACE4_DELETE_CHILD) != 0) {
-            mask.add(AclEntryPermission.DELETE_CHILD);
-        }
-        if ((aclMask & ACE4_READ_ATTRIBUTES) != 0) {
-            mask.add(AclEntryPermission.READ_ATTRIBUTES);
-        }
-        if ((aclMask & ACE4_WRITE_ATTRIBUTES) != 0) {
-            mask.add(AclEntryPermission.WRITE_ATTRIBUTES);
-        }
-        if ((aclMask & ACE4_DELETE) != 0) {
-            mask.add(AclEntryPermission.DELETE);
-        }
-        if ((aclMask & ACE4_READ_ACL) != 0) {
-            mask.add(AclEntryPermission.READ_ACL);
-        }
-        if ((aclMask & ACE4_WRITE_ACL) != 0) {
-            mask.add(AclEntryPermission.WRITE_ACL);
-        }
-        if ((aclMask & ACE4_WRITE_OWNER) != 0) {
-            mask.add(AclEntryPermission.WRITE_OWNER);
-        }
-        if ((aclMask & ACE4_SYNCHRONIZE) != 0) {
-            mask.add(AclEntryPermission.SYNCHRONIZE);
-        }
-        UserPrincipal who = new DefaultGroupPrincipal(aclWho);
-        return AclEntry.newBuilder()
-                .setType(type)
-                .setFlags(flags)
-                .setPermissions(mask)
-                .setPrincipal(who)
-                .build();
-    }
-
-    protected void sendStatus(int id, Exception e) throws IOException {
-        int substatus;
-        if (e instanceof NoSuchFileException || e instanceof FileNotFoundException) {
-            substatus = SSH_FX_NO_SUCH_FILE;
-        } else if (e instanceof FileAlreadyExistsException) {
-            substatus = SSH_FX_FILE_ALREADY_EXISTS;
-        } else if (e instanceof DirectoryNotEmptyException) {
-            substatus = SSH_FX_DIR_NOT_EMPTY;
-        } else if (e instanceof AccessDeniedException) {
-            substatus = SSH_FX_PERMISSION_DENIED;
-        } else if (e instanceof OverlappingFileLockException) {
-            substatus = SSH_FX_LOCK_CONFLICT;
-        } else {
-            substatus = SSH_FX_FAILURE;
-        }
-        sendStatus(id, substatus, e.toString());
-    }
-
-    protected void sendStatus(int id, int substatus, String msg) throws IOException {
-        sendStatus(id, substatus, msg != null ? msg : "", "");
-    }
-
-    protected void sendStatus(int id, int substatus, String msg, String lang) throws IOException {
-        if (log.isDebugEnabled()) {
-            log.debug("Send SSH_FXP_STATUS (substatus={}, lang={}, msg={})",
-                      new Object[] { Integer.valueOf(substatus), lang, msg });
-        }
-
-        Buffer buffer = new ByteArrayBuffer();
-        buffer.putByte((byte) SSH_FXP_STATUS);
-        buffer.putInt(id);
-        buffer.putInt(substatus);
-        buffer.putString(msg);
-        buffer.putString(lang);
-        send(buffer);
-    }
-
-    protected void send(Buffer buffer) throws IOException {
-        DataOutputStream dos = new DataOutputStream(out);
-        dos.writeInt(buffer.available());
-        dos.write(buffer.array(), buffer.rpos(), buffer.available());
-        dos.flush();
-    }
-
-    @Override
-    public void destroy() {
-        if (!closed) {
-            if (log.isDebugEnabled()) {
-                log.debug("destroy() - mark as closed");
-            }
-
-            closed = true;
-
-            // if thread has not completed, cancel it
-            if ((pendingFuture != null) && (!pendingFuture.isDone())) {
-                boolean result = pendingFuture.cancel(true);
-                // TODO consider waiting some reasonable (?) amount of time for cancellation
-                if (log.isDebugEnabled()) {
-                    log.debug("destroy() - cancel pending future=" + result);
-                }
-            }
-
-            pendingFuture = null;
-
-            if ((executors != null) && (!executors.isShutdown()) && shutdownExecutor) {
-                Collection<Runnable> runners = executors.shutdownNow();
-                if (log.isDebugEnabled()) {
-                    log.debug("destroy() - shutdown executor service - runners count=" + ((runners == null) ? 0 : runners.size()));
-                }
-            }
-
-            executors = null;
-
-            try {
-                fileSystem.close();
-            } catch (UnsupportedOperationException e) {
-                // Ignore
-            } catch (IOException e) {
-                log.debug("Error closing FileSystem", e);
-            }
-        }
-    }
-
-    private Path resolveFile(String path) {
-        //in case we are running on Windows
-        String localPath = SelectorUtils.translateToLocalPath(path);
-        return defaultDir.resolve(localPath);
-    }
-
-    private final static String[] MONTHS = { "Jan", "Feb", "Mar", "Apr", "May",
-            "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
-
-    /**
-     * Get unix style date string.
-     */
-    private static String getUnixDate(FileTime time) {
-        return getUnixDate(time != null ? time.toMillis() : -1);
-    }
-
-    private static String getUnixDate(long millis) {
-        if (millis < 0) {
-            return "------------";
-        }
-
-        StringBuilder sb = new StringBuilder(16);
-        Calendar cal = new GregorianCalendar();
-        cal.setTimeInMillis(millis);
-
-        // month
-        sb.append(MONTHS[cal.get(Calendar.MONTH)]);
-        sb.append(' ');
-
-        // day
-        int day = cal.get(Calendar.DATE);
-        if (day < 10) {
-            sb.append(' ');
-        }
-        sb.append(day);
-        sb.append(' ');
-
-        long sixMonth = 15811200000L; // 183L * 24L * 60L * 60L * 1000L;
-        long nowTime = System.currentTimeMillis();
-        if (Math.abs(nowTime - millis) > sixMonth) {
-
-            // year
-            int year = cal.get(Calendar.YEAR);
-            sb.append(' ');
-            sb.append(year);
-        } else {
-
-            // hour
-            int hh = cal.get(Calendar.HOUR_OF_DAY);
-            if (hh < 10) {
-                sb.append('0');
-            }
-            sb.append(hh);
-            sb.append(':');
-
-            // minute
-            int mm = cal.get(Calendar.MINUTE);
-            if (mm < 10) {
-                sb.append('0');
-            }
-            sb.append(mm);
-        }
-        return sb.toString();
-    }
-
-    protected static class PrincipalBase implements Principal {
-        private final String name;
-
-        public PrincipalBase(String name) {
-            if (name == null) {
-                throw new IllegalArgumentException("name is null");
-            }
-            this.name = name;
-        }
-
-        @Override
-        public final String getName() {
-            return name;
-        }
-
-        @Override
-        public boolean equals(Object o) {
-            if (this == o) {
-                return true;
-            }
-            if ((o == null) || (getClass() != o.getClass())) {
-                return false;
-            }
-
-            Principal that = (Principal) o;
-            if (Objects.equals(getName(),that.getName())) {
-                return true;
-            } else {
-                return false;    // debug breakpoint
-            }
-        }
-
-        @Override
-        public int hashCode() {
-            return Objects.hashCode(getName());
-        }
-
-        @Override
-        public String toString() {
-            return getName();
-        }
-    }
-
-    protected static class DefaultUserPrincipal extends PrincipalBase implements UserPrincipal {
-        public DefaultUserPrincipal(String name) {
-            super(name);
-        }
-    }
-
-    protected static class DefaultGroupPrincipal extends PrincipalBase implements GroupPrincipal {
-        public DefaultGroupPrincipal(String name) {
-            super(name);
-        }
-    }
-}