tauri_plugin_persistence/
desktop.rs

1use std::{collections::HashMap, str::FromStr, sync::Arc};
2
3use serde::{de::DeserializeOwned, Serialize};
4use tauri::{plugin::PluginApi, AppHandle, Manager, Runtime, State};
5use tokio::sync::Mutex;
6
7use crate::{api::types::{CollectionSpecifier, ContextSpecifier, DatabaseSpecifier, FileHandleSpecifier}, state::{ContextState, PluginState}};
8
9pub fn init<R: Runtime, C: DeserializeOwned>(
10  app: &AppHandle<R>,
11  _api: PluginApi<R, C>,
12) -> crate::Result<Persistence<R>> {
13  Ok(Persistence(app.clone()))
14}
15
16/// Access to the persistence APIs.
17pub struct Persistence<R: Runtime>(AppHandle<R>);
18
19impl<R: Runtime> Persistence<R> {
20    fn contexts(&self) -> State<'_, PluginState> {
21        self.0.state::<PluginState>().clone()
22    }
23
24    fn handle(&self) -> AppHandle<R> {
25        self.0.clone()
26    }
27
28    async fn context_ids(&self) -> Vec<String> {
29        let ctx = self.contexts();
30        let contexts = ctx.lock().await;
31        contexts.keys().map(|k| k.clone()).collect()
32    }
33
34    /// Opens a context at a path, or returns the existing context if it's already open at that path.
35    /// Attmepting to open an existing context at a new path will fail.
36    pub async fn open_context(&self, name: impl AsRef<str>, path: impl AsRef<str>) -> crate::Result<crate::Context<R>> {
37        let ctx = self.contexts();
38        let resolved_path = std::path::PathBuf::from_str(path.as_ref()).or(Err(crate::Error::invalid_path(path.as_ref())))?;
39        let mut contexts = ctx.lock().await;
40
41        if let Some(ctx) = contexts.get(&name.as_ref().to_string()) {
42            if ctx.root_path == path.as_ref().to_string() {
43                Ok(crate::Context::<R>::create(self.handle(), name.as_ref().to_string(), path.as_ref().to_string()))
44            } else {
45                Err(crate::Error::open_context(name, path, "Context already open at a different path."))
46            }
47        } else {
48            if resolved_path.exists() {
49                if resolved_path.is_dir() {
50                    let _ = contexts.insert(name.as_ref().to_string(), ContextState {name: name.as_ref().to_string(), root_path: path.as_ref().to_string(), databases: Arc::new(Mutex::new(HashMap::new())), files: Arc::new(Mutex::new(HashMap::new()))});
51                    Ok(crate::Context::<R>::create(self.handle(), name.as_ref().to_string(), path.as_ref().to_string()))
52                } else {
53                    Err(crate::Error::open_context(name, path, "Specified path is not a directory."))
54                }
55            } else {
56                tokio::fs::create_dir_all(resolved_path).await.or_else(|e| Err(crate::Error::open_context(name.as_ref(), path.as_ref(), format!("Failed to create context directory: {e:?}"))))?;
57                let _ = contexts.insert(name.as_ref().to_string(), ContextState {name: name.as_ref().to_string(), root_path: path.as_ref().to_string(), databases: Arc::new(Mutex::new(HashMap::new())), files: Arc::new(Mutex::new(HashMap::new()))});
58                Ok(crate::Context::<R>::create(self.handle(), name.as_ref().to_string(), path.as_ref().to_string()))
59            }
60        }
61    }
62
63    /// Returns an already-open context
64    pub async fn aliased_context(&self, name: impl AsRef<str>) -> crate::Result<crate::Context<R>> {
65        if let Some(ctx) = self.contexts().lock().await.get(&name.as_ref().to_string()) {
66            Ok(crate::Context::<R>::create(self.handle(), name.as_ref().to_string(), ctx.root_path.clone()))
67        } else {
68            Err(crate::Error::unknown_context(name))
69        }
70    }
71
72    /// Returns a context based on a [ContextSpecifier]. This abstracts [Persistence::open_context] and [Persistence::aliased_context]
73    pub async fn context(&self, context: ContextSpecifier) -> crate::Result<crate::Context<R>> {
74        match context {
75            ContextSpecifier::Aliased { alias } => self.aliased_context(alias).await,
76            ContextSpecifier::Direct { alias, path } => self.open_context(alias, path).await
77        }
78    }
79
80    /// Returns a database based on a [ContextSpecifier] and a [DatabaseSpecifier]
81    pub async fn database(&self, context: ContextSpecifier, database: DatabaseSpecifier) -> crate::Result<crate::Database<R>> {
82        let context = self.context(context).await?;
83        match database {
84            DatabaseSpecifier::Aliased { alias } => context.database(alias).await,
85            DatabaseSpecifier::Direct { alias, path } => context.open_database(alias, path).await
86        }
87    }
88
89    /// Returns a file handle based on a [ContextSpecifier] and a [FileHandleSpecifier]
90    pub async fn file_handle(&self, context: ContextSpecifier, file_handle: FileHandleSpecifier) -> crate::Result<crate::FileHandle<R>> {
91        let context = self.context(context).await?;
92        match file_handle {
93            FileHandleSpecifier::Aliased { id } => context.file_handle(id).await,
94            FileHandleSpecifier::Direct { path, mode } => context.open_file_handle(path, mode).await
95        }
96    }
97
98    /// Returns an open transaction based on a [ContextSpecifier] and a [DatabaseSpecifier], as well as the transaction ID
99    pub async fn transaction(&self, context: ContextSpecifier, database: DatabaseSpecifier, transaction: bson::Uuid) -> crate::Result<crate::Transaction<R>> {
100        let database = self.database(context, database).await?;
101        database.get_transaction(transaction).await
102    }
103
104    /// Returns a collection based on a [ContextSpecifier], [DatabaseSpecifier], and a [CollectionSpecifier]
105    pub async fn collection<T: Serialize + DeserializeOwned + Sync + Send>(&self, context: ContextSpecifier, database: DatabaseSpecifier, collection: CollectionSpecifier) -> crate::Result<crate::Collection<T, R>> {
106        let database = self.database(context, database).await?;
107        match collection {
108            CollectionSpecifier::Global { name } => Ok(database.collection(name).await),
109            CollectionSpecifier::Transaction { transaction, name } => {
110                let trn = database.get_transaction(transaction).await?;
111                Ok(trn.collection(name))
112            }
113        }
114    }
115
116    /// Closes a context based on a [ContextSpecifier]
117    pub async fn close_context(&self, context: ContextSpecifier) -> crate::Result<()> {
118        let ctx = match context {
119            ContextSpecifier::Aliased { alias } => self.aliased_context(alias).await,
120            ContextSpecifier::Direct { alias, path } => self.open_context(alias, path).await
121        }?;
122        let cid = ctx.name();
123        ctx.close().await?;
124        let ctxs = self.contexts();
125        let mut contexts = ctxs.lock().await;
126        let _ = contexts.remove(&cid);
127        Ok(())
128    }
129
130    /// Closes all active contexts and removes them from tracking.
131    pub async fn cleanup(&self) -> crate::Result<()> {
132        for context in self.context_ids().await {
133            self.close_context(ContextSpecifier::Aliased { alias: context }).await?;
134        }
135
136        Ok(())
137    }
138}