cpclib-xfer 0.6.0

cpclib libraries related to snapshots
Documentation
use std::fs;
use std::path::Path;

use curl::easy::{Easy, Form};
use curl::Error;
use custom_error::custom_error;
use path_absolutize::*;
use {cpclib_disc as disc, cpclib_sna as sna, path_absolutize};

use crate::disc::amsdos::AmsdosFileType;
use crate::sna::{Snapshot, SnapshotVersion};

custom_error! {#[allow(missing_docs)] pub XferError
    ConnectionError{source: Error} = "There is a connection error with the Cpc Wifi.",
    ConnectionError2{source: reqwest::Error} = "There is a connection error with the Cpc Wifi.",

    CdError{from: String, to: String} = @ {
        format!(
            "Unable to move in {}. Current working directory is {}.",
            from, to)
    },
    InternalError{reason: String} = @ {
        format!("Internal error: {}", reason)
    }
}

#[derive(Debug)]
/// File in a list of files generated by the m4
pub struct M4File {
    /// File name
    fname: String,
    /// TODO search what it is
    unknown: String,
    /// File size
    size: String
}

impl M4File {
    /// Return the filname
    pub fn fname(&self) -> &str {
        &self.fname
    }
}

impl From<&str> for M4File {
    fn from(line: &str) -> Self {
        let mut splitted = line.split(',');
        Self {
            fname: splitted.next().unwrap().into(),
            unknown: splitted.next().unwrap().into(),
            size: splitted.next().unwrap().into()
        }
    }
}

/// List of files in the M4
#[derive(Debug)]
pub struct M4FilesList {
    /// Directory of the list
    cwd: String,
    /// List of files
    files: Vec<M4File>
}

impl From<&str> for M4FilesList {
    // TODO Better test for catart

    fn from(buffer: &str) -> Self {
        let mut lines = buffer.lines();
        let mut path = lines.next().unwrap();
        if path == "//" {
            path = "/";
        }

        let mut lines = lines.map(String::from).collect::<Vec<String>>();
        let mut idx = 0;
        while idx < lines.len() {
            // check if current line is ok
            if lines[idx].match_indices(';').count() >= 2 && lines[lines.len() - 1] == "K" {
                idx = idx + 1; // we can assume it is a standard file even if it may not be one
            }
            else if lines[idx].ends_with(",0,0") {
                // we can assume it is a directory
                idx = idx + 1;
            }
            else {
                // we can consider it is a mistake because of cat art
                let new_string = format!("{}\n{}", lines[idx], lines[idx + 1]);
                lines[idx] = new_string;
            }
        }
        let files = lines
            .iter()
            .map(|s| s.as_str().into())
            .collect::<Vec<M4File>>();
        Self {
            cwd: path.into(),
            files
        }
    }
}

#[allow(missing_docs)]
impl M4FilesList {
    pub fn cwd(&self) -> &String {
        &self.cwd
    }

    pub fn nb_files(&self) -> usize {
        self.files.len()
    }

    // TODO do not give acces to the vector
    pub fn files(&self) -> &Vec<M4File> {
        &self.files
    }
}

/// Bridget the the CPC Wifi card
#[derive(Debug)]
pub struct CpcXfer {
    /// CPC Wifi hostname
    hostname: String
}

#[allow(missing_docs)]
impl CpcXfer {
    /// Create the CpcXfer given an address
    pub fn new<S: AsRef<str>>(hostname: S) -> Self {
        Self {
            hostname: String::from(hostname.as_ref())
        }
    }

    pub fn hostname(&self) -> &str {
        &self.hostname
    }

    /// Return the appropriate uri
    fn uri(&self, path: &str) -> String {
        format!("http://{}/{}", self.hostname, path)
    }

    /// Make a simple query
    fn simple_query(&self, query: &[(&str, &str)]) -> reqwest::Result<reqwest::blocking::Response> {
        reqwest::blocking::Client::new()
            .get(&self.uri("config.cgi"))
            .query(query)
            .header("User-Agent", "User-Agent: cpcxfer")
            .send()
    }

    /// Reset the M4
    pub fn reset_m4(&self) -> Result<(), XferError> {
        self.simple_query(&[("mres", "")])?;
        Ok(())
    }

    /// Reset the Cpc
    pub fn reset_cpc(&self) -> Result<(), XferError> {
        self.simple_query(&[("cres", "")])?;
        Ok(())
    }

    /// Run the file from the current selected path
    /// TODO debug this
    pub fn run_rom_current_path(&self, fname: &str) -> Result<(), XferError> {
        self.simple_query(&[("run", fname)])?;
        Ok(())
    }

    /// Run the file whose complete path is provided
    pub fn run(&self, path: &str) -> Result<(), XferError> {
        let absolute = self.absolute_path(path)?;
        self.simple_query(&[("run2", &absolute)])?;
        Ok(())
    }

    /// Remove the file whose complete path is provided
    pub fn rm<S: AsRef<str>>(&self, path: S) -> Result<(), XferError> {
        self.simple_query(&[("rm", path.as_ref())])?;
        Ok(())
    }

    /// upload a file on the M4
    pub fn upload<P>(
        &self,
        path: P,
        m4_path: &str,
        header: Option<(AmsdosFileType, u16, u16)>
    ) -> Result<(), XferError>
    where
        P: AsRef<Path>
    {
        self.upload_impl(path.as_ref(), m4_path, header)
    }

