pacenotes_server/store/
sidecar_migration.rs1#[derive(Debug, Default)]
3pub struct Migration {
4 pub written: Vec<String>,
6 pub errors: Vec<String>,
8}
9
10pub fn migrate_to_sidecars(data_dir: &std::path::Path) -> anyhow::Result<Migration> {
17 let current = crate::store::read_manifest(&data_dir.join(crate::store::MANIFEST_FILE))?;
18 let mut migration = Migration::default();
19 for package in ¤t.packages {
20 let id = package.download_path();
21 let Some(zip) = crate::store::zip_file(data_dir, package).filter(|zip| zip.is_file())
22 else {
23 migration.errors.push(format!(
24 "{id}: its zip is gone — nothing to attach the values to"
25 ));
26 continue;
27 };
28 let sidecar_path = zip.with_extension("toml");
29 let text = match std::fs::read_to_string(&sidecar_path) {
30 Ok(text) => text,
31 Err(e) if e.kind() == std::io::ErrorKind::NotFound => String::new(),
32 Err(e) => {
33 migration
34 .errors
35 .push(format!("{id}: reading {}: {e}", sidecar_path.display()));
36 continue;
37 }
38 };
39 let existing = match crate::store::sidecar::parse_sidecar(&text) {
40 Ok(existing) => existing,
41 Err(e) => {
42 migration.errors.push(format!(
43 "{id}: {} does not parse ({e}) — fix it first",
44 sidecar_path.display()
45 ));
46 continue;
47 }
48 };
49
50 let missing = missing_keys(&existing, package);
51 if missing.is_empty() {
52 continue;
53 }
54 migration
55 .written
56 .extend(missing.iter().map(|(key, _)| format!("{id}: {key}")));
57 std::fs::write(&sidecar_path, with_keys_appended(text, &missing))
58 .map_err(|e| anyhow::anyhow!("writing {}: {e}", sidecar_path.display()))?;
59 }
60 Ok(migration)
61}
62
63fn missing_keys(
65 existing: &crate::store::sidecar::Sidecar,
66 package: &pacenotes_shared::PackageInfo,
67) -> Vec<(&'static str, toml::Value)> {
68 let text = |value: &str| toml::Value::String(value.to_string());
69 let mut missing = Vec::new();
70 if existing.version.is_none() && crate::store::is_chosen_version(&package.version) {
71 missing.push(("version", text(&package.version)));
72 }
73 let optional_fields = [
74 (
75 "display_name",
76 &existing.display_name,
77 &package.display_name,
78 ),
79 ("description", &existing.description, &package.description),
80 (
81 "exclusive_group",
82 &existing.exclusive_group,
83 &package.exclusive_group,
84 ),
85 ];
86 for (key, in_sidecar, in_manifest) in optional_fields {
87 if let (None, Some(value)) = (in_sidecar, in_manifest) {
88 missing.push((key, text(value)));
89 }
90 }
91 if existing.requires.is_none() && !package.requires.is_empty() {
92 let references = package.requires.iter().map(|reference| text(reference));
93 missing.push(("requires", toml::Value::Array(references.collect())));
94 }
95 missing
96}
97
98fn with_keys_appended(mut text: String, keys: &[(&'static str, toml::Value)]) -> String {
99 if text.is_empty() {
100 text.push_str("# Moved here from manifest.json by `manifest migrate-to-sidecars`.\n");
101 } else if !text.ends_with('\n') {
102 text.push('\n');
103 }
104 for (key, value) in keys {
105 text.push_str(&format!("{key} = {value}\n"));
106 }
107 text
108}
109
110#[cfg(test)]
111mod tests;