Add basic support for store plugin query strings

This commit is contained in:
NGnius (Graham) 2023-12-30 20:43:05 -05:00
parent fbfcd89880
commit 512e03d9c7
9 changed files with 573 additions and 671 deletions

1177
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -29,8 +29,3 @@ chrono = { version = "0.4" }
# cli # cli
clap = { version = "4.0", features = ["derive"] } clap = { version = "4.0", features = ["derive"] }
[workspace]
include = [
"decky_api"
]

View file

@ -9,7 +9,7 @@ pub struct CliArgs {
/// Cache results for a period /// Cache results for a period
#[arg(name = "cache", long)] #[arg(name = "cache", long)]
pub cache_duration: Option<i64>, pub cache_duration: Option<i64>,
/// Local server port (default: 222252) /// Local server port (default: 22252)
#[arg(name = "port", short, long)] #[arg(name = "port", short, long)]
pub server_port: Option<u16>, pub server_port: Option<u16>,
/// Storage adapter /// Storage adapter

View file

@ -5,7 +5,10 @@ use actix_web::{get, web, Responder};
use crate::storage::IStorage; use crate::storage::IStorage;
#[get("/plugins")] #[get("/plugins")]
pub async fn decky_plugins(data: actix_web::web::Data<Box<dyn IStorage>>) -> impl Responder { pub async fn decky_plugins(req: actix_web::HttpRequest, data: actix_web::web::Data<Box<dyn IStorage>>) -> impl Responder {
let plugins: StorePluginList = web::block(move || data.plugins()).await.unwrap(); let query_string = req.query_string().to_owned();
log::debug!("Got request with uri {}", req.uri());
let plugins: StorePluginList = web::block(move || data.plugins(&query_string)).await.unwrap();
log::debug!("Got {} plugin results", plugins.len());
web::Json(plugins) web::Json(plugins)
} }

View file

@ -49,27 +49,43 @@ impl<T: Clone> Cached<T> {
pub struct CachedStorage<S: AsRef<dyn IStorage> + Send + Sync> { pub struct CachedStorage<S: AsRef<dyn IStorage> + Send + Sync> {
fallback: S, fallback: S,
plugins_cache: Cached<StorePluginList>, plugins_cache: RwLock<HashMap<String, Cached<StorePluginList>>>,
statistics_cache: Cached<HashMap<String, u64>>, statistics_cache: Cached<HashMap<String, u64>>,
artifacts_cache: Cached<HashMap<String, Bytes>>, artifacts_cache: Cached<HashMap<String, Bytes>>,
images_cache: Cached<HashMap<String, Bytes>>, images_cache: Cached<HashMap<String, Bytes>>,
ttl: i64,
} }
impl<S: AsRef<dyn IStorage> + Send + Sync> CachedStorage<S> { impl<S: AsRef<dyn IStorage> + Send + Sync> CachedStorage<S> {
pub fn new(duration: i64, inner: S) -> Self { pub fn new(duration: i64, inner: S) -> Self {
Self { Self {
plugins_cache: Cached::new(inner.as_ref().plugins(), duration), plugins_cache: RwLock::new(HashMap::new()),
statistics_cache: Cached::new(inner.as_ref().get_statistics(), duration), statistics_cache: Cached::new(inner.as_ref().get_statistics(), duration),
artifacts_cache: Cached::new(HashMap::new(), duration), artifacts_cache: Cached::new(HashMap::new(), duration),
images_cache: Cached::new(HashMap::new(), duration), images_cache: Cached::new(HashMap::new(), duration),
fallback: inner, fallback: inner,
ttl: duration
} }
} }
} }
impl<S: AsRef<dyn IStorage> + Send + Sync> IStorage for CachedStorage<S> { impl<S: AsRef<dyn IStorage> + Send + Sync> IStorage for CachedStorage<S> {
fn plugins(&self) -> StorePluginList { fn plugins(&self, query: &str) -> StorePluginList {
self.plugins_cache.get(|| self.fallback.as_ref().plugins()) let is_already_cached = self.plugins_cache.read().expect("Failed to acquire plugins_cache read lock")
.contains_key(query);
if is_already_cached
{
let lock = self.plugins_cache.read()
.expect("Failed to acquire plugins_cache read lock");
let cached_result = lock.get(query)
.unwrap(); // cannot fail (already checked that exists, and existing entries are never removed)
cached_result.get(|| self.fallback.as_ref().plugins(query))
} else {
let result_to_cache = self.fallback.as_ref().plugins(query);
let mut lock = self.plugins_cache.write().expect("Failed to acquire plugins_cache write lock");
lock.insert(query.to_owned(), Cached::new(result_to_cache.clone(), self.ttl));
result_to_cache
}
} }
fn get_artifact(&self, name: &str, version: &str, hash: &str) -> Result<bytes::Bytes, std::io::Error> { fn get_artifact(&self, name: &str, version: &str, hash: &str) -> Result<bytes::Bytes, std::io::Error> {

View file

@ -114,7 +114,7 @@ impl FileStorage {
let extension = entry_path.extension().unwrap().to_string_lossy().into_owned(); let extension = entry_path.extension().unwrap().to_string_lossy().into_owned();
if extension == "zip" { if extension == "zip" {
let version_name = entry_path.file_stem().unwrap().to_string_lossy().into_owned(); let version_name = entry_path.file_stem().unwrap().to_string_lossy().into_owned();
let hash_str = sha256::try_digest(entry_path.as_ref())?; let hash_str = sha256::try_digest(&entry_path)?;
let artifact_url = format!("{}/plugins/{}/{}/{}.zip", self.domain_root, plugin_name, version_name, hash_str); let artifact_url = format!("{}/plugins/{}/{}/{}.zip", self.domain_root, plugin_name, version_name, hash_str);
versions.push(StorePluginVersion { versions.push(StorePluginVersion {
name: version_name, name: version_name,
@ -137,7 +137,8 @@ impl FileStorage {
} }
impl IStorage for FileStorage { impl IStorage for FileStorage {
fn plugins(&self) -> StorePluginList { fn plugins(&self, _query: &str) -> StorePluginList {
// TODO handle query string
match self.read_all_plugins() { match self.read_all_plugins() {
Err(e) => { Err(e) => {
log::error!("Plugins read error: {}", e); log::error!("Plugins read error: {}", e);

View file

@ -1,5 +1,5 @@
pub trait IStorage: Send + Sync { pub trait IStorage: Send + Sync {
fn plugins(&self) -> decky_api::StorePluginList; fn plugins(&self, query: &str) -> decky_api::StorePluginList;
fn get_artifact(&self, _name: &str, _version: &str, _hash: &str) -> Result<bytes::Bytes, std::io::Error> { fn get_artifact(&self, _name: &str, _version: &str, _hash: &str) -> Result<bytes::Bytes, std::io::Error> {
Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Artifact downloading not supported")) Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Artifact downloading not supported"))
@ -17,7 +17,7 @@ pub trait IStorage: Send + Sync {
pub struct EmptyStorage; pub struct EmptyStorage;
impl IStorage for EmptyStorage { impl IStorage for EmptyStorage {
fn plugins(&self) -> decky_api::StorePluginList { fn plugins(&self, _query: &str) -> decky_api::StorePluginList {
Vec::new() Vec::new()
} }
} }

View file

@ -71,14 +71,15 @@ impl<S: AsRef<dyn IStorage> + Send + Sync> MergedStorage<S> {
} }
impl<S: AsRef<dyn IStorage> + Send + Sync> IStorage for MergedStorage<S> { impl<S: AsRef<dyn IStorage> + Send + Sync> IStorage for MergedStorage<S> {
fn plugins(&self) -> StorePluginList { fn plugins(&self, query: &str) -> StorePluginList {
let mut merged_plugins = HashMap::new(); let mut merged_plugins = HashMap::new();
log::debug!("Acquiring store map write locks"); log::debug!("Acquiring store map write locks");
let mut arti_lock = self.store_artifact_map.write().expect("Failed to acquire store_artifact_map write lock"); let mut arti_lock = self.store_artifact_map.write().expect("Failed to acquire store_artifact_map write lock");
let mut img_lock = self.store_image_map.write().expect("Failed to acquire store_image_map write lock"); let mut img_lock = self.store_image_map.write().expect("Failed to acquire store_image_map write lock");
for (index, store) in self.stores.iter().enumerate() { for (index, store) in self.stores.iter().enumerate() {
let plugins = store.as_ref().plugins(); log::debug!("Handling store #{}", index);
// re-build store mappins let plugins = store.as_ref().plugins(query);
// re-build store mappings
for plugin in &plugins { for plugin in &plugins {
for version in &plugin.versions { for version in &plugin.versions {
let hashable_ver = HashablePluginVersion { let hashable_ver = HashablePluginVersion {
@ -96,6 +97,7 @@ impl<S: AsRef<dyn IStorage> + Send + Sync> IStorage for MergedStorage<S> {
} }
} }
Self::merge_plugins_into(&mut merged_plugins, plugins); Self::merge_plugins_into(&mut merged_plugins, plugins);
log::debug!("Completed store #{}", index);
} }
Self::map_to_vec(merged_plugins) Self::map_to_vec(merged_plugins)
} }

View file

@ -17,16 +17,16 @@ impl ProxiedStorage {
} }
} }
fn plugins_url(&self) -> String { fn plugins_url(&self, query: &str) -> String {
format!("{}/plugins", self.store_url) format!("{}/plugins?{}", self.store_url, query)
} }
fn default_artifact_url(ver: &StorePluginVersion) -> String { fn default_artifact_url(ver: &StorePluginVersion) -> String {
format!("https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/versions/{}.zip", ver.hash) format!("https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/versions/{}.zip", ver.hash)
} }
fn proxy_plugins(&self) -> StorePluginList { fn proxy_plugins(&self, query: &str) -> StorePluginList {
let url = self.plugins_url(); let url = self.plugins_url(query);
match self.agent.get(&url).call() { match self.agent.get(&url).call() {
Err(e) => { Err(e) => {
log::error!("Plugins proxy error for {}: {}", url, e); log::error!("Plugins proxy error for {}: {}", url, e);
@ -46,8 +46,8 @@ impl ProxiedStorage {
} }
impl IStorage for ProxiedStorage { impl IStorage for ProxiedStorage {
fn plugins(&self) -> StorePluginList { fn plugins(&self, query: &str) -> StorePluginList {
let mut proxy = self.proxy_plugins(); let mut proxy = self.proxy_plugins(query);
for plugin in &mut proxy { for plugin in &mut proxy {
for version in &mut plugin.versions { for version in &mut plugin.versions {
if version.artifact.is_none() { if version.artifact.is_none() {