doctor_diff_core/
patch.rs1use 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}