Listing files

We'll start this chapter by implementing the command to list files. This will allow us to actually see the files in an FTP client, and we'll be able to tests some commands from the previous chapter by navigating in the directories. So, let's add a case in the Client::handle_cmd() method:

#[async]
fn handle_cmd(mut self, cmd: Command) -> Result<Self> {
    match cmd {
        Command::List(path) => self = await!(self.list(path))?,
        // …
    }
}

This simply calls the list() method, which begins as follows:

use std::fs::read_dir;

#[async]
fn list(mut self, path: Option<PathBuf>) -> Result<Self> {
    if self.data_writer.is_some() {
        let path = self.cwd.join(path.unwrap_or_default());
        let directory = PathBuf::from(&path);
        let (new_self, res) = self.complete_path(directory);
        self = new_self;
        if let Ok(path) = res {
            self = await!
(self.send(Answer::new(ResultCode::DataConnectionAlreadyOpen, "Starting to list directory...")))?;

We first check that the data channel is opened and, if this is the case, we check that the provided optional path is valid. If it is, we send a response that indicates to the client that we're about to send it the data. The next part of the method is as follows:

            let mut out = vec![];
            if path.is_dir() {
                if let Ok(dir) = read_dir(path) {
                    for entry in dir {
                        if let Ok(entry) = entry {
                            add_file_info(entry.path(), &mut out);
                        }
                    }
                } else {
                    self = await!
(self.send(Answer::new(ResultCode::InvalidParameterOrArgument, "No such file or
directory")))?; return Ok(self); } } else { add_file_info(path, &mut out); }

We first create a variable, out, that will contain the data to send to the client. If the specified path is a directory, we use the read_dir() function from the standard library. We then iterate over all files in the directory to gather the info about every file. If we were unable to open the directory, we send an error back to the client. If the path is not a directory, for example, if it is a file, we only get the info for this single file. Here's the end of the method:

            self = await!(self.send_data(out))?;
            println!("-> and done!");
        } else {
            self = await!
(self.send(Answer::new(ResultCode::InvalidParameterOrArgument, "No such file or directory")))?; } } else { self = await!(self.send(Answer::new(ResultCode::ConnectionClosed, "No opened
data connection")))?; } if self.data_writer.is_some() { self.close_data_connection(); self = await!(self.send(Answer::new(ResultCode::ClosingDataConnection,
"Transfer done")))?; } Ok(self) }

We then send the data in the right channel using the send_data() method that we'll see later. If there was another error, we send the appropriate response to the client. If we successfully sent the data, we close the connection and indicate this action to the client. This code used a few new methods, so let's implement them.

First, here's the method that sends data in the data channel:

#[async]
fn send_data(mut self, data: Vec<u8>) -> Result<Self> {
    if let Some(writer) = self.data_writer {
        self.data_writer = Some(await!(writer.send(data))?);
    }
    Ok(self)
}

It is very similar to the send() method, but this one only sends the data if the data socket is opened. Another method that is needed is the one that closes the connection:

fn close_data_connection(&mut self) {
    self.data_reader = None;
    self.data_writer = None;
}

We need to implement the method to gather the info about a file. Here is how it starts:

const MONTHS: [&'static str; 12] = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
                                    "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];

fn add_file_info(path: PathBuf, out: &mut Vec<u8>) {
    let extra = if path.is_dir() { "/" } else { "" };
    let is_dir = if path.is_dir() { "d" } else { "-" };

    let meta = match ::std::fs::metadata(&path) {
        Ok(meta) => meta,
        _ => return,
    };
    let (time, file_size) = get_file_info(&meta);
    let path = match path.to_str() {
        Some(path) => match path.split("/").last() {
            Some(path) => path,
            _ => return,
        },
        _ => return,
    };
    let rights = if meta.permissions().readonly() {
        "r--r--r--"
    } else {
        "rw-rw-rw-"
    };

The parameter out is a mutable reference, because we'll append the info in this variable. Then, we gather the different required info and permissions of the file. Here's the rest of the function:

    let file_str = format!("{is_dir}{rights} {links} {owner} {group} {size} {month}  
{day} {hour}:{min} {path}{extra}\r\n", is_dir=is_dir, rights=rights, links=1, // number of links owner="anonymous", // owner name group="anonymous", // group name size=file_size, month=MONTHS[time.tm_mon as usize], day=time.tm_mday, hour=time.tm_hour, min=time.tm_min, path=path, extra=extra); out.extend(file_str.as_bytes()); println!("==> {:?}", &file_str); }

It formats the info and appends it to the variable out.

This function uses another one:

extern crate time;

use std::fs::Metadata;

#[cfg(windows)]
fn get_file_info(meta: &Metadata) -> (time::Tm, u64) {
    use std::os::windows::prelude::*;
    (time::at(time::Timespec::new(meta.last_write_time())), meta.file_size())
}

#[cfg(not(windows))]
fn get_file_info(meta: &Metadata) -> (time::Tm, u64) {
    use std::os::unix::prelude::*;
    (time::at(time::Timespec::new(meta.mtime(), 0)), meta.size())
}

Here, we have two versions of get_file_info(): one for Windows and the other for all non-Windows operating systems. Since we use a new crate, we need to add this line in Cargo.toml:

time = "0.1.38"

We can now test, in the FTP client, that the files are indeed listed (on the right):

Figure 10.1

If we double-click on a directory, for instance, src, the FTP client will update its content:

Figure 10.2