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