git_simple_encrypt/crypt/
repo.rs1use std::{
2 path::{Path, PathBuf},
3 sync::atomic::{AtomicUsize, Ordering},
4};
5
6use dashmap::DashMap;
7use pathdiff::diff_paths;
8use rand::prelude::*;
9use rayon::prelude::*;
10
11use crate::{
12 crypt::{
13 file::{decrypt_file_with_cache, encrypt_file},
14 header::SALT_LEN,
15 key::{KeyCache, get_or_derive_key},
16 },
17 error::{Error, Result},
18 repo::Repo,
19 salt_cache::{self, CacheRef},
20 utils::{
21 Progress, is_file_encrypted, print_post_report, print_pre_report, resolve_target_files,
22 },
23};
24
25#[must_use]
27pub fn cache_key(file_path: &Path, repo_path: &Path) -> Vec<u8> {
28 let relative = if file_path.is_absolute() {
29 diff_paths(file_path, repo_path).unwrap_or_else(|| file_path.to_path_buf())
30 } else {
31 file_path.to_path_buf()
32 };
33 let mut bytes = relative.into_os_string().into_encoded_bytes();
34 for b in &mut bytes {
35 if *b == b'\\' {
36 *b = b'/';
37 }
38 }
39 bytes
40}
41
42pub fn encrypt_repo(repo: &Repo, paths: &[PathBuf]) -> Result<()> {
44 let key = repo.get_key()?;
45 if key.is_empty() {
46 return Err(Error::EmptyKey);
47 }
48
49 let target_files = resolve_target_files(paths, &repo.conf.crypt_list, repo.path());
50 if target_files.is_empty() {
51 return Err(Error::NoFile("encrypt"));
52 }
53
54 print_pre_report("Encrypting", &target_files, repo.path());
55
56 let reader = salt_cache::SaltCacheReader::load(repo.path());
57 let key_cache: KeyCache = DashMap::new();
58
59 let mut batch_salt = [0u8; SALT_LEN];
60 rand::rng().fill_bytes(&mut batch_salt);
61
62 let pb = Progress::new(target_files.len(), "Encrypt");
63 let skipped = AtomicUsize::new(0);
64 let failed = AtomicUsize::new(0);
65
66 let result = {
67 let errors: parking_lot::Mutex<Vec<Error>> = parking_lot::Mutex::new(Vec::new());
68 target_files.par_iter().for_each(|f| {
69 let relative_key = cache_key(f, repo.path());
70 let (salt, cached_file_id) = reader
71 .get(&relative_key)
72 .map_or((batch_salt, None), |entry| {
73 (entry.salt, Some(entry.file_id))
74 });
75
76 let derived_key = match get_or_derive_key(&key_cache, key.as_bytes(), &salt) {
77 Ok(k) => k,
78 Err(e) => {
79 failed.fetch_add(1, Ordering::Relaxed);
80 errors.lock().push(e);
81 pb.inc(1);
82 return;
83 }
84 };
85
86 let r = encrypt_file(
87 f,
88 &derived_key,
89 &salt,
90 cached_file_id,
91 repo.conf.use_zstd.then_some(repo.conf.zstd_level),
92 )
93 .map_err(|e| Error::Other(format!("Failed to encrypt {}: {e}", f.display())));
94
95 match r {
96 Ok(Some(_)) => {}
97 Ok(None) => {
98 skipped.fetch_add(1, Ordering::Relaxed);
99 }
100 Err(e) => {
101 failed.fetch_add(1, Ordering::Relaxed);
102 errors.lock().push(e);
103 }
104 }
105
106 pb.inc(1);
107 });
108 errors.into_inner()
109 };
110
111 pb.finish_and_clear();
112
113 print_post_report(
114 "Encrypt",
115 target_files.len(),
116 skipped.load(Ordering::Relaxed),
117 failed.load(Ordering::Relaxed),
118 );
119
120 if let Some(first) = result.into_iter().next() {
121 return Err(first);
122 }
123
124 Ok(())
125}
126
127pub fn decrypt_repo(repo: &Repo, paths: &[PathBuf]) -> Result<()> {
129 let key = repo.get_key()?;
130 if key.is_empty() {
131 return Err(Error::EmptyKey);
132 }
133
134 let target_files = resolve_target_files(paths, &repo.conf.crypt_list, repo.path());
135 if target_files.is_empty() {
136 return Err(Error::NoFile("decrypt"));
137 }
138
139 print_pre_report("Decrypting", &target_files, repo.path());
140
141 let key_cache: KeyCache = DashMap::new();
142 let (sender, saver) = salt_cache::create_writer(repo.path());
143
144 let pb = Progress::new(target_files.len(), "Decrypt");
145 let skipped = AtomicUsize::new(0);
146 let failed = AtomicUsize::new(0);
147
148 let result = {
149 let errors: parking_lot::Mutex<Vec<Error>> = parking_lot::Mutex::new(Vec::new());
150 target_files.par_iter().for_each(|f| {
151 match is_file_encrypted(f) {
152 Ok(true) => {}
153 Ok(false) => {
154 skipped.fetch_add(1, Ordering::Relaxed);
155 pb.inc(1);
156 return;
157 }
158 Err(e) => {
159 failed.fetch_add(1, Ordering::Relaxed);
160 errors.lock().push(e);
161 pb.inc(1);
162 return;
163 }
164 }
165
166 let relative_key = cache_key(f, repo.path());
167
168 let r = decrypt_file_with_cache(
169 f,
170 &key_cache,
171 Some(CacheRef {
172 sender: &sender,
173 key: &relative_key,
174 }),
175 key.as_bytes(),
176 )
177 .map_err(|e| Error::Other(format!("Failed to decrypt {}: {e}", f.display())));
178
179 if let Err(e) = r {
180 failed.fetch_add(1, Ordering::Relaxed);
181 errors.lock().push(e);
182 }
183
184 pb.inc(1);
185 });
186 errors.into_inner()
187 };
188
189 drop(sender);
190 saver.save();
191
192 pb.finish_and_clear();
193
194 print_post_report(
195 "Decrypt",
196 target_files.len(),
197 skipped.load(Ordering::Relaxed),
198 failed.load(Ordering::Relaxed),
199 );
200
201 if let Some(first) = result.into_iter().next() {
202 return Err(first);
203 }
204
205 Ok(())
206}