cargo_l1x/
create.rs

1use anyhow::anyhow;
2use std::fs;
3use std::fs::File;
4use std::io::Cursor;
5use std::path::PathBuf;
6use std::str::FromStr;
7use thiserror::Error;
8use zip::ZipArchive;
9
10#[derive(Error, Debug)]
11pub enum CreateError {
12    #[error("filesystem error: {0}")]
13    IoError(anyhow::Error, std::io::Error),
14    #[error("unknown template: {0}")]
15    UnknownTemplate(String),
16    #[error("Connection error: {0}")]
17    ConnectionError(#[from] reqwest::Error),
18    #[error("Zip error: {0}")]
19    ZipError(#[from] zip::result::ZipError),
20    #[error("A directory with this name already exists: {0}")]
21    DirectoryAlreadyExists(String),
22}
23
24#[derive(Debug, Default)]
25pub enum Template {
26    #[default]
27    LocalDefault,
28    Default,
29    Ft,
30    Nft,
31}
32
33impl FromStr for Template {
34    type Err = CreateError;
35
36    fn from_str(s: &str) -> Result<Self, Self::Err> {
37        match s {
38            "local_default" => Ok(Template::LocalDefault),
39            "default" => Ok(Template::Default),
40            "ft" => Ok(Template::Ft),
41            "nft" => Ok(Template::Nft),
42            _ => Err(CreateError::UnknownTemplate(s.to_string())),
43        }
44    }
45}
46
47impl Template {
48    fn get_zip_template(&self) -> Result<ZipArchive<Cursor<Vec<u8>>>, CreateError> {
49        match self {
50            Template::LocalDefault => {
51                let content =
52                    include_bytes!(concat!(env!("OUT_DIR"), "/default_template.zip")).to_vec();
53                let reader = std::io::Cursor::new(content);
54                let zip = ZipArchive::new(reader)?;
55                Ok(zip)
56            }
57            Template::Default => {
58                let response_body = reqwest::blocking::get("https://github.com/L1X-Foundation/cargo-l1x-templates/archive/refs/heads/default.zip")?.bytes()?;
59                let reader = Cursor::new(response_body.to_vec());
60                let zip = ZipArchive::new(reader)?;
61                Ok(zip)
62            }
63            Template::Ft => {
64                let response_body = reqwest::blocking::get("https://github.com/L1X-Foundation/cargo-l1x-templates/archive/refs/heads/ft.zip")?.bytes()?;
65                let reader = Cursor::new(response_body.to_vec());
66                let zip = ZipArchive::new(reader)?;
67                Ok(zip)
68            }
69            Template::Nft => {
70                let response_body = reqwest::blocking::get("https://github.com/L1X-Foundation/cargo-l1x-templates/archive/refs/heads/nft.zip")?.bytes()?;
71                let reader = Cursor::new(response_body.to_vec());
72                let zip = ZipArchive::new(reader)?;
73                Ok(zip)
74            }
75        }
76    }
77
78    fn unzip(
79        archive: &mut ZipArchive<Cursor<Vec<u8>>>,
80        destination_path: &PathBuf,
81    ) -> Result<(), CreateError> {
82        let mut top_level_dir_name = None;
83
84        for i in 0..archive.len() {
85            let mut file = archive.by_index(i)?;
86            let mut file_path = file.mangled_name();
87            if let Some(top_level_dir_name) = top_level_dir_name.as_ref() {
88                if let Ok(stripped_file_path) = file_path.strip_prefix(top_level_dir_name) {
89                    file_path = stripped_file_path.to_owned();
90                }
91            }
92            let mut path = destination_path.join(file_path);
93            let file_name = file.name().to_owned();
94            if file.is_dir() {
95                if top_level_dir_name.is_none() {
96                    top_level_dir_name = Some(file_name);
97                    continue; // Skip the top-level directory
98                }
99                std::fs::create_dir_all(&path).map_err(|e| {
100                    CreateError::IoError(
101                        anyhow!("Couldn't create a directory: {}", path.display()),
102                        e,
103                    )
104                })?;
105            } else {
106                let parent = path.parent().unwrap();
107                if !parent.exists() {
108                    std::fs::create_dir_all(parent).map_err(|e| {
109                        CreateError::IoError(
110                            anyhow!("Couldn't create a directory: {}", parent.display()),
111                            e,
112                        )
113                    })?;
114                }
115                if path.file_name() == Some("Cargo.toml.template".as_ref()) {
116                    path = path.with_file_name("Cargo.toml");
117                }
118                let mut outfile = File::create(&path).map_err(|e| {
119                    CreateError::IoError(anyhow!("Couldn't create a file: {}", path.display()), e)
120                })?;
121                std::io::copy(&mut file, &mut outfile).map_err(|e| {
122                    CreateError::IoError(anyhow!("Couldn't copy file: {}", path.display()), e)
123                })?;
124            }
125        }
126        Ok(())
127    }
128}
129
130pub fn create(name: String, from_template: String) -> Result<(), CreateError> {
131    let template = Template::from_str(&from_template)?;
132
133    let destination_path = PathBuf::from(&name);
134    if destination_path.exists() {
135        return Err(CreateError::DirectoryAlreadyExists(name));
136    }
137
138    fs::create_dir_all(name.clone())
139        .map_err(|e| CreateError::IoError(anyhow!("Couldn't create a directory: {}", name), e))?;
140
141    let mut archive = template.get_zip_template()?;
142
143    Template::unzip(&mut archive, &destination_path)?;
144
145    Ok(())
146}