doctor_diff_core/
patch.rs

1use crate::{hash::HashValue, utils::hash_directory};
2use serde::{Deserialize, Serialize};
3use std::{
4    collections::{HashMap, HashSet},
5    env::temp_dir,
6    fs::{create_dir_all, read_to_string, remove_file, write, File},
7    io::{Read, Result, Write},
8    path::{Path, PathBuf},
9    time::SystemTime,
10};
11use zip::{write::FileOptions, CompressionMethod, ZipArchive, ZipWriter};
12
13#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
14pub enum Change {
15    Add,
16    Update,
17    Remove,
18}
19
20pub fn patch_request<P>(workspace: P, hashes: P) -> Result<()>
21where
22    P: AsRef<Path>,
23{
24    println!("* Patch request");
25    let local_hashes = hash_directory(workspace.as_ref())?;
26    println!("* Hashes: {:#?}", local_hashes);
27    let data = serde_json::to_string_pretty(&local_hashes)?;
28    write(hashes.as_ref(), data)
29}
30
31pub fn patch_create<P, PD>(workspace: P, hashes: P, archive: PD) -> Result<()>
32where
33    P: AsRef<Path>,
34    PD: AsRef<Path> + std::fmt::Debug,
35{
36    println!("* Patch create");
37    let local_hashes = hash_directory(workspace.as_ref())?;
38    let hashes = read_to_string(hashes)?;
39    let hashes = serde_json::from_str(&hashes)?;
40    let changes = diff_changes(&hashes, &local_hashes);
41    println!("* Changes: {:#?}", changes);
42    let number = match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) {
43        Ok(duration) => duration.as_nanos(),
44        Err(_) => 0,
45    };
46    let mut archive_path = temp_dir();
47    archive_path.push(format!("doctor-diff-{}.zip", number));
48    archive_changes(workspace, archive, &changes)
49}
50
51pub fn patch_apply<P>(workspace: P, archive: P) -> Result<()>
52where
53    P: AsRef<Path>,
54{
55    unarchive_changes(workspace, archive)
56}
57
58pub fn diff_changes(
59    client_hashes: &HashMap<PathBuf, HashValue>,
60    server_hashes: &HashMap<PathBuf, HashValue>,
61) -> HashMap<PathBuf, Change> {
62    let mut result = HashMap::with_capacity(server_hashes.len());
63    let server_paths = server_hashes.keys().collect::<HashSet<_>>();
64    let client_paths = client_hashes.keys().collect::<HashSet<_>>();
65    for path in server_paths.intersection(&client_paths) {
66        let server_hash = server_hashes.get(*path).unwrap();
67        let client_hash = client_hashes.get(*path).unwrap();
68        if server_hash != client_hash {
69            result.insert((*path).to_owned(), Change::Update);
70        }
71    }
72    for path in server_paths.difference(&client_paths) {
73        result.insert((*path).to_owned(), Change::Add);
74    }
75    for path in client_paths.difference(&server_paths) {
76        result.insert((*path).to_owned(), Change::Remove);
77    }
78    result
79}
80
81pub fn archive_changes<P, PD>(
82    workspace: P,
83    archive: PD,
84    changes: &HashMap<PathBuf, Change>,
85) -> Result<()>
86where
87    P: AsRef<Path>,
88    PD: AsRef<Path> + std::fmt::Debug,
89{
90    println!("* Archive changes to: {:?}", archive.as_ref());
91    let mut archive = ZipWriter::new(File::create(archive)?);
92    let comment = serde_json::to_string_pretty(changes)?;
93    archive.set_comment(&comment);
94    let options = FileOptions::default().compression_method(CompressionMethod::Bzip2);
95    for (path, change) in changes {
96        match change {
97            Change::Add | Change::Update => {
98                println!("* Archive change: {:?}", path);
99                let mut reader = File::open(workspace.as_ref().join(path))?;
100                #[allow(deprecated)]
101                archive.start_file_from_path(path, options.clone())?;
102                let mut buffer = [0; 10240];
103                loop {
104                    let count = reader.read(&mut buffer)?;
105                    if count == 0 {
106                        break;
107                    }
108                    archive.write(&buffer[..count])?;
109                }
110            }
111            _ => {}
112        }
113    }
114    archive.finish()?;
115    Ok(())
116}
117
118pub fn unarchive_changes<P>(workspace: P, archive: P) -> Result<()>
119where
120    P: AsRef<Path>,
121{
122    println!("* Unarchive changes from: {:?}", archive.as_ref());
123    let mut archive = ZipArchive::new(File::open(archive)?)?;
124    let changes = serde_json::from_slice::<HashMap<PathBuf, Change>>(archive.comment())?;
125    for (path, change) in changes {
126        match change {
127            Change::Add | Change::Update => match archive.by_name(&archivable_path(&path)) {
128                Ok(mut reader) => {
129                    let mut dir = workspace.as_ref().join(&path);
130                    dir.pop();
131                    create_dir_all(dir)?;
132                    println!("* Unarchive change: {:?}", path);
133                    let mut writer = File::create(workspace.as_ref().join(&path))?;
134                    let mut buffer = [0; 10240];
135                    loop {
136                        let count = reader.read(&mut buffer)?;
137                        if count == 0 {
138                            break;
139                        }
140                        writer.write(&buffer[..count])?;
141                    }
142                }
143                Err(error) => println!("* Could not update file: {:?} - {:?}", path, error),
144            },
145            Change::Remove => {
146                if let Err(error) = remove_file(workspace.as_ref().join(&path)) {
147                    println!("* Could not remove local file: {:?} - {:?}", path, error);
148                }
149            }
150        }
151    }
152    Ok(())
153}
154
155pub fn archivable_path<P>(path: P) -> String
156where
157    P: AsRef<Path>,
158{
159    path.as_ref().to_string_lossy().replace("\\", "/")
160}