// Copyright 2019 metadata-backup Authors (see AUTHORS.md) // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // http://www.apache.org/licenses/LICENSE-2.0 // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. extern crate tempdir; use std::fs; use std::io::{Read, Write}; use std::path::{Path, PathBuf}; #[cfg(target_os = "linux")] use std::os::unix::fs::PermissionsExt; use tempdir::TempDir; use metadata_backup::backup; /// Currently this is just a "smoke test" to ensure that no errors are raised /// when building a zip file. #[test] fn test_backup_basic() { let cwd = TempDir::new("backup_test").unwrap(); let cwd_path = cwd.path(); let base_path = cwd_path.join("root"); let dir1 = base_path.join("dir1"); let dir2 = base_path.join("dir2"); let dir2_sub = dir2.clone().join("subdir"); let mut tree = vec![ FSO::directory(base_path.clone()), FSO::directory(base_path.join("empty")), // Empty directory FSO::directory(dir1.clone()), // Directory with files in it FSO::file(dir1.join("file1"), b"Hello"), FSO::file(dir1.join("file2"), b"Goodbye"), FSO::directory(dir2.clone()), // Directory with only directories in it FSO::directory(dir2_sub.clone()), // Subdirectory with files in it FSO::file(dir2_sub.join("subfile1"), b"Nested at depth 2"), FSO::directory(dir2.join("empty")), // Empty subdirectory ]; build_tree(&mut tree); let output_loc = cwd_path.join("backup.zip"); backup::write_backup(&base_path, &output_loc).ok(); assert!(output_loc.exists()); // TODO: Assert something meaningful about the contents of the zip file. } #[cfg(target_os = "linux")] #[test] fn test_backup_permissions() { let cwd = TempDir::new("backup_test").unwrap(); let cwd_path = cwd.path(); let base_path = cwd_path.join("root"); let good_dir = base_path.join("good_dir"); let bad_dir = base_path.join("bad_dir"); let bad_file = good_dir.join("bad_file"); let bad_subdir = good_dir.join("bad_subdir"); let mut tree = vec![ FSO::directory(base_path.clone()), FSO::directory(good_dir.clone()), FSO::directory(bad_dir.clone()), FSO::directory(bad_subdir.clone()), FSO::file(good_dir.join("file1"), b"Hello"), FSO::file(bad_dir.join("file2"), b"Goodbye"), FSO::file(bad_file.clone(), b"Bad permissions"), FSO::file(bad_subdir.join("subfile"), b""), ]; build_tree(&mut tree); // Now remove the x permission on the file let output_loc = cwd_path.join("backup.zip"); // Set some of this tree to have bad permissions, then reset the permissions // afterwards so this can easily be cleaned up. set_mode(&bad_dir, 0o000).unwrap(); set_mode(&bad_file, 0o000).unwrap(); set_mode(&bad_subdir, 0o000).unwrap(); let res = backup::write_backup(&base_path, &output_loc); set_mode(&bad_dir, 0o777).unwrap(); set_mode(&bad_file, 0o777).unwrap(); set_mode(&bad_subdir, 0o777).unwrap(); assert!(res.ok().is_some()); // Doesn't include anything where there should have been a permissions error let mut expected_contents = vec![ ("FILESYSTEM_ROOT/", true), ("FILESYSTEM_ROOT/contents.csv", false), ("FILESYSTEM_ROOT/good_dir/", true), ("FILESYSTEM_ROOT/good_dir/contents.csv", false), (backup::MANIFEST_FILE_NAME, false), ]; let mut output_contents = read_backup(&output_loc); expected_contents.sort(); output_contents.sort(); let expected_contents = expected_contents; let output_contents = output_contents; assert_eq!( expected_contents.len(), output_contents.len(), "\nExpected: {:?}\nOutput: {:?}", expected_contents, output_contents ); for ((exp_name, exp_is_dir), (ref act_name, ref act_size)) in expected_contents.iter().zip(output_contents) { let act_is_dir = !act_size.is_some(); assert_eq!(exp_name, act_name); assert_eq!(*exp_is_dir, act_is_dir); } } #[test] fn test_manifest() { let cwd = TempDir::new("backup_test").unwrap(); let cwd_path = cwd.path(); let base_path = cwd_path.join("root"); let dir1 = base_path.join("dir1"); let dir2 = base_path.join("dir2"); let subdir_fs = dir1.join("subdir_file_subdir"); let subdir_l2 = subdir_fs.join("subdir_l2"); let mut tree = vec![ FSO::directory(base_path.clone()), FSO::directory(dir1.clone()), FSO::directory(dir2.clone()), FSO::directory(subdir_fs.clone()), FSO::directory(dir1.join("empty_subdir")), FSO::directory(subdir_l2.clone()), FSO::file(dir1.join("file1.txt"), b"Foo"), FSO::file(dir1.join("file2.txt"), b"Bar"), FSO::file(dir2.join("empty_file.txt"), b""), FSO::file(subdir_fs.join("subfile.txt"), b"Subdir file"), FSO::file( subdir_l2.join("deepest_nested.txt"), b"Deepest nested file.", ), ]; let mut expected = vec![ "dir1", "dir1/file1.txt", "dir1/file2.txt", "dir1/empty_subdir", "dir1/subdir_file_subdir", "dir1/subdir_file_subdir/subfile.txt", "dir1/subdir_file_subdir/subdir_l2", "dir1/subdir_file_subdir/subdir_l2/deepest_nested.txt", "dir2", "dir2/empty_file.txt", ]; expected.sort(); let expected = expected; build_tree(&mut tree); let output_loc = cwd_path.join("output.zip"); let res = backup::write_backup(&base_path, &output_loc); assert!(res.ok().is_some()); let manifest_output = read_file_manifest(&output_loc); assert_eq!( expected.len(), manifest_output.len(), "Length differs from expecataion\nExpected: {:?}\nActual: {:?}", expected, manifest_output, ); for (expected_path, actual_path) in expected.iter().zip(&manifest_output) { assert_eq!( *expected_path, *actual_path, "Element mismatch {:?} != {:?}\nExpected: {:?}\nActual: {:?})", expected_path, actual_path, expected, manifest_output ); } } fn build_tree(tree: &mut Vec) { tree.sort(); for fso in tree { match fso { FSO::Directory(path) => fs::create_dir(path).unwrap(), FSO::File((path, contents)) => { let mut fobj = fs::File::create(path).unwrap(); fobj.write_all(contents.as_slice()).unwrap(); } } } } fn read_backup>(p: P) -> Vec<(String, Option)> { let f = fs::File::open(p.as_ref()).unwrap(); let mut archive = zip::ZipArchive::new(f).unwrap(); let mut out: Vec<(String, Option)> = Vec::with_capacity(archive.len()); for i in 0..archive.len() { let zf = archive.by_index(i).unwrap(); let name = zf.name().to_owned(); let size = if zf.is_dir() { None } else { Some(zf.size()) }; out.push((name, size)); } out } fn read_file_manifest>(p: P) -> Vec { let f = fs::File::open(p.as_ref()).unwrap(); let mut archive = zip::ZipArchive::new(f).unwrap(); let mut manifest_file = archive.by_name(backup::MANIFEST_FILE_NAME).unwrap(); let mut buffer = String::new(); manifest_file.read_to_string(&mut buffer).unwrap(); // Remove trailing whitespace then read one file path per line buffer.trim_end().split("\n").map(String::from).collect() } #[cfg(target_os = "linux")] fn set_mode>(p: P, mode: u32) -> Result<(), std::io::Error> { let mut perms = p.as_ref().metadata()?.permissions(); perms.set_mode(mode); fs::set_permissions(p, perms)?; Ok(()) } #[derive(Debug)] enum FileSystemObject { File((PathBuf, Vec)), Directory(PathBuf), } type FSO = FileSystemObject; impl FileSystemObject { fn file(p: PathBuf, contents: &[u8]) -> Self { Self::File { 0: (p, contents.to_vec()), } } fn directory(p: PathBuf) -> Self { Self::Directory { 0: p } } fn get_path(&self) -> &Path { match self { FileSystemObject::File((path, _contents)) => &path, FileSystemObject::Directory(path) => &path, } } } impl Ord for FileSystemObject { fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.get_path().cmp(other.get_path()) } } impl Eq for FileSystemObject {} impl PartialEq for FileSystemObject { fn eq(&self, other: &Self) -> bool { self.get_path() == other.get_path() } } impl PartialOrd for FileSystemObject { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } }