1use std::collections::HashMap;
2use std::fs;
3use std::io;
4use std::path::{Path, PathBuf};
5use std::sync::Arc;
6
7use serde_json::Value;
8
9use crate::catalog::{read_catalog_text, Catalog, ReadCatalogError, CATALOG_SCOPE_NAME};
10use crate::crypto::{keyed_physical_path, CipherOptions, Sealer};
11use crate::schema::{is_physical_path, physical_path_of, FieldType};
12use crate::storage::LOG_SUFFIX;
13
14use super::backup::{
15 owned_entries, read_catalog_of, refuse, restore_staged, sealer_of, ManifestEntry, Owned, RestorePart,
16};
17use super::database::{Database, DatabaseError, LockMode};
18use super::layout::LayoutOptions;
19
20#[derive(Debug, Clone, Default)]
21pub struct ConvertOptions {
22 pub from: Option<CipherOptions>,
24 pub to: Option<CipherOptions>,
26 pub lock: LockMode,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct ConvertResult {
31 pub files: usize,
32 pub tables: usize,
33 pub images: usize,
34 pub lines_renamed: u64,
36}
37
38fn read_error(error: ReadCatalogError) -> DatabaseError {
39 match error {
40 ReadCatalogError::Io(error) => DatabaseError::Io(error),
41 ReadCatalogError::Cipher(error)
42 | ReadCatalogError::Encrypted(error)
43 | ReadCatalogError::NotEncrypted(error) => DatabaseError::Cipher(Box::new(error)),
44 }
45}
46
47fn open_stored(sealer: Option<&Sealer>, scope: &str, bytes: Vec<u8>) -> Result<Vec<u8>, DatabaseError> {
48 match sealer {
49 Some(sealer) => Ok(sealer.open_blob(scope, &bytes)?),
50 None => Ok(bytes),
51 }
52}
53
54fn seal_stored(sealer: Option<&Sealer>, scope: &str, bytes: &[u8]) -> Vec<u8> {
55 match sealer {
56 Some(sealer) => sealer.seal_blob(scope, bytes),
57 None => bytes.to_vec(),
58 }
59}
60
61fn write_synced(path: &Path, bytes: &[u8]) -> io::Result<()> {
62 if let Some(parent) = path.parent() {
63 fs::create_dir_all(parent)?;
64 }
65 let mut file = fs::File::create(path)?;
66 io::Write::write_all(&mut file, bytes)?;
67 file.sync_all()
68}
69
70fn rename_images(line: &str, image_fields: &[String], renamed: &HashMap<String, String>) -> Option<String> {
72 let mut value: Value = serde_json::from_str(line).ok()?;
73 let object = value.as_object_mut()?;
74 let mut changed = false;
75 for field in image_fields {
76 let Some(Value::Object(image)) = object.get_mut(field) else {
77 continue;
78 };
79 let Some(Value::String(path)) = image.get_mut("physicalPath") else {
80 continue;
81 };
82 if let Some(new) = renamed.get(path.as_str()) {
83 *path = new.clone();
84 changed = true;
85 }
86 }
87 changed.then(|| serde_json::to_string(&value).expect("a parsed value serializes"))
88}
89
90pub fn convert_encryption(dir: &Path, options: &ConvertOptions) -> Result<ConvertResult, DatabaseError> {
99 if options.from.is_none() && options.to.is_none() {
100 return Err(refuse(
101 "neither \"from\" nor \"to\" names keys — there is nothing to convert".to_string(),
102 ));
103 }
104 let from = sealer_of(options.from.as_ref())?;
105 let to = sealer_of(options.to.as_ref())?;
106 let layout = Database::resolve_layout_sealed(dir, &LayoutOptions::default(), from.as_deref())?;
107 let catalog = read_catalog_of(&layout, from.as_deref())?;
108 let tables_before: Vec<String> = catalog.tables.keys().cloned().collect();
110 let part = RestorePart {
111 sub: PathBuf::new(),
112 encryption: options.to.clone(),
113 current_encryption: Some(options.from.clone()),
114 metadata_file: layout.metadata_file().to_string(),
115 entries: Vec::new(),
116 };
117 let mut summary = ConvertResult {
118 files: 0,
119 tables: tables_before.len(),
120 images: 0,
121 lines_renamed: 0,
122 };
123 let outcome = {
124 let summary = &mut summary;
125 restore_staged(dir, vec![part], &options.lock, None, |staged, parts| {
126 let (bytes, entries, result) = convert_files(dir, staged, &tables_before, from.clone(), to.clone())?;
127 parts[0].entries = entries;
128 *summary = result;
129 Ok(bytes)
130 })?
131 };
132 let _ = outcome;
133 Ok(summary)
134}
135
136fn convert_files(
137 root: &Path,
138 staged: &Path,
139 tables_before: &[String],
140 from: Option<Arc<Sealer>>,
141 to: Option<Arc<Sealer>>,
142) -> Result<(u64, Vec<ManifestEntry>, ConvertResult), DatabaseError> {
143 let layout = Database::resolve_layout_sealed(root, &LayoutOptions::default(), from.as_deref())?;
144 let catalog: Catalog = read_catalog_of(&layout, from.as_deref())?;
145 if catalog.tables.keys().cloned().collect::<Vec<_>>() != tables_before {
146 return Err(refuse(
147 "the database changed while the conversion was being prepared — run it again".to_string(),
148 ));
149 }
150 if layout.journal_path().exists() {
151 return Err(refuse(format!(
152 "\"{}\" holds an unfinished unit of work — open the database for writing once so it is finished, then convert it",
153 layout.journal_path().display()
154 )));
155 }
156 let files = owned_entries(root, &layout, &catalog, true, &[])?;
157
158 let mut entries = Vec::new();
159 let mut total = 0u64;
160 let mut image_count = 0usize;
161 let mut lines_renamed = 0u64;
162
163 let mut renamed: HashMap<String, HashMap<String, String>> = HashMap::new();
165 let mut written: Vec<(PathBuf, u64)> = Vec::new();
166 for (relative, kind) in &files {
167 let Owned::Image(table) = kind else { continue };
168 let name = relative.file_name().and_then(|n| n.to_str()).unwrap_or_default();
169 if !is_physical_path(name) {
170 let bytes = fs::read(root.join(relative))?;
172 write_synced(&staged.join(relative), &bytes)?;
173 written.push((relative.clone(), bytes.len() as u64));
174 continue;
175 }
176 let stored = fs::read(root.join(relative))?;
177 let plain = open_stored(from.as_deref(), &format!("image:{table}"), stored)?;
178 let new_name = match to.as_deref() {
179 Some(sealer) => keyed_physical_path(&sealer.image_name_key(), &plain),
180 None => physical_path_of(&plain),
181 };
182 let target = relative.with_file_name(&new_name);
183 let sealed = seal_stored(to.as_deref(), &format!("image:{table}"), &plain);
184 write_synced(&staged.join(&target), &sealed)?;
185 written.push((target, sealed.len() as u64));
186 if new_name != name {
187 renamed
188 .entry(table.clone())
189 .or_default()
190 .insert(name.to_string(), new_name);
191 }
192 image_count += 1;
193 }
194
195 for (relative, kind) in &files {
196 match kind {
197 Owned::Image(_) | Owned::Index(_) => continue,
198 Owned::Metadata => {
199 let text = read_catalog_text(&root.join(relative), from.as_deref()).map_err(read_error)?;
200 let text = text.ok_or_else(|| refuse("the metadata document disappeared".to_string()))?;
201 let bytes = seal_stored(to.as_deref(), CATALOG_SCOPE_NAME, text.as_bytes());
202 write_synced(&staged.join(relative), &bytes)?;
203 written.push((relative.clone(), bytes.len() as u64));
204 }
205 Owned::Log(table) => {
206 let image_fields: Vec<String> = catalog.tables[table]
207 .fields
208 .iter()
209 .filter(|(_, spec)| spec.spec.field_type == FieldType::Image)
210 .map(|(field, _)| field.clone())
211 .collect();
212 let names = renamed.get(table);
213 let scope = format!("log:{table}");
214 let text = fs::read_to_string(root.join(relative))?;
215 let mut out = String::with_capacity(text.len());
216 for line in text.split_inclusive('\n') {
217 if !line.ends_with('\n') {
218 break;
220 }
221 let stored = &line[..line.len() - 1];
222 if stored.is_empty() {
223 continue;
224 }
225 let mut plain = match from.as_deref() {
226 Some(sealer) => sealer.open_line(&scope, stored)?,
227 None => stored.to_string(),
228 };
229 if let (Some(names), false) = (names, image_fields.is_empty()) {
230 if let Some(rewritten) = rename_images(&plain, &image_fields, names) {
231 plain = rewritten;
232 lines_renamed += 1;
233 }
234 }
235 out.push_str(&match to.as_deref() {
236 Some(sealer) => sealer.seal_line(&scope, &plain),
237 None => plain,
238 });
239 out.push('\n');
240 }
241 write_synced(&staged.join(relative), out.as_bytes())?;
242 written.push((relative.clone(), out.len() as u64));
243 }
244 }
245 }
246 let _ = LOG_SUFFIX;
247
248 for (relative, bytes) in written {
249 total += bytes;
250 entries.push(ManifestEntry { relative, bytes });
251 }
252 let result = ConvertResult {
253 files: entries.len(),
254 tables: tables_before.len(),
255 images: image_count,
256 lines_renamed,
257 };
258 Ok((total, entries, result))
259}