tauri_plugin_ota_self_update/
lib.rs1use std::{
2 borrow::Cow,
3 cell::OnceCell,
4 collections::HashMap,
5 sync::{Arc, Mutex},
6};
7
8use serde::{Deserialize, Deserializer};
9use tauri::{
10 plugin::{Builder, TauriPlugin},
11 utils::assets::{AssetKey, CspHash},
12 App, Assets, Context, Manager, Runtime,
13};
14
15pub use models::*;
16
17mod commands;
18mod core;
19mod error;
20mod models;
21mod runtime;
22
23pub use error::{Error, Result};
24
25use runtime::OtaSelfUpdate;
26
27const DEFAULT_CHANNEL: &str = "stable";
28
29#[derive(Clone, Deserialize)]
30#[serde(rename_all = "camelCase")]
31pub struct Config {
32 pub base_url: String,
33 pub pubkey: String,
34 #[serde(default, deserialize_with = "channel_deserializer")]
35 pub channel: Option<String>,
36 #[serde(default)]
37 pub request_headers: HashMap<String, String>,
38 #[serde(default)]
39 pub timeout_secs: Option<u64>,
40 #[serde(default)]
41 pub activation_policy: ActivationPolicy,
42}
43
44fn channel_deserializer<'de, D>(deserializer: D) -> std::result::Result<Option<String>, D::Error>
45where
46 D: Deserializer<'de>,
47{
48 let s = Option::<String>::deserialize(deserializer)?;
49 Ok(match s {
50 Some(channel) if channel == DEFAULT_CHANNEL => None,
51 Some(channel) if channel.is_empty() => None,
52 other => other,
53 })
54}
55
56pub trait OtaSelfUpdateExt<R: Runtime> {
58 fn ota_self_update(&self) -> &OtaSelfUpdate<R>;
59}
60
61impl<R: Runtime, T: Manager<R>> crate::OtaSelfUpdateExt<R> for T {
62 fn ota_self_update(&self) -> &OtaSelfUpdate<R> {
63 self.state::<OtaSelfUpdate<R>>().inner()
64 }
65}
66
67pub(crate) struct OtaAssets<R: Runtime> {
68 overlay_assets: Arc<Mutex<HashMap<AssetKey, Vec<u8>>>>,
69 embedded_assets: OnceCell<Box<dyn Assets<R>>>,
70 csp_hashes: Vec<CspHash<'static>>,
71}
72
73unsafe impl<R: Runtime> Sync for OtaAssets<R> {}
74
75impl<R: Runtime> Assets<R> for OtaAssets<R> {
76 fn setup(&self, app: &App<R>) {
77 let ota = app.state::<OtaSelfUpdate<R>>();
78 self.embedded_assets.get_or_init(|| {
79 let assets = ota.embedded_assets.lock().unwrap().take().unwrap();
80 assets.setup(app);
81 assets
82 });
83 }
84
85 fn csp_hashes(&self, _html_path: &AssetKey) -> Box<dyn Iterator<Item = CspHash<'_>> + '_> {
86 Box::new(self.csp_hashes.iter().copied())
87 }
88
89 fn get(&self, key: &AssetKey) -> Option<Cow<'_, [u8]>> {
90 self
91 .overlay_assets
92 .lock()
93 .unwrap()
94 .get(key)
95 .map(|bytes| Cow::Owned(bytes.clone()))
96 .or_else(|| self.embedded_assets.get().unwrap().get(key))
97 }
98
99 fn iter(&self) -> Box<dyn Iterator<Item = (Cow<'_, str>, Cow<'_, [u8]>)> + '_> {
100 Box::new(
101 self
102 .overlay_assets
103 .lock()
104 .unwrap()
105 .clone()
106 .into_iter()
107 .map(|(k, v)| (Cow::Owned(k.as_ref().to_string()), Cow::Owned(v))),
108 )
109 }
110}
111
112pub fn init<R: Runtime>(context: Context<R>) -> (TauriPlugin<R, Config>, Context<R>) {
113 let overlay_assets = Arc::new(Mutex::new(HashMap::<AssetKey, Vec<u8>>::new()));
114 let mut context = context;
115 let embedded_assets = context.set_assets(Box::new(OtaAssets {
116 overlay_assets: overlay_assets.clone(),
117 embedded_assets: Default::default(),
118 csp_hashes: Default::default(),
119 }));
120
121 let plugin = Builder::<R, Config>::new("ota-self-update")
122 .invoke_handler(tauri::generate_handler![
123 commands::check_for_updates,
124 commands::apply_update,
125 commands::set_channel,
126 commands::get_current_version
127 ])
128 .setup(move |app, api| {
129 let ota_self_update = runtime::init(app, api, overlay_assets.clone(), embedded_assets)?;
130 app.manage(ota_self_update);
131 Ok(())
132 })
133 .build();
134 (plugin, context)
135}