/*
 * 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.sftp.subsystem;

import org.apache.sshd.common.NamedFactory;
import org.apache.sshd.common.Session;
import org.apache.sshd.common.file.FileSystemAware;
import org.apache.sshd.common.file.FileSystemView;
import org.apache.sshd.common.file.SshFile;
import org.apache.sshd.common.util.Buffer;
import org.apache.sshd.common.util.SelectorUtils;
import org.apache.sshd.common.util.ThreadUtils;
import org.apache.sshd.server.*;
import org.apache.sshd.server.channel.ChannelDataReceiver;
import org.apache.sshd.server.channel.ChannelSession;
import org.apache.sshd.server.session.ServerSession;
import org.apache.sshd.sftp.*;
import org.apache.sshd.sftp.reply.*;
import org.apache.sshd.sftp.request.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import static org.apache.sshd.sftp.subsystem.SftpConstants.*;

/**
 * SFTP subsystem
 *
 * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
 */
public class SftpSubsystem implements Command, SessionAware, FileSystemAware, SftpSession, ChannelDataReceiver, ChannelSessionAware {

    protected static final Logger LOG = LoggerFactory.getLogger(SftpSubsystem.class);

    public static class Factory implements NamedFactory<Command> {

        public Factory() {
        }

        public Command create() {
            return new SftpSubsystem();
        }

        public String getName() {
            return "sftp";
        }
    }

    public static final int LOWER_SFTP_IMPL = 3; // Working implementation from v3
    public static final int HIGHER_SFTP_IMPL = 6; //  .. up to
    public static final String ALL_SFTP_IMPL = "3,4,5,6";
    public static final int  MAX_PACKET_LENGTH = 1024 * 16;

