http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/a6e2bf9e/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java
----------------------------------------------------------------------
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java
 
b/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java
new file mode 100644
index 0000000..c6baa5c
--- /dev/null
+++ 
b/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java
@@ -0,0 +1,2280 @@
+/*
+ * 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.subsystem.sftp;
+
+import static org.apache.sshd.common.subsystem.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:[email protected]";>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);
+        }
+    }
+}

Reply via email to