Skip to main content

pacenotes_server/store/
sidecar_migration.rs

1/// What `migrate_to_sidecars` did.
2#[derive(Debug, Default)]
3pub struct Migration {
4    /// One line per key written, e.g. `rbr/public/style/Descriptive: version`.
5    pub written: Vec<String>,
6    /// Packages whose sidecar is unreadable or whose zip is gone.
7    pub errors: Vec<String>,
8}
9
10/// `manifest migrate-to-sidecars`: appends each value the current manifest
11/// carries and its package's sidecar lacks, so a pure regeneration keeps it.
12///
13/// Never overwrites a key, never pins the legacy or a derived version, and a
14/// second run writes nothing; appending keeps a sidecar's comments, and a
15/// top-level key is valid at the end of a flat TOML file.
16pub 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 &current.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
63/// The package's values the sidecar does not set, as TOML.
64fn 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;