1 use std::fs::{canonicalize, create_dir};
2 use std::path::Path;
3 
4 use errors::{bail, Result};
5 use utils::fs::create_file;
6 
7 use crate::console;
8 use crate::prompt::{ask_bool, ask_url};
9 
10 const CONFIG: &str = r#"
11 # The URL the site will be built for
12 base_url = "%BASE_URL%"
13 
14 # Whether to automatically compile all Sass files in the sass directory
15 compile_sass = %COMPILE_SASS%
16 
17 # Whether to build a search index to be used later on by a JavaScript library
18 build_search_index = %SEARCH%
19 
20 [markdown]
21 # Whether to do syntax highlighting
22 # Theme can be customised by setting the `highlight_theme` variable to a theme supported by Zola
23 highlight_code = %HIGHLIGHT%
24 
25 [extra]
26 # Put all your custom variables here
27 "#;
28 
29 // canonicalize(path) function on windows system returns a path with UNC.
30 // Example: \\?\C:\Users\VssAdministrator\AppData\Local\Temp\new_project
31 // More details on Universal Naming Convention (UNC):
32 // https://en.wikipedia.org/wiki/Path_(computing)#Uniform_Naming_Convention
33 // So the following const will be used to remove the network part of the UNC to display users a more common
34 // path on windows systems.
35 // This is a workaround until this issue https://github.com/rust-lang/rust/issues/42869 was fixed.
36 const LOCAL_UNC: &str = "\\\\?\\";
37 
38 // Given a path, return true if it is a directory and it doesn't have any
39 // non-hidden files, otherwise return false (path is assumed to exist)
40 pub fn is_directory_quasi_empty(path: &Path) -> Result<bool> {
41     if path.is_dir() {
42         let mut entries = match path.read_dir() {
43             Ok(entries) => entries,
44             Err(e) => {
45                 bail!(
46                     "Could not read `{}` because of error: {}",
47                     path.to_string_lossy().to_string(),
48                     e
49                 );
50             }
51         };
52         // If any entry raises an error or isn't hidden (i.e. starts with `.`), we raise an error
53         if entries.any(|x| match x {
54             Ok(file) => !file
55                 .file_name()
56                 .to_str()
57                 .expect("Could not convert filename to &str")
58                 .starts_with('.'),
59             Err(_) => true,
60         }) {
61             return Ok(false);
62         }
63         return Ok(true);
64     }
65 
66     Ok(false)
67 }
68 
69 // Remove the unc part of a windows path
70 fn strip_unc(path: &Path) -> String {
71     let path_to_refine = path.to_str().unwrap();
72     path_to_refine.trim_start_matches(LOCAL_UNC).to_string()
73 }
74 
75 pub fn create_new_project(name: &str, force: bool) -> Result<()> {
76     let path = Path::new(name);
77 
78     // Better error message than the rust default
79     if path.exists() && !is_directory_quasi_empty(path)? && !force {
80         if name == "." {
81             bail!("The current directory is not an empty folder (hidden files are ignored).");
82         } else {
83             bail!(
84                 "`{}` is not an empty folder (hidden files are ignored).",
85                 path.to_string_lossy().to_string()
86             )
87         }
88     }
89 
90     console::info("Welcome to Zola!");
91     console::info("Please answer a few questions to get started quickly.");
92     console::info("Any choices made can be changed by modifying the `config.toml` file later.");
93 
94     let base_url = ask_url("> What is the URL of your site?", "https://example.com")?;
95     let compile_sass = ask_bool("> Do you want to enable Sass compilation?", true)?;
96     let highlight = ask_bool("> Do you want to enable syntax highlighting?", false)?;
97     let search = ask_bool("> Do you want to build a search index of the content?", false)?;
98 
99     let config = CONFIG
100         .trim_start()
101         .replace("%BASE_URL%", &base_url)
102         .replace("%COMPILE_SASS%", &format!("{}", compile_sass))
103         .replace("%SEARCH%", &format!("{}", search))
104         .replace("%HIGHLIGHT%", &format!("{}", highlight));
105 
106     populate(path, compile_sass, &config)?;
107 
108     println!();
109     console::success(&format!(
110         "Done! Your site was created in {}",
111         strip_unc(&canonicalize(path).unwrap())
112     ));
113     println!();
114     console::info(
115         "Get started by moving into the directory and using the built-in server: `zola serve`",
116     );
117     println!("Visit https://www.getzola.org for the full documentation.");
118     Ok(())
119 }
120 
populate(path: &Path, compile_sass: bool, config: &str) -> Result<()>121 fn populate(path: &Path, compile_sass: bool, config: &str) -> Result<()> {
122     if !path.exists() {
123         create_dir(path)?;
124     }
125     create_file(&path.join("config.toml"), config)?;
126     create_dir(path.join("content"))?;
127     create_dir(path.join("templates"))?;
128     create_dir(path.join("static"))?;
129     create_dir(path.join("themes"))?;
130     if compile_sass {
131         create_dir(path.join("sass"))?;
132     }
133 
134     Ok(())
135 }
136 
137 #[cfg(test)]
138 mod tests {
139     use super::*;
140     use std::env::temp_dir;
141     use std::fs::{create_dir, remove_dir, remove_dir_all};
142     use std::path::Path;
143 
144     #[test]
init_empty_directory()145     fn init_empty_directory() {
146         let mut dir = temp_dir();
147         dir.push("test_empty_dir");
148         if dir.exists() {
149             remove_dir_all(&dir).expect("Could not free test directory");
150         }
151         create_dir(&dir).expect("Could not create test directory");
152         let allowed = is_directory_quasi_empty(&dir)
153             .expect("An error happened reading the directory's contents");
154         remove_dir(&dir).unwrap();
155         assert!(allowed);
156     }
157 
158     #[test]
init_non_empty_directory()159     fn init_non_empty_directory() {
160         let mut dir = temp_dir();
161         dir.push("test_non_empty_dir");
162         if dir.exists() {
163             remove_dir_all(&dir).expect("Could not free test directory");
164         }
165         create_dir(&dir).expect("Could not create test directory");
166         let mut content = dir.clone();
167         content.push("content");
168         create_dir(&content).unwrap();
169         let allowed = is_directory_quasi_empty(&dir)
170             .expect("An error happened reading the directory's contents");
171         remove_dir(&content).unwrap();
172         remove_dir(&dir).unwrap();
173         assert!(!allowed);
174     }
175 
176     #[test]
init_quasi_empty_directory()177     fn init_quasi_empty_directory() {
178         let mut dir = temp_dir();
179         dir.push("test_quasi_empty_dir");
180         if dir.exists() {
181             remove_dir_all(&dir).expect("Could not free test directory");
182         }
183         create_dir(&dir).expect("Could not create test directory");
184         let mut git = dir.clone();
185         git.push(".git");
186         create_dir(&git).unwrap();
187         let allowed = is_directory_quasi_empty(&dir)
188             .expect("An error happened reading the directory's contents");
189         remove_dir(&git).unwrap();
190         remove_dir(&dir).unwrap();
191         assert!(allowed);
192     }
193 
194     #[test]
populate_existing_directory()195     fn populate_existing_directory() {
196         let mut dir = temp_dir();
197         dir.push("test_existing_dir");
198         if dir.exists() {
199             remove_dir_all(&dir).expect("Could not free test directory");
200         }
201         create_dir(&dir).expect("Could not create test directory");
202         populate(&dir, true, "").expect("Could not populate zola directories");
203 
204         assert!(dir.join("config.toml").exists());
205         assert!(dir.join("content").exists());
206         assert!(dir.join("templates").exists());
207         assert!(dir.join("static").exists());
208         assert!(dir.join("themes").exists());
209         assert!(dir.join("sass").exists());
210 
211         remove_dir_all(&dir).unwrap();
212     }
213 
214     #[test]
populate_non_existing_directory()215     fn populate_non_existing_directory() {
216         let mut dir = temp_dir();
217         dir.push("test_non_existing_dir");
218         if dir.exists() {
219             remove_dir_all(&dir).expect("Could not free test directory");
220         }
221         populate(&dir, true, "").expect("Could not populate zola directories");
222 
223         assert!(dir.exists());
224         assert!(dir.join("config.toml").exists());
225         assert!(dir.join("content").exists());
226         assert!(dir.join("templates").exists());
227         assert!(dir.join("static").exists());
228         assert!(dir.join("themes").exists());
229         assert!(dir.join("sass").exists());
230 
231         remove_dir_all(&dir).unwrap();
232     }
233 
234     #[test]
populate_without_sass()235     fn populate_without_sass() {
236         let mut dir = temp_dir();
237         dir.push("test_wihout_sass_dir");
238         if dir.exists() {
239             remove_dir_all(&dir).expect("Could not free test directory");
240         }
241         create_dir(&dir).expect("Could not create test directory");
242         populate(&dir, false, "").expect("Could not populate zola directories");
243 
244         assert!(!dir.join("sass").exists());
245 
246         remove_dir_all(&dir).unwrap();
247     }
248 
249     #[test]
strip_unc_test()250     fn strip_unc_test() {
251         let mut dir = temp_dir();
252         dir.push("new_project1");
253         if dir.exists() {
254             remove_dir_all(&dir).expect("Could not free test directory");
255         }
256         create_dir(&dir).expect("Could not create test directory");
257         if cfg!(target_os = "windows") {
258             let stripped_path = strip_unc(&canonicalize(Path::new(&dir)).unwrap());
259             assert!(same_file::is_same_file(Path::new(&stripped_path), &dir).unwrap());
260             assert!(!stripped_path.starts_with(LOCAL_UNC), "The path was not stripped.");
261         } else {
262             assert_eq!(
263                 strip_unc(&canonicalize(Path::new(&dir)).unwrap()),
264                 canonicalize(Path::new(&dir)).unwrap().to_str().unwrap().to_string()
265             );
266         }
267 
268         remove_dir_all(&dir).unwrap();
269     }
270 
271     // If the following test fails it means that the canonicalize function is fixed and strip_unc
272     // function/workaround is not anymore required.
273     // See issue https://github.com/rust-lang/rust/issues/42869 as a reference.
274     #[test]
275     #[cfg(target_os = "windows")]
strip_unc_required_test()276     fn strip_unc_required_test() {
277         let mut dir = temp_dir();
278         dir.push("new_project2");
279         if dir.exists() {
280             remove_dir_all(&dir).expect("Could not free test directory");
281         }
282         create_dir(&dir).expect("Could not create test directory");
283 
284         let canonicalized_path = canonicalize(Path::new(&dir)).unwrap();
285         assert!(same_file::is_same_file(Path::new(&canonicalized_path), &dir).unwrap());
286         assert!(canonicalized_path.to_str().unwrap().starts_with(LOCAL_UNC));
287 
288         remove_dir_all(&dir).unwrap();
289     }
290 }
291