phemios_core/
personal_backup.rs1use crate::{article_store, federation::Source, network::Client, podcast_store};
3use serde_json::{Value, json};
4const LIMIT: usize = 12 * 1024 * 1024;
5fn fields(value: &Value, allowed: &[&str]) -> Result<(), String> {
6 let object = value
7 .as_object()
8 .ok_or("Sauvegarde des préférences invalide.")?;
9 if object.len() != allowed.len() || object.keys().any(|key| !allowed.contains(&key.as_str())) {
10 return Err("Sauvegarde des préférences invalide.".into());
11 }
12 Ok(())
13}
14fn text(value: &Value, max: usize) -> bool {
15 value.as_str().is_some_and(|s| {
16 !s.trim().is_empty() && s.encode_utf16().count() <= max && !s.chars().any(char::is_control)
17 })
18}
19pub fn validate(payload: &Value) -> Result<(), String> {
21 fields(payload, &["origin", "podcasts", "articles"])?;
22 let origin = &payload["origin"];
23 fields(origin, &["source", "authority", "accountId", "name"])?;
24 if !matches!(origin["source"].as_str(), Some("local" | "server"))
25 || !text(&origin["authority"], 4096)
26 || !text(&origin["accountId"], 256)
27 || !text(&origin["name"], 256)
28 || (origin["source"] == "local" && origin["authority"] != "phemios-managed-local")
29 {
30 return Err("Sauvegarde des préférences invalide.".into());
31 }
32 fields(&payload["podcasts"], &["feeds", "episodes"])?;
33 podcast_store::validate(&payload["podcasts"])?;
34 article_store::validate(&json!({"articles":payload["articles"]}))?;
35 if serde_json::to_vec(payload)
36 .map_err(|_| "Sauvegarde des préférences invalide.")?
37 .len()
38 > LIMIT
39 {
40 return Err("Sauvegarde des préférences invalide.".into());
41 }
42 Ok(())
43}
44pub fn read(
46 podcasts: &podcast_store::Store,
47 articles: &article_store::Store,
48 source: Source,
49 client: &Client,
50) -> Result<Value, String> {
51 let account = client.account();
52 let id = account["account"]["id"]
53 .as_str()
54 .ok_or("Compte privé invalide.")?;
55 let name = account["account"]["username"].as_str().unwrap_or(id);
56 let authority = if source == Source::Local {
57 "phemios-managed-local"
58 } else {
59 account["server"].as_str().ok_or("Compte privé invalide.")?
60 };
61 let podcast = podcasts.read(source, client)?;
62 let article = articles.read(source, client)?;
63 let value = json!({"origin":{"source":source.name(),"authority":authority,"accountId":id,"name":name},"podcasts":podcast,"articles":article["articles"]});
64 validate(&value)?;
65 Ok(value)
66}
67pub fn restore(
71 podcasts: &podcast_store::Store,
72 articles: &article_store::Store,
73 source: Source,
74 client: &Client,
75 payload: &Value,
76) -> Result<(), String> {
77 validate(payload)?;
78 let podcast = podcasts.prepare_restore(source, client, &payload["podcasts"])?;
79 let article =
80 articles.prepare_restore(source, client, &json!({"articles":payload["articles"]}))?;
81 podcasts.commit_restore(source, client, podcast)?;
82 articles.commit_restore(source,client,article).map_err(|_| "Restauration interrompue. Les données existantes sont conservées ; réessayez l’import pour terminer.".to_owned())
83}
84#[cfg(test)]
85#[path = "personal_backup_tests.rs"]
86mod tests;