diff --git a/lektor_nkdb/src/database/epoch.rs b/lektor_nkdb/src/database/epoch.rs index c09db611feea06b8f69d05d273a3cc88ea580b34..36c63d2b3f4e7216ca02da0509d0cd341e0b9237 100644 --- a/lektor_nkdb/src/database/epoch.rs +++ b/lektor_nkdb/src/database/epoch.rs @@ -45,6 +45,11 @@ impl Epoch { self.0.get(&id) } + /// Returns whever the [Epoch] contains the [KId] or not. + pub fn contains(&self, id: &KId) -> bool { + self.0.contains_key(id) + } + /// Get access to the karas in the epoch. pub fn data(&self) -> &EpochData { &self.0 diff --git a/lektor_nkdb/src/database/pool.rs b/lektor_nkdb/src/database/pool.rs index ac76b200b3d8182bb57d92e94b74d6153bc9a060..e3a975da6df42d8a9b9870659a8e76f9fbdcfa3c 100644 --- a/lektor_nkdb/src/database/pool.rs +++ b/lektor_nkdb/src/database/pool.rs @@ -28,16 +28,27 @@ impl Pool { // Compress things if needed! let this = Self::default(); let vec = futures::future::join_all(iter.into_iter().map(|(kid, rkid)| async { - ( - this.get_str::<KId>(kid.0).await, - this.get_str::<RemoteKId>(rkid.0).await, - ) + let kid = this.get_str::<KId>(kid.0).await; + let rkid = this.get_str::<RemoteKId>(rkid.0).await; + (kid, rkid) })) .await; let _ = std::mem::replace(&mut (*this.id_mapping.write().await), vec); this } + /// Get other possible ids from an id, we may want to do that to get all the local ids for a + /// remote id, to get a more up-to-date version of a kara. + pub(crate) async fn get_other_locals(&self, id: KId) -> Vec<KId> { + let mapping = self.id_mapping.read().await; + + let find_remote = |(kid, rkid): &_| id.eq(kid).then_some(rkid).cloned(); + let remote = mapping.iter().find_map(find_remote).expect("no remote id"); + + let find_locals = |(kid, rkid): &(KId, _)| remote.eq(rkid).then_some(kid.clone()); + mapping.iter().filter_map(find_locals).collect() + } + /// Get the tuple (local, remote) from the string representation of the remote id. Id the local /// id is not present, returns none, else some(local_id). pub(crate) async fn get_from_remote(&self, rkid: impl AsRef<str>) -> (Option<KId>, RemoteKId) { @@ -77,6 +88,14 @@ impl Pool { .into() } + /// Same as [Pool::get_str], but don't create the id if not present to avoid being DOSed... + pub(crate) async fn try_get_str<I: sealed::Id>(&self, id: impl AsRef<str>) -> Option<I> { + let mut hash = DefaultHasher::new(); + hash.write(id.as_ref().as_bytes()); + let this = self.string_cache.write().await; + this.get(&hash.finish()).cloned().map(Into::into) + } + /// Get the maximal id present in the pool. pub(crate) async fn maximal_id(&self) -> u64 { let list = self.id_mapping.read().await; diff --git a/lektor_nkdb/src/lib.rs b/lektor_nkdb/src/lib.rs index e5690d8e71d96db53d3f4df8a4bb15fc98f51dd7..f501a0028196217e114fe6976c72ebb19ab58438 100644 --- a/lektor_nkdb/src/lib.rs +++ b/lektor_nkdb/src/lib.rs @@ -113,11 +113,34 @@ impl<Storage: DatabaseStorage> Database<Storage> { .await } + /// Refresh the [KId]s of the playlists to use a most up-to-date version of a kara if + /// available. Should be called after a successfull update process. In case of failure we log + /// the error and do nothing else... + pub async fn refresh_playlist_contents(&self) { + let Some(epoch) = self.last_epoch().await else { + log::error!("no epoch in database, can't refresh playlists"); + return; + }; + for name in self.playlists.list_names().await { + log::info!("refresh ids for playlist {name}"); + let ids = stream::iter(self.playlists.get_content(&name).await.unwrap_or_default()) + .then(|id| async { + let locals = self.pool.get_other_locals(id).await; + locals.into_iter().filter(|id| epoch.contains(id)) + }) + .collect::<FuturesUnordered<_>>(); + self.playlists + .refresh(&name, ids.await.into_iter().flatten().collect::<Vec<KId>>()) + .await + .map_err(|err| log::error!("failed to refresh ids of playlist {name}: {err}")) + .unwrap_or_default(); + } + } + /// Get the [KId] out of its string representation. We query the pool to guaranty the pointer /// unicity for faster checks. - pub async fn get_kid_from_str(&self, kid: impl AsRef<str>) -> KId { - log::error!("find a way to get the str only if it exists..."); - self.pool.get_str(kid).await + pub async fn get_kid_from_str(&self, kid: impl AsRef<str>) -> Option<KId> { + self.pool.try_get_str(kid).await } /// Get a [Kara] by its [u64] representation in the last epoch. diff --git a/lektor_nkdb/src/playlist/register.rs b/lektor_nkdb/src/playlist/register.rs index 68b31b4761b2e2fa07b4c8e4b98c70941c03500a..a85aaaf8de2ebad81e79ace87dcc549d55eea699 100644 --- a/lektor_nkdb/src/playlist/register.rs +++ b/lektor_nkdb/src/playlist/register.rs @@ -2,7 +2,7 @@ //! change. use crate::{DatabaseStorage, KId, Playlist, PlaylistInfo, PlaylistName}; -use anyhow::{bail, Result}; +use anyhow::{anyhow, bail, Result}; use hashbrown::{hash_map::OccupiedEntry, HashMap}; use lektor_utils::log; use rand::{seq::SliceRandom, thread_rng}; @@ -176,6 +176,25 @@ impl_playlists! { self.0.iter().map(|(name, plt)| (name.clone(), plt.deref().clone())).collect() } + /// Get the list of all the playlists. + read fn list_names(&self) -> Vec<PlaylistName> { + self.0.iter().map(|(name, _)| name.clone()).collect() + } + + /// Refresh some ids of a playlist with the new ones. Here we skip the user check thing as this + /// function should not be called by a user but as part of the update process. + write fn refresh(&self, name: impl AsRef<str>, ids: Vec<KId>) -> Result<()> { + let (mut ids, name) = (ids, name.as_ref()); + let plt = self.0.get_mut(name).ok_or_else(|| anyhow!("no playlist {name} to refresh"))?; + ids.retain(|id| !plt.content.contains(id)); + if ids.is_empty() { + log::warn!("no ids to refresh the playlist {name} with..."); + } else { + plt.content.append(&mut ids); + } + Ok(()) + } + /// Rename a playlist. write fn rename(&self, name: PlaylistName, new_name: PlaylistName, user: impl AsRef<str>, admin: bool) -> Result<()> { if self.0.contains_key(&new_name) { diff --git a/lektord/src/app/routes.rs b/lektord/src/app/routes.rs index fca68aefac73fc7db22f9a0a5a627d0728efe29d..e95260b2866bbf9d41a04760445817f178c1204d 100644 --- a/lektord/src/app/routes.rs +++ b/lektord/src/app/routes.rs @@ -116,8 +116,10 @@ pub(crate) async fn get_kara_by_kid( Path(id): Path<String>, ) -> Result<Json<Kara>, LektordError> { log::error!("find a way to not clone the kara"); - let id = state.database.get_kid_from_str(&decode_base64(id)?).await; - Ok(Json(state.database.get_kara_by_kid(id).await?.clone())) + match state.database.get_kid_from_str(&decode_base64(&id)?).await { + Some(id) => Ok(Json(state.database.get_kara_by_kid(id).await?.clone())), + None => Err(anyhow!("no kara found with id {id}").into()), + } } /// Search some karas from a search set with a regex. We take the json argument as base64 encoded @@ -263,17 +265,15 @@ pub(crate) async fn adm_update( .expect("failed to build tokio runtime to update database from repo"); let local = LocalSet::new(); local.spawn_local(async move { - match tokio::task::spawn_local(async move { + let res = tokio::task::spawn_local(async move { let handle = state.database.update().await; let count = state.repo.update_with(&handle).await?; handle.finished().await; + state.database.refresh_playlist_contents().await; Ok::<_, LektordError>(count) - }) - .await - { - Ok(Ok(count)) => { - log::info!("finished updating database, toto downloaded karas: {count}") - } + }); + match res.await { + Ok(Ok(count)) => log::info!("finished updating database with karas: {count}"), Ok(Err(err)) => log::error!("{err}"), Err(err) => log::error!("{err}"), }