initiative_core/storage/
backup.rs

1use super::repository::{Change, Error as RepositoryError, KeyValue, Repository};
2use crate::world::thing::{Thing, ThingData};
3use futures::join;
4use serde::{Deserialize, Serialize};
5use std::fmt;
6
7#[derive(Clone, Debug, Deserialize, Serialize)]
8pub struct BackupData {
9    #[serde(rename(serialize = "_"), skip_deserializing)]
10    pub comment: &'static str,
11
12    pub things: Vec<Thing>,
13
14    #[serde(rename = "keyValue")]
15    pub key_value: KeyValueBackup,
16}
17
18#[derive(Clone, Debug, Deserialize, Serialize)]
19pub struct KeyValueBackup {
20    pub time: Option<String>,
21}
22
23#[derive(Default)]
24pub struct ImportStats {
25    npc_stats: ImportStat,
26    place_stats: ImportStat,
27    key_value_stats: ImportStat,
28}
29
30#[derive(Default)]
31struct ImportStat {
32    created: usize,
33    updated: usize,
34    failed: usize,
35}
36
37pub async fn export(repo: &Repository) -> BackupData {
38    let (things, time) = join!(repo.journal(), repo.get_key_value(&KeyValue::Time(None)));
39
40    BackupData {
41        comment: "This document is exported from initiative.sh. Please note that this format is currently undocumented and no guarantees of forward compatibility are provided, although a reasonable effort will be made to ensure that older backups can be safely imported.",
42        things: things.unwrap_or_default(),
43        key_value: KeyValueBackup {
44            time: time.ok().and_then(|t| t.time()).map(|t| t.display_short().to_string()),
45        },
46    }
47}
48
49pub async fn import(
50    repo: &mut Repository,
51    mut data: BackupData,
52) -> Result<ImportStats, RepositoryError> {
53    let mut stats = ImportStats::default();
54
55    for thing in data.things.into_iter() {
56        match (
57            match &thing.data {
58                ThingData::Npc(_) => &mut stats.npc_stats,
59                ThingData::Place(_) => &mut stats.place_stats,
60            },
61            repo.modify_without_undo(Change::CreateAndSave {
62                thing_data: thing.data,
63                uuid: Some(thing.uuid),
64            })
65            .await,
66        ) {
67            (stat, Ok(_)) => stat.created += 1,
68            (
69                stat,
70                Err((
71                    Change::CreateAndSave { thing_data, .. },
72                    RepositoryError::NameAlreadyExists(existing_thing)
73                    | RepositoryError::UuidAlreadyExists(existing_thing),
74                )),
75            ) => {
76                let name = thing_data.name().to_string();
77                match repo
78                    .modify_without_undo(Change::Edit {
79                        name,
80                        uuid: Some(existing_thing.uuid),
81                        diff: thing_data,
82                    })
83                    .await
84                {
85                    Ok(_) => stat.updated += 1,
86                    Err(_) => stat.failed += 1,
87                }
88            }
89            (stat, Err(_)) => stat.failed += 1,
90        }
91    }
92
93    if let Some(time) = data.key_value.time.take().and_then(|s| s.parse().ok()) {
94        match repo
95            .modify_without_undo(Change::SetKeyValue {
96                key_value: KeyValue::Time(Some(time)),
97            })
98            .await
99        {
100            Ok(Change::SetKeyValue {
101                key_value: KeyValue::Time(None),
102            }) => stats.key_value_stats.created += 1,
103            Ok(Change::SetKeyValue {
104                key_value: KeyValue::Time(Some(_)),
105            }) => stats.key_value_stats.updated += 1,
106            Ok(_) => unreachable!(),
107            Err(_) => stats.key_value_stats.failed += 1,
108        }
109    }
110
111    Ok(stats)
112}
113
114impl fmt::Display for ImportStats {
115    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
116        let mut first = true;
117
118        if !self.place_stats.is_empty() {
119            write!(f, "Places: {}", self.place_stats)?;
120            first = false;
121        }
122
123        if !self.npc_stats.is_empty() {
124            if !first {
125                writeln!(f, " \\")?;
126            }
127            write!(f, "Characters: {}", self.npc_stats)?;
128            first = false;
129        }
130
131        if !self.key_value_stats.is_empty() {
132            if !first {
133                writeln!(f, " \\")?;
134            }
135            write!(f, "Key/values: {}", self.key_value_stats)?;
136            first = false;
137        }
138
139        if first {
140            write!(f, "Nothing to import.")?;
141        }
142
143        Ok(())
144    }
145}
146
147impl ImportStat {
148    fn is_empty(&self) -> bool {
149        self.created == 0 && self.updated == 0 && self.failed == 0
150    }
151}
152
153impl fmt::Display for ImportStat {
154    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
155        let mut first = true;
156
157        if self.created != 0 {
158            write!(f, "{} created", self.created)?;
159            first = false;
160        }
161
162        if self.updated != 0 {
163            if !first {
164                write!(f, ", ")?;
165            }
166            write!(f, "{} updated", self.updated)?;
167            first = false;
168        }
169
170        if self.failed != 0 {
171            if !first {
172                write!(f, ", ")?;
173            }
174            write!(f, "{} failed", self.failed)?;
175        }
176
177        Ok(())
178    }
179}