    /**
     * 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";


    private ExitCallback callback;
    private InputStream in;
    private OutputStream out;
    private OutputStream err;
    private Environment env;
    private ServerSession session;
    private ChannelSession channel;
    private boolean closed = false;

    private FileSystemView root;

    private int version;
    private Map<String, Handle> handles = new HashMap<String, Handle>();

    private Sftplet sftpLet = new DefaultSftpletContainer();
    private Serializer serializer = new Serializer(this);

    private final ExecutorService executor;

    public SftpSubsystem() {
        executor = ThreadUtils.newSingleThreadExecutor("sftp[" + Integer.toHexString(hashCode()) + "]");
    }

    public void setSftpLet(final Sftplet sftpLet) {
        this.sftpLet = sftpLet;
    }

    public int getVersion() {
        return version;
    }

    public Session getSession() {
        return session;
    }

    public Handle getHandle(String id) {
        return handles.get(id);
    }

    public Handle createFileHandle(SshFile file, int flags) {
        String id = UUID.randomUUID().toString();
        Handle handle = new FileHandle(id,  file, flags);
        handles.put(id, handle);
        return handle;
    }

    public Handle createDirectoryHandle(SshFile file) {
        String id = UUID.randomUUID().toString();
        Handle handle = new DirectoryHandle(id, file);
        handles.put(id, handle);
        return handle;
    }

    public void setChannelSession(ChannelSession channel) {
        this.channel = channel;
        channel.setDataReceiver(this);
    }

    public void setSession(ServerSession session) {
        this.session = session;
        sftpLet.onConnect(this);
    }

    public void setFileSystemView(FileSystemView view) {
        this.root = view;
    }

    public void setExitCallback(ExitCallback callback) {
        this.callback = callback;
    }

    public void setInputStream(InputStream in) {
        this.in = in;
    }

    public void setOutputStream(OutputStream out) {
        this.out = out;
    }

    public void setErrorStream(OutputStream err) {
        this.err = err;
    }

    public void start(Environment env) throws IOException {
        this.env = env;
    }

    private Buffer buffer = new Buffer();

    public int data(ChannelSession channel, byte[] buf, int start, int len) throws IOException {
        Buffer incoming = new Buffer(buf,  start, len);
        // If we already have partial data, we need to append it to the buffer and use it
        if (buffer.available() > 0) {
            buffer.putBuffer(incoming);
            incoming = buffer;
        }
        // Process commands
        int rpos = incoming.rpos();
        while (receive(incoming));
        int read = incoming.rpos() - rpos;
        // Compact and add remaining data
        buffer.compact();
        if (buffer != incoming && incoming.available() > 0) {
            buffer.putBuffer(incoming);
        }
        return read;
    }

    protected boolean receive(Buffer incoming) throws IOException {
        int rpos = incoming.rpos();
        int wpos = incoming.wpos();
        if (wpos - rpos > 4) {
            int length = incoming.getInt();
            if (length < 5) {
                throw new IOException("Illegal sftp packet length: " + length);
            }
            if (wpos - rpos >= length + 4) {
                incoming.rpos(rpos);
                incoming.wpos(rpos + 4 + length);
                process(incoming);
                incoming.rpos(rpos + 4 + length);
                incoming.wpos(wpos);
                return true;
            }
        }
        incoming.rpos(rpos);
        return false;
    }

    public void close() throws IOException {
        executor.shutdownNow();
        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);
        sftpLet.onDisconnect(this);
    }

    public void process(Buffer buffer) throws IOException {
        final Request request = serializer.readRequest(buffer);
        if (LOG.isDebugEnabled()) {
            LOG.debug("Received sftp request: " + request);
        }
        executor.execute(new Runnable() {
            public void run() {
                try {
                    Reply reply = sftpLet.beforeCommand(SftpSubsystem.this, request);
                    if (reply == null) {
                        reply = doProcess(request);
                    }
                    reply = sftpLet.afterCommand(SftpSubsystem.this, request, reply);
                    if (reply != null) {
                        if (LOG.isDebugEnabled()) {
                            LOG.debug("Sending sftp reply: " + reply);
                        }
                        Buffer buffer = serializer.writeReply(reply);
                        send(buffer);
                    }
                } catch (Throwable t) {
                    // TODO do something
                    t.printStackTrace();
                }
            }
        });
    }

    protected Reply doProcess(Request request) throws IOException {
        try {
            if (request instanceof SshFxpInitRequest) {
                return doProcessInit((SshFxpInitRequest) request);
            } else if (request instanceof SshFxpOpenRequest) {
                return doProcessOpen((SshFxpOpenRequest) request);
            } else if (request instanceof SshFxpCloseRequest) {
                return doProcessClose((SshFxpCloseRequest) request);
            } else if (request instanceof SshFxpReadRequest) {
                return doProcessRead((SshFxpReadRequest) request);
            } else if (request instanceof SshFxpWriteRequest) {
                return doProcessWrite((SshFxpWriteRequest) request);
            } else if ((request instanceof SshFxpLstatRequest)
                    || (request instanceof SshFxpStatRequest)) {
                return doProcessStat(request);
            } else if (request instanceof SshFxpFstatRequest) {
                return doProcessFstat((SshFxpFstatRequest) request);
            } else if (request instanceof SshFxpOpendirRequest) {
                return doProcessOpendir((SshFxpOpendirRequest) request);
            } else if (request instanceof SshFxpReaddirRequest) {
                return doProcessReaddir((SshFxpReaddirRequest) request);
            } else if (request instanceof SshFxpRemoveRequest) {
                return doProcessRemove((SshFxpRemoveRequest) request);
            } else if (request instanceof SshFxpMkdirRequest) {
                return doProcessMkdir((SshFxpMkdirRequest) request);
            } else if (request instanceof SshFxpRmdirRequest) {
                return doProcessRmdir((SshFxpRmdirRequest) request);
            } else if (request instanceof SshFxpRealpathRequest) {
                return doProcessRealpath((SshFxpRealpathRequest) request);
            } else if (request instanceof SshFxpRenameRequest) {
                return doProcessRename((SshFxpRenameRequest) request);
            } else if ((request instanceof SshFxpSetstatRequest)
                    || (request instanceof SshFxpFsetstatRequest)) {
                return doProcessSetstat(request);

            } else {
                LOG.error("Received: {}", request);
                int id = request.getId();
                return new SshFxpStatusReply(id, SSH_FX_OP_UNSUPPORTED, "Command " + request + " is unsupported or not implemented");
            }
        } catch (IOException e) {
            int id = request.getId();
            return new SshFxpStatusReply(id, SSH_FX_FAILURE, e.getMessage());
        }
    }

    private Reply doProcessSetstat(Request request) throws IOException {
        // This is required for WinSCP / Cyberduck to upload properly
        // Blindly reply "OK"
        // TODO implement it
        int id = request.getId();
        return new SshFxpStatusReply(id, SSH_FX_OK, "");
    }

    private Reply doProcessRename(SshFxpRenameRequest request) throws IOException {
        int id = request.getId();
        String oldPath = request.getOldPath();
        String newPath = request.getNewPath();
        SshFile o = resolveFile(oldPath);
        SshFile n = resolveFile(newPath);
        if (!o.doesExist()) {
            return new SshFxpStatusReply(id, SSH_FX_NO_SUCH_FILE, o.getAbsolutePath());
        } else if (n.doesExist()) {
            return new SshFxpStatusReply(id, SSH_FX_FILE_ALREADY_EXISTS, n.getAbsolutePath());
        } else if (!o.move(n)) {
            return new SshFxpStatusReply(id, SSH_FX_FAILURE, "Failed to rename file");
        } else {
            return new SshFxpStatusReply(id, SSH_FX_OK, "");
        }
    }

    private Reply doProcessRealpath(SshFxpRealpathRequest request) throws IOException {
        int id = request.getId();
        String path = request.getPath();
        if (path.trim().length() == 0) {
            path = ".";
        }
        SshFile p = resolveFile(path);
        for (String s : request.getCompose()) {
            p = this.root.getFile(p, s);
        }
        String normalizedPath = SelectorUtils.normalizePath(p.getAbsolutePath(), "/");
        if (normalizedPath.length() == 0) {
            normalizedPath = "/";
        }
        p = resolveFile(normalizedPath);
        if (p.getName().length() == 0) {
            p = resolveFile(".");
        }
        boolean exists = (request.getOptions() != SSH_FXP_REALPATH_NO_CHECK) && p.doesExist();
        if (!exists && request.getOptions() == SSH_FXP_REALPATH_STAT_ALWAYS) {
            return new SshFxpStatusReply(id, SSH_FX_NO_SUCH_FILE, p.getAbsolutePath());
        } else if (exists && (request.getOptions() == SSH_FXP_REALPATH_STAT_IF || request.getOptions() == SSH_FXP_REALPATH_STAT_ALWAYS)) {
            SshFxpNameReply reply = new SshFxpNameReply(id);
            int flags = SSH_FILEXFER_ATTR_PERMISSIONS | SSH_FILEXFER_ATTR_SIZE;
            reply.addFile(p, normalizedPath, getLongName(p), new FileAttributes(p, flags));
            return reply;
        } else {
            SshFxpNameReply reply = new SshFxpNameReply(id);
            reply.addFile(p, normalizedPath, getLongName(p), new FileAttributes());
            return reply;
        }
    }

    private Reply doProcessRmdir(SshFxpRmdirRequest request) throws IOException {
        int id = request.getId();
        String path = request.getPath();
        // attrs
        SshFile p = resolveFile(path);
        if (p.isDirectory()) {
            if (p.doesExist()) {
                if (p.listSshFiles().size() == 0) {
                    if (p.delete()) {
                        return new SshFxpStatusReply(id, SSH_FX_OK, "");
                    } else {
                        return new SshFxpStatusReply(id, SSH_FX_FAILURE, "Unable to delete directory " + path);
                    }
                } else {
                    return new SshFxpStatusReply(id, SSH_FX_DIR_NOT_EMPTY, path);
                }
            } else {
                return new SshFxpStatusReply(id, SSH_FX_NO_SUCH_PATH, path);
            }
        } else {
            return new SshFxpStatusReply(id, SSH_FX_NOT_A_DIRECTORY, p.getAbsolutePath());
        }
    }

    private Reply doProcessMkdir(SshFxpMkdirRequest request) throws IOException {
        int id = request.getId();
        String path = request.getPath();
        // attrs
        SshFile p = resolveFile(path);
        if (p.doesExist()) {
            if (p.isDirectory()) {
                return new SshFxpStatusReply(id, SSH_FX_FILE_ALREADY_EXISTS, p.getAbsolutePath());
            } else {
                return new SshFxpStatusReply(id, SSH_FX_NOT_A_DIRECTORY, p.getAbsolutePath());
            }
        } else if (!p.isWritable()) {
            return new SshFxpStatusReply(id, SSH_FX_PERMISSION_DENIED, p.getAbsolutePath());
        } else if (!p.mkdir()) {
            return new SshFxpStatusReply(id, SSH_FX_FAILURE, "Error creating dir " + path);
        } else {
            return new SshFxpStatusReply(id, SSH_FX_OK, "");
        }
    }

    private Reply doProcessRemove(SshFxpRemoveRequest request) throws IOException {
        int id = request.getId();
        String path = request.getPath();
        SshFile p = resolveFile(path);
        if (!p.doesExist()) {
            return new SshFxpStatusReply(id, SSH_FX_NO_SUCH_FILE, p.getAbsolutePath());
        } else if (p.isDirectory()) {
            return new SshFxpStatusReply(id, SSH_FX_FILE_IS_A_DIRECTORY, p.getAbsolutePath());
        } else if (!p.delete()) {
            return new SshFxpStatusReply(id, SSH_FX_FAILURE, "Failed to delete file");
        } else {
            return new SshFxpStatusReply(id, SSH_FX_OK, "");
        }
    }

    private Reply doProcessReaddir(SshFxpReaddirRequest request) throws IOException {
        int id = request.getId();
        String handle = request.getHandleId();
        Handle p = getHandle(handle);
        if (!(p instanceof DirectoryHandle)) {
            return new SshFxpStatusReply(id, SSH_FX_INVALID_HANDLE, handle);
        } else if (((DirectoryHandle) p).isDone()) {
            return new SshFxpStatusReply(id, SSH_FX_EOF, "", "");
        } else if (!p.getFile().doesExist()) {
            return new SshFxpStatusReply(id, SSH_FX_NO_SUCH_FILE, p.getFile().getAbsolutePath());
        } else if (!p.getFile().isDirectory()) {
            return new SshFxpStatusReply(id, SSH_FX_NOT_A_DIRECTORY, p.getFile().getAbsolutePath());
        } else if (!p.getFile().isReadable()) {
            return new SshFxpStatusReply(id, SSH_FX_PERMISSION_DENIED, p.getFile().getAbsolutePath());
        } 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.
                Reply reply = sendName(id, dh);
                if (!dh.hasNext()) {
                    // if no more files to send
                    dh.setDone(true);
                    dh.clearFileList();
                }
                return reply;
            } else {
                // empty directory
                dh.setDone(true);
                dh.clearFileList();
                return new SshFxpStatusReply(id, SSH_FX_EOF, "", "");
            }
        }
    }

    private Reply doProcessOpendir(SshFxpOpendirRequest request) throws IOException {
        int id = request.getId();
        String path = request.getPath();
        SshFile p = resolveFile(path);
        if (!p.doesExist()) {
            return new SshFxpStatusReply(id, SSH_FX_NO_SUCH_FILE, path);
        } else if (!p.isDirectory()) {
            return new SshFxpStatusReply(id, SSH_FX_NOT_A_DIRECTORY, path);
        } else if (!p.isReadable()) {
            return new SshFxpStatusReply(id, SSH_FX_PERMISSION_DENIED, path);
        } else {
            Handle handle = createDirectoryHandle(p);
            return new SshFxpHandleReply(id, handle);
        }
    }

    private Reply doProcessFstat(SshFxpFstatRequest request) throws IOException {
        int id = request.getId();
        String handle = request.getHandleId();
        Handle p = getHandle(handle);
        if (p == null) {
            return new SshFxpStatusReply(id, SSH_FX_INVALID_HANDLE, handle);
        } else {
            int flags = SSH_FILEXFER_ATTR_PERMISSIONS | SSH_FILEXFER_ATTR_SIZE;
            return new SshFxpAttrsReply(id, new FileAttributes(p.getFile(), flags));
        }
    }

    private Reply doProcessStat(Request sftpRequest) throws IOException {
        int id = sftpRequest.getId();
        String path;
        if (sftpRequest instanceof SshFxpLstatRequest) {
            SshFxpLstatRequest sshFxpLstatRequest = (SshFxpLstatRequest) sftpRequest;
            path = sshFxpLstatRequest.getPath();
        } else {
            SshFxpStatRequest sshFxpStatRequest = (SshFxpStatRequest) sftpRequest;
            path = sshFxpStatRequest.getPath();
        }
        SshFile p = resolveFile(path);
        if (!p.doesExist()) {
            return new SshFxpStatusReply(id, SSH_FX_NO_SUCH_FILE, p.getAbsolutePath());
        }
        int flags = SSH_FILEXFER_ATTR_PERMISSIONS | SSH_FILEXFER_ATTR_SIZE;
        return new SshFxpAttrsReply(id, new FileAttributes(p, flags));
    }

    private Reply doProcessWrite(SshFxpWriteRequest request) throws IOException {
        int id = request.getId();
        String handle = request.getHandleId();
        long offset = request.getOffset();
        byte[] data = request.getData();
        Handle p = getHandle(handle);
        if (!(p instanceof FileHandle)) {
            return new SshFxpStatusReply(id, SSH_FX_INVALID_HANDLE, handle);
        } else {
            FileHandle fh = (FileHandle) p;
            fh.write(data, offset);
            SshFile sshFile = fh.getFile();

            sshFile.setLastModified(new Date().getTime());

            return new SshFxpStatusReply(id, SSH_FX_OK, "");
        }
    }

    private Reply doProcessRead(SshFxpReadRequest request) throws IOException {
        int id = request.getId();
        String handle = request.getHandleId();
        long offset = request.getOffset();
        int len = request.getLength();
        Handle p = getHandle(handle);
        if (!(p instanceof FileHandle)) {
            return new SshFxpStatusReply(id, SSH_FX_INVALID_HANDLE, handle);
        } else {
            FileHandle fh = (FileHandle) p;
            byte[] b = new byte[len];
            len = fh.read(b, offset);
            if (len >= 0) {
                return new SshFxpDataReply(id, b, 0, len, len < b.length);
            } else {
                return new SshFxpStatusReply(id, SSH_FX_EOF, "");
            }
        }
    }

    private Reply doProcessClose(SshFxpCloseRequest sftpRequest) throws IOException {
        int id = sftpRequest.getId();
        SshFxpCloseRequest sshFxpCloseRequest = sftpRequest;
        String handle = sshFxpCloseRequest.getHandleId();
        Handle h = getHandle(handle);
        if (h == null) {
            return new SshFxpStatusReply(id, SSH_FX_INVALID_HANDLE, handle, "");
        } else {
            handles.remove(handle);
            h.close();
            return new SshFxpStatusReply(id, SSH_FX_OK, "", "");
        }
    }

    private Reply doProcessOpen(SshFxpOpenRequest request) throws IOException {
        int id = request.getId();
        if (session.getFactoryManager().getProperties() != null) {
            String maxHandlesString = session.getFactoryManager().getProperties().get(MAX_OPEN_HANDLES_PER_SESSION);
            if (maxHandlesString != null) {
                int maxHandleCount = Integer.parseInt(maxHandlesString);
                if (handles.size() > maxHandleCount) {
                    return new SshFxpStatusReply(id, SSH_FX_FAILURE, "Too many open handles");
                }
            }
        }

        int accValue = request.getAcc();
        if (accValue == 0) {
            String path = request.getPath();
            int flags = request.getFlags();
            // attrs
            SshFile file = resolveFile(path);
            if (file.doesExist()) {
                if (((flags & SSH_FXF_CREAT) != 0) && ((flags & SSH_FXF_EXCL) != 0)) {
                    return new SshFxpStatusReply(id, SSH_FX_FILE_ALREADY_EXISTS, path);
                }
            } else {
                if ((flags & SSH_FXF_CREAT) != 0) {
                    if (!file.isWritable()) {
                        return new SshFxpStatusReply(id, SSH_FX_PERMISSION_DENIED, "Can not create " + path);
                    }
                    file.create();
                }
            }
            if ((flags & SSH_FXF_TRUNC) != 0) {
                file.truncate();
            }
            return new SshFxpHandleReply(id, createFileHandle(file, flags));
        } else {
            String path = request.getPath();
            int acc = accValue;
            int flags = request.getFlags();
            // attrs
            SshFile file = resolveFile(path);
            switch (flags & SSH_FXF_ACCESS_DISPOSITION) {
                case SSH_FXF_CREATE_NEW: {
                    if (file.doesExist()) {
                        return new SshFxpStatusReply(id, SSH_FX_FILE_ALREADY_EXISTS, path);
                    } else if (!file.isWritable()) {
                        return new SshFxpStatusReply(id, SSH_FX_PERMISSION_DENIED, "Can not create " + path);
                    }
                    file.create();
                    break;
                }
                case SSH_FXF_CREATE_TRUNCATE: {
                    if (file.doesExist()) {
                        return new SshFxpStatusReply(id, SSH_FX_FILE_ALREADY_EXISTS, path);
                    } else if (!file.isWritable()) {
                        return new SshFxpStatusReply(id, SSH_FX_PERMISSION_DENIED, "Can not create " + path);
                    }
                    file.truncate();
                    break;
                }
                case SSH_FXF_OPEN_EXISTING: {
                    if (!file.doesExist()) {
                        if (!file.getParentFile().doesExist()) {
                            return new SshFxpStatusReply(id, SSH_FX_NO_SUCH_PATH, path);
                        } else {
                            return new SshFxpStatusReply(id, SSH_FX_NO_SUCH_FILE, path);
                        }
                    }
                    break;
                }
                case SSH_FXF_OPEN_OR_CREATE: {
                    if (!file.doesExist()) {
                        file.create();
                    }
                    break;
                }
                case SSH_FXF_TRUNCATE_EXISTING: {
                    if (!file.doesExist()) {
                        if (!file.getParentFile().doesExist()) {
                            return new SshFxpStatusReply(id, SSH_FX_NO_SUCH_PATH, path);
                        } else {
                            return new SshFxpStatusReply(id, SSH_FX_NO_SUCH_FILE, path);
                        }
                    }
                    file.truncate();
                    break;
                }
                default:
                    throw new IllegalArgumentException("Unsupported open mode: " + flags);
            }
            return new SshFxpHandleReply(id, createFileHandle(file, flags));
        }
    }

    private Reply doProcessInit(SshFxpInitRequest request) throws IOException {
        int id = request.getId();
        version = id;
        if (version >= LOWER_SFTP_IMPL) {
            version = Math.min(version, HIGHER_SFTP_IMPL);
            return new SshFxpVersionReply(version);
        } else {
            // We only support version >= 3 (Version 1 and 2 are not common)
            return new SshFxpStatusReply(id, SSH_FX_OP_UNSUPPORTED, "SFTP server only support versions " + ALL_SFTP_IMPL);
        }
    }

    protected SshFxpNameReply sendName(int id, Iterator<SshFile> files) throws IOException {
        SshFxpNameReply reply = new SshFxpNameReply(id);
        int nb = 0;
        while (files.hasNext() && nb < MAX_PACKET_LENGTH / 2) {
            SshFile f = files.next();
            String filename = f.getName();
            if (version <= 3) {
                nb += 55 + filename.length() * 2;
            } else {
                nb += filename.length();
            }
            nb += 10; // Attrs size
            int flags = SSH_FILEXFER_ATTR_PERMISSIONS | SSH_FILEXFER_ATTR_SIZE | SSH_FILEXFER_ATTR_ACCESSTIME;
            reply.addFile(f, filename, getLongName(f), new FileAttributes(f, flags));
        }
        reply.setEol(!files.hasNext());
        return reply;
    }

    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();
    }

    public void destroy() {
        closed = true;
    }

    private SshFile resolveFile(String path) {
        return this.root.getFile(path);
    }


    private String getLongName(SshFile f) {
        String username = f.getOwner();
        if (username.length() > 8) {
            username = username.substring(0, 8);
        } else {
            for (int i = username.length(); i < 8; i++) {
                username = username + " ";
            }
        }

        long length = f.getSize();
        String lengthString = String.format("%1$8s", length);

        StringBuilder sb = new StringBuilder();
        sb.append((f.isDirectory() ? "d" : "-"));
        sb.append((f.isReadable() ? "r" : "-"));
        sb.append((f.isWritable() ? "w" : "-"));
        sb.append((f.isExecutable() ? "x" : "-"));
        sb.append((f.isReadable() ? "r" : "-"));
        sb.append((f.isWritable() ? "w" : "-"));
        sb.append((f.isExecutable() ? "x" : "-"));
        sb.append((f.isReadable() ? "r" : "-"));
        sb.append((f.isWritable() ? "w" : "-"));
        sb.append((f.isExecutable() ? "x" : "-"));
        sb.append(" ");
        sb.append("  1");
        sb.append(" ");
        sb.append(username);
        sb.append(" ");
        sb.append(username);
        sb.append(" ");
        sb.append(lengthString);
        sb.append(" ");
        sb.append(getUnixDate(f.getLastModified()));
        sb.append(" ");
        sb.append(f.getName());

        return sb.toString();
    }

    private final static String[] MONTHS = {"Jan", "Feb", "Mar", "Apr", "May",
            "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};

    /**
     * Get unix style date string.
     */
    private final static String getUnixDate(long millis) {
        if (millis < 0) {
            return "------------";
        }

        StringBuffer sb = new StringBuffer(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();
    }

}