    #[allow(clippy::similar_names)]
    pub fn upload_impl(
        &self,
        path: &Path,
        m4_path: &str,
        header: Option<(AmsdosFileType, u16, u16)>
    ) -> Result<(), XferError> {
        let local_fname = path.to_str().unwrap();

        if m4_path.len() > 255 {
            panic!(
                "{} path is too long (should be limited to 255 chars)",
                m4_path
            );
        }
        let _file_contents = fs::read(local_fname).expect("Unable to read PC file");

        let local_fname = match header {
            Some(_header) => {
                unimplemented!();
                // Need to build header and compute checksum
                // Need to inject header
            }
            None => {
                // Header is already included within the file
                // TODO check that the header is correct
                local_fname
            }
        };

        // TODO manage more cases in order to allow to provide a destination folder or a destination filename or a different name
        let destination = Path::new(m4_path).join(
            Path::new(local_fname)
                .file_name()
                .expect("Unable to retreive the filename of the file to upload")
        );
        let destination = destination.to_str().unwrap().to_owned();

        println!("Destination : {:?}", destination);

        let mut form = Form::new();
        form.part("upfile")
            .file(local_fname)
            .filename(&destination)
            .add()
            .unwrap();
        let mut easy = Easy::new();
        easy.url(&self.uri("files.shtml"))?;
        easy.httppost(form)?;
        easy.perform()?;

        Ok(())
    }

    /// Directly sends the SNA to the M4. SNA is first saved as a V2 version as M4 is unable to read other ones
    pub fn upload_and_run_sna(&self, sna: &Snapshot) -> Result<(), XferError> {
        use tempfile::Builder;
        let file = Builder::new()
            .prefix("xfer")
            .suffix(".sna")
            .rand_bytes(4)
            .tempfile()
            .or_else(|e| {
                Err(XferError::InternalError {
                    reason: e.to_string()
                })
            })?;
        let temp_path = file.into_temp_path();

        sna.save(&temp_path, SnapshotVersion::V2).or_else(|e| {
            Err(XferError::InternalError {
                reason: format!("Unable to save the snapshot. {}", e)
            })
        })?;
        self.upload_and_run(&temp_path, None)?;

        // sleep a bit to be sure the file is not deleted BEFORE sending it to CPC
        std::thread::sleep(std::time::Duration::from_secs(5));
        temp_path.close().map_err(|e| {
            XferError::InternalError {
                reason: e.to_string()
            }
        })
    }

    pub fn upload_and_run<P: AsRef<Path>>(
        &self,
        path: P,
        header: Option<(AmsdosFileType, u16, u16)>
    ) -> Result<(), XferError> {
        self.upload_and_run_impl(path.as_ref(), header)
    }

    fn upload_and_run_impl(
        &self,
        path: &Path,
        header: Option<(AmsdosFileType, u16, u16)>
    ) -> Result<(), XferError> {
        // We are sure it is not a snapshot there
        self.upload_impl(path, "/tmp", header)?;
        self.run(&format!(
            "/tmp/{}",
            path.file_name().unwrap().to_str().unwrap()
        ))?;
        Ok(())
    }

    pub fn current_folder_content(&self) -> Result<M4FilesList, XferError> {
        self.download_dir()
    }

    pub fn current_working_directory(&self) -> Result<String, XferError> {
        let data = self.download_dir()?;
        Ok(data.cwd().clone())
    }

    fn download_dir(&self) -> Result<M4FilesList, XferError> {
        let mut dst = Vec::new();
        {
            {
                let mut easy = Easy::new();
                easy.url(&self.uri("sd/m4/dir.txt"))?;
                let mut easy = easy.transfer();
                easy.write_function(|data| {
                    dst.extend_from_slice(data);
                    Ok(data.len())
                })?;
                easy.perform()?;
            }
        }

        let content =
            std::str::from_utf8(&dst).expect("Unable to create an UTF8 string for M4 content");

        Ok(M4FilesList::from(content))
    }

    /// Change the current directory
    pub fn cd(&self, directory: &str) -> Result<(), XferError> {
        // Get the absolute directory
        let mut directory = if let Some('/') = directory.chars().next() {
            directory.to_owned()
        }
        else {
            self.absolute_path(directory)?
        };

        self.ls_request(&directory)?;

        // Ensure theire is a / at the end
        if directory.chars().rev().next().unwrap() != '/' {
            directory.push('/');
        }
        let cwd = self.current_working_directory()?;

        if cwd == directory {
            Ok(())
        }
        else {
            Err(XferError::CdError {
                from: directory,
                to: cwd
            })
        }
    }

    fn absolute_path(&self, relative: &str) -> Result<String, XferError> {
        match relative.chars().next() {
            None => {
                Err(XferError::InternalError {
                    reason: "No path provided".into()
                })
            }
            Some('/') => Ok(relative.to_owned()),
            _ => {
                let cwd = self.current_working_directory()?;
                let absolute = Path::new(&cwd).join(relative);

                let absolute = absolute.absolutize().unwrap();
                let path: String = absolute.to_str().unwrap().into();
                if cfg!(target_os = "windows") {
                    return Ok(path.replace("C:\\", "/"));
                }

                Ok(path)
            }
        }
    }

    pub fn ls_request(&self, folder: &str) -> Result<(), XferError> {
        let mut easy = Easy::new();
        let folder = easy.url_encode(folder.as_bytes());
        easy.get(true)?;
        let url = format!("{}?ls={}", self.uri("config.cgi"), folder);
        easy.url(&url)?;
        easy.perform()?;
        Ok(())
    }
}