initiative_core/storage/
backup.rs1use 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}