metadata_backup/
backup.rs

1// Copyright 2019 metadata-backup Authors (see AUTHORS.md)
2
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6
7//     http://www.apache.org/licenses/LICENSE-2.0
8
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::io::Write;
16use std::path::{Path, PathBuf};
17
18use std::cell::RefCell;
19use std::collections::BinaryHeap;
20use std::sync::Mutex;
21
22use rayon::prelude::*;
23use walkdir::WalkDir;
24
25use super::metadata;
26
27pub static MANIFEST_FILE_NAME: &'static str = "FILE_MANIFEST";
28
29/// Writes a zip file with the metadata backup for all files under `file_root`
30/// to `out_path`.
31pub fn write_backup<P: AsRef<Path>>(file_root: P, out_path: P) -> Result<(), Error> {
32    let file_root = file_root.as_ref();
33    let out_path = out_path.as_ref();
34    let zip_root = PathBuf::from("FILESYSTEM_ROOT");
35
36    // This can panic, so we shold do it before we create the zip file
37    let directories: Vec<PathBuf> = get_all_directories(file_root)?;
38
39    let basic_options = zip::write::FileOptions::default().unix_permissions(0o755);
40    let zip_file = std::fs::File::create(out_path)?;
41    let zip_writer = Mutex::new(zip::ZipWriter::new(zip_file));
42    let file_manifest = Mutex::new(BinaryHeap::new());
43
44    // Now write the directories and the contents of all the directories
45    thread_local! {
46        static DATA_BUFFER : RefCell<Vec<u8>> = RefCell::new(Vec::new());
47    };
48
49    let write_directory = |directory_path: &PathBuf| -> Result<(), Error> {
50        let base_dir_path = directory_path.strip_prefix(file_root)?;
51        let dir_zpath = zip_root.join(&base_dir_path);
52        let contents_path = dir_zpath.join("contents.csv");
53
54        let records = match load_directory_metadata(&directory_path) {
55            Ok(records) => records,
56            Err(e) => {
57                if is_skippable_error(&e) {
58                    eprintln!("Warning: {} at path {}", e, directory_path.display());
59                    return Ok(());
60                } else {
61                    return Err(e.into());
62                }
63            }
64        };
65
66        // Add the files we've seen to the manifest
67        let to_manifest_listing = |md: &metadata::Metadata| -> String {
68            base_dir_path
69                .join(md.name.clone())
70                .to_string_lossy()
71                .into_owned()
72        };
73
74        let mut manifest = file_manifest.lock().unwrap();
75        manifest.extend(records.iter().map(to_manifest_listing));
76
77        // Write the data to the thread-local data buffer
78        DATA_BUFFER.with(|buffer| -> Result<(), Error> {
79            let mut buffer = buffer.borrow_mut();
80            let mut csv_writer = csv::Writer::from_writer(&mut *buffer);
81            for record in records {
82                csv_writer.serialize(record)?;
83            }
84            Ok(())
85        })?;
86        // Acquire the lock while writing to the zip file
87        let mut writer = zip_writer.lock().unwrap();
88
89        writer.add_directory(
90            dir_zpath
91                .to_str()
92                .ok_or(Error::new("Failed to convert directory to string"))?,
93            basic_options,
94        )?;
95
96        writer.start_file_from_path(&contents_path, basic_options)?;
97        DATA_BUFFER.with(|buffer| -> Result<(), Error> {
98            let mut buffer = buffer.borrow_mut();
99            writer.write_all(&mut *buffer)?;
100            buffer.clear();
101            Ok(())
102        })?;
103
104        Ok(())
105    };
106
107    directories.par_iter().try_for_each(write_directory)?;
108
109    // Finally, write the FILE_MANIFEST of all the files we've seen.
110    let file_manifest_vec = file_manifest.into_inner().unwrap().into_sorted_vec();
111    let mut writer = zip_writer.lock().unwrap();
112    writer.start_file_from_path(&PathBuf::from(MANIFEST_FILE_NAME), basic_options)?;
113    for file_path_str in file_manifest_vec {
114        writer.write_all(&file_path_str.as_bytes())?;
115        writer.write_all(b"\n")?;
116    }
117    Ok(())
118}
119
120fn load_directory_metadata<P: AsRef<Path>>(
121    dir_path: P,
122) -> Result<Vec<metadata::Metadata>, std::io::Error> {
123    let dir_path = dir_path.as_ref();
124    std::fs::read_dir(dir_path)?
125        .into_iter()
126        .map(|entry| metadata::Metadata::new(&(entry?).path()))
127        .collect()
128}
129
130fn is_skippable_error(err: &std::io::Error) -> bool {
131    match err.kind() {
132        std::io::ErrorKind::PermissionDenied => true,
133        _ => false,
134    }
135}
136
137fn get_all_directories<P: AsRef<Path>>(base_path: P) -> Result<Vec<PathBuf>, Error> {
138    WalkDir::new(base_path.as_ref())
139        .into_iter()
140        .filter_map(|entry| match entry.as_ref() {
141            Ok(val) => {
142                let path = val.path();
143                if path.is_dir() {
144                    Some(Ok(path.to_path_buf()))
145                } else {
146                    None
147                }
148            }
149            Err(e) => {
150                if is_skippable_error(e.io_error()?) {
151                    eprintln!("Warning: {}", e.to_string());
152                    None
153                } else {
154                    Some(Err(Error::new(e.to_string())))
155                }
156            }
157        })
158        .collect()
159}
160
161pub struct Error {
162    message: String,
163}
164
165impl Error {
166    pub fn new<T: Into<String>>(message: T) -> Error {
167        Error {
168            message: message.into(),
169        }
170    }
171}
172
173impl From<csv::Error> for Error {
174    fn from(err: csv::Error) -> Error {
175        Error::new(err.to_string())
176    }
177}
178
179impl From<std::path::StripPrefixError> for Error {
180    fn from(err: std::path::StripPrefixError) -> Error {
181        Error::new(err.to_string())
182    }
183}
184impl From<zip::result::ZipError> for Error {
185    fn from(err: zip::result::ZipError) -> Error {
186        Error::new(err.to_string())
187    }
188}
189
190impl From<std::io::Error> for Error {
191    fn from(err: std::io::Error) -> Error {
192        Error::new(err.to_string())
193    }
194}
195
196impl From<walkdir::Error> for Error {
197    fn from(err: walkdir::Error) -> Error {
198        Error::new(err.to_string())
199    }
200}
201
202impl std::fmt::Display for Error {
203    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
204        write!(f, "{}", self.message)
205    }
206}