1use std::fs;
63use std::io::{Error, ErrorKind};
64use std::path::{Path, PathBuf};
65
66use chrono::prelude::*;
67
68pub fn backup(path: impl AsRef<Path>) -> Result<PathBuf, std::io::Error> {
107 if !path.as_ref().exists() {
109 return Err(Error::new(ErrorKind::NotFound, "Path does not exist."));
110 }
111
112 if path.as_ref().canonicalize()? == std::env::current_dir()?.canonicalize()? {
114 return Err(Error::new(
115 ErrorKind::PermissionDenied,
116 "Cannot backup the current working directory.",
117 ));
118 }
119
120 let parent = match path.as_ref().parent() {
122 Some(x) => match x.to_str() {
123 Some("") => ".",
124 Some(x) => x,
125 None => {
126 return Err(Error::new(
127 ErrorKind::Unsupported,
128 "Path is not a valid UTF-8.",
129 ))
130 }
131 },
132 None => return Err(Error::new(ErrorKind::Unsupported, "Path is root.")),
133 };
134
135 let filename = match path.as_ref().file_name() {
137 Some(x) => match x.to_str() {
138 Some(x) => x,
139 None => {
140 return Err(Error::new(
141 ErrorKind::Unsupported,
142 "Path is not a valid UTF-8.",
143 ))
144 }
145 },
146 None => return Err(Error::new(ErrorKind::Unsupported, "Path ends in '..'.")),
147 };
148
149 let time = Local::now().format("%Y-%m-%d-%H-%M-%S").to_string();
151 let mut backup_name = Path::new(&format!("{}/#{}-{}#", parent, filename, &time)).to_path_buf();
152
153 while backup_name.exists() {
156 let time = Local::now();
157 let micros = time.timestamp_subsec_micros();
158 let time_fmt = time.format("%Y-%m-%d-%H-%M-%S").to_string();
159
160 backup_name = Path::new(&format!(
161 "{}/#{}-{}-{}#",
162 parent, filename, &time_fmt, micros
163 ))
164 .to_path_buf();
165 }
166
167 match fs::rename(path, &backup_name) {
169 Ok(()) => Ok(backup_name),
170 Err(e) => Err(e),
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177 use std::fs::{self, File};
178 use std::io::prelude::*;
179
180 #[test]
181 fn file() {
182 let mut file = File::create("test_file1.txt").unwrap();
183 file.write_all(b"Some content to test.").unwrap();
184
185 let backup = match backup("test_file1.txt") {
186 Ok(x) => x,
187 Err(_) => panic!("Backup failed."),
188 };
189
190 drop(file);
191
192 let mut content = String::new();
193 let mut read = File::open(&backup).unwrap();
194 read.read_to_string(&mut content).unwrap();
195
196 assert_eq!(content, "Some content to test.");
197
198 fs::remove_file(backup).unwrap();
199 }
200
201 #[test]
202 fn file_multiple_backups() {
203 let mut backups = Vec::new();
204 for i in 0..20 {
205 let mut file = File::create("test_file2.txt").unwrap();
206 let text = format!("Unique string for file {}", i);
207 file.write_all(text.as_bytes()).unwrap();
208
209 let backup = match backup("test_file2.txt") {
210 Ok(x) => x,
211 Err(_) => panic!("Backup failed."),
212 };
213
214 backups.push(backup);
215 }
216
217 for (i, path) in backups.iter().enumerate() {
218 let mut content = String::new();
219 let mut read = File::open(&path).unwrap();
220
221 read.read_to_string(&mut content).unwrap();
222
223 let test = format!("Unique string for file {}", i);
224 assert_eq!(content, test);
225
226 fs::remove_file(path).unwrap();
227 }
228 }
229
230 #[test]
231 fn file_in_different_directory() {
232 fs::create_dir("test_dir").unwrap();
233
234 let mut file = File::create("test_dir/test_file.txt").unwrap();
235 file.write_all(b"Some content to test.").unwrap();
236
237 let backup = match backup("test_dir/test_file.txt") {
238 Ok(x) => x,
239 Err(_) => panic!("Backup failed."),
240 };
241
242 drop(file);
243
244 let mut content = String::new();
245 let mut read = File::open(&backup).unwrap();
246 read.read_to_string(&mut content).unwrap();
247
248 assert_eq!(content, "Some content to test.");
249
250 fs::remove_file(&backup).unwrap();
251 fs::remove_dir("test_dir").unwrap();
252 }
253
254 #[test]
255 fn file_multiple_backups_in_different_directory() {
256 fs::create_dir("test_dir2").unwrap();
257
258 let mut backups = Vec::new();
259 for i in 0..20 {
260 let mut file = File::create("test_dir2/test_file.txt").unwrap();
261 let text = format!("Unique string for file {}", i);
262 file.write_all(text.as_bytes()).unwrap();
263
264 let backup = match backup("test_dir2/test_file.txt") {
265 Ok(x) => x,
266 Err(_) => panic!("Backup failed."),
267 };
268
269 backups.push(backup);
270 }
271
272 for (i, path) in backups.iter().enumerate() {
273 let mut content = String::new();
274 let mut read = File::open(&path).unwrap();
275
276 read.read_to_string(&mut content).unwrap();
277
278 let test = format!("Unique string for file {}", i);
279 assert_eq!(content, test);
280
281 fs::remove_file(path).unwrap();
282 }
283
284 fs::remove_dir("test_dir2").unwrap();
285 }
286
287 #[test]
288 fn directory() {
289 fs::create_dir("test_dir3").unwrap();
290
291 let mut file = File::create("test_dir3/test_file.txt").unwrap();
292 file.write_all(b"Some content to test.").unwrap();
293
294 let backup = match backup("test_dir3") {
295 Ok(x) => x,
296 Err(_) => panic!("Backup failed."),
297 };
298
299 drop(file);
300
301 let mut content = String::new();
302 let file_in_backup = backup.join(Path::new("test_file.txt"));
303 let mut read = File::open(&file_in_backup).unwrap();
304 read.read_to_string(&mut content).unwrap();
305
306 assert_eq!(content, "Some content to test.");
307
308 fs::remove_file(&file_in_backup).unwrap();
309 fs::remove_dir(&backup).unwrap();
310 }
311
312 #[test]
313 fn directory_multiple_backups() {
314 let mut backups = Vec::new();
315 for i in 0..10 {
316 fs::create_dir("test_dir4").unwrap();
317
318 let mut file = File::create("test_dir4/test_file.txt").unwrap();
319 let text = format!("Unique string for file {}", i);
320 file.write_all(text.as_bytes()).unwrap();
321
322 let backup = match backup("test_dir4") {
323 Ok(x) => x,
324 Err(_) => panic!("Backup failed."),
325 };
326
327 backups.push(backup);
328 }
329
330 for (i, path) in backups.iter().enumerate() {
331 let file_in_backup = path.join(Path::new("test_file.txt"));
332
333 let mut content = String::new();
334 let mut read = File::open(&file_in_backup).unwrap();
335
336 read.read_to_string(&mut content).unwrap();
337
338 let test = format!("Unique string for file {}", i);
339 assert_eq!(content, test);
340
341 fs::remove_file(&file_in_backup).unwrap();
342 fs::remove_dir(&path).unwrap();
343 }
344 }
345
346 #[test]
347 fn nonexistent() {
348 match backup("nonexistent.txt") {
349 Ok(_) => panic!("Backup should have failed, but it was successful."),
350 Err(e) => assert_eq!(e.to_string(), "Path does not exist."),
351 };
352 }
353
354 #[test]
355 fn root() {
356 match backup("/") {
357 Ok(_) => panic!("Backup should have failed, but it was successful."),
358 Err(e) => assert_eq!(e.to_string(), "Path is root."),
359 };
360 }
361
362 #[test]
363 fn empty() {
364 match backup("") {
365 Ok(_) => panic!("Backup should have failed, but it was successful."),
366 Err(e) => assert_eq!(e.to_string(), "Path does not exist."),
367 };
368 }
369
370 #[test]
371 fn dotdot() {
372 match backup("..") {
373 Ok(_) => panic!("Backup should have failed, but it was successful."),
374 Err(e) => assert_eq!(e.to_string(), "Path ends in '..'."),
375 };
376 }
377
378 #[test]
379 fn current_working_directory1() {
380 match backup(".") {
381 Ok(_) => panic!("Backup should have failed, but it was successful."),
382 Err(e) => assert_eq!(
383 e.to_string(),
384 "Cannot backup the current working directory."
385 ),
386 }
387 }
388
389 #[test]
390 fn current_working_directory2() {
391 match backup("../backitup/../backitup") {
392 Ok(_) => panic!("Backup should have failed, but it was successful."),
393 Err(e) => assert_eq!(
394 e.to_string(),
395 "Cannot backup the current working directory."
396 ),
397 }
398 }
399}