Skip to main content

git_simple_encrypt/crypt/
repo.rs

1use 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/// Compute a repo-relative cache key from a file path.
26#[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
42/// Encrypt given files in the repo.
43pub 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
127/// Decrypt given files in the repo.
128pub 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}