Compare commits

...

3 commits

19 changed files with 1186 additions and 691 deletions

2
.gitignore vendored
View file

@ -1,3 +1,5 @@
/target /target
**/target
/store /store
not-decky-* not-decky-*
/*.sh

1184
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,14 +1,15 @@
[package] [package]
name = "not-decky-store" name = "not-decky-store"
version = "0.1.0" version = "0.2.0"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
decky_api = { version = "0.1.0", path = "./decky_api" } decky_api = { version = "0.2.0", path = "./decky_api" }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0" } serde_json = { version = "1.0" }
serde_urlencoded = "0.7"
bytes = "1.3" bytes = "1.3"
sha256 = "1.1" sha256 = "1.1"
@ -29,8 +30,3 @@ chrono = { version = "0.4" }
# cli # cli
clap = { version = "4.0", features = ["derive"] } clap = { version = "4.0", features = ["derive"] }
[workspace]
include = [
"decky_api"
]

365
decky_api/Cargo.lock generated Normal file
View file

@ -0,0 +1,365 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "bumpalo"
version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
[[package]]
name = "cc"
version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
dependencies = [
"libc",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-targets 0.48.5",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
[[package]]
name = "decky_api"
version = "0.2.0"
dependencies = [
"chrono",
"serde",
]
[[package]]
name = "iana-time-zone"
version = "0.1.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "js-sys"
version = "0.3.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.151"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4"
[[package]]
name = "log"
version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
[[package]]
name = "num-traits"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "proc-macro2"
version = "1.0.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a293318316cf6478ec1ad2a21c49390a8d5b5eae9fab736467d93fbc0edc29c5"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
dependencies = [
"proc-macro2",
]
[[package]]
name = "serde"
version = "1.0.193"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.193"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "syn"
version = "2.0.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee659fb5f3d355364e1f3e5bc10fb82068efbf824a1e9d1c9504244a6469ad53"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "wasm-bindgen"
version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f"
[[package]]
name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets 0.52.0",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]]
name = "windows-targets"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd"
dependencies = [
"windows_aarch64_gnullvm 0.52.0",
"windows_aarch64_msvc 0.52.0",
"windows_i686_gnu 0.52.0",
"windows_i686_msvc 0.52.0",
"windows_x86_64_gnu 0.52.0",
"windows_x86_64_gnullvm 0.52.0",
"windows_x86_64_msvc 0.52.0",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"

View file

@ -1,9 +1,10 @@
[package] [package]
name = "decky_api" name = "decky_api"
version = "0.1.0" version = "0.2.0"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
chrono = { version = "0.4", features = ["serde"] }

View file

@ -1,3 +1,3 @@
mod store_plugin; mod store_plugin;
pub use store_plugin::{StorePlugin, StorePluginVersion, StorePluginList}; pub use store_plugin::{StorePlugin, StorePluginVersion, StorePluginList, StorePluginQuery, StorePluginQuerySortColumn, StorePluginQuerySortDirection, StorePluginIncrement};

View file

@ -11,6 +11,10 @@ pub struct StorePlugin {
pub description: String, pub description: String,
pub tags: Vec<String>, pub tags: Vec<String>,
pub image_url: String, pub image_url: String,
pub downloads: Option<u64>,
pub updates: Option<u64>,
pub created: Option<chrono::DateTime<chrono::Utc>>,
pub updated: Option<chrono::DateTime<chrono::Utc>>,
} }
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
@ -18,4 +22,37 @@ pub struct StorePluginVersion {
pub name: String, pub name: String,
pub hash: String, pub hash: String,
pub artifact: Option<String>, pub artifact: Option<String>,
pub created: Option<chrono::DateTime<chrono::Utc>>,
pub downloads: Option<u64>,
pub updates: Option<u64>,
}
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct StorePluginQuery {
pub sort_by: Option<StorePluginQuerySortColumn>,
pub sort_direction: Option<StorePluginQuerySortDirection>,
}
#[derive(Serialize, Deserialize, Clone)]
pub enum StorePluginQuerySortColumn {
#[serde(rename = "name")]
Name,
#[serde(rename = "date")]
Date,
#[serde(rename = "downloads")]
Downloads
}
#[derive(Serialize, Deserialize, Clone)]
pub enum StorePluginQuerySortDirection {
#[serde(rename = "asc")]
Ascending,
#[serde(rename = "desc")]
Descending,
}
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct StorePluginIncrement {
#[serde(rename = "isUpdate")]
pub is_update: bool
} }

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

@ -42,8 +42,12 @@ fn build_storage_box(storage: &cli::StorageArgs) -> Box<dyn storage::IStorage> {
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
let args = cli::CliArgs::get(); let args = cli::CliArgs::get();
let log_filepath = std::path::Path::new("/tmp").join(format!("{}.log", consts::PACKAGE_NAME)); let log_filepath = std::path::Path::new("/tmp").join(format!("{}.log", consts::PACKAGE_NAME));
#[cfg(debug_assertions)]
let log_level = LevelFilter::Debug;
#[cfg(not(debug_assertions))]
let log_level = LevelFilter::Info;
WriteLogger::init( WriteLogger::init(
LevelFilter::Debug, log_level,
Default::default(), Default::default(),
std::fs::File::create(&log_filepath).unwrap(), std::fs::File::create(&log_filepath).unwrap(),
) )
@ -77,6 +81,7 @@ async fn main() -> std::io::Result<()> {
.service(not_decky::decky_artifact) .service(not_decky::decky_artifact)
.service(not_decky::decky_image) .service(not_decky::decky_image)
.service(not_decky::decky_statistics) .service(not_decky::decky_statistics)
.service(not_decky::decky_statistics_increment)
}) })
.bind(("0.0.0.0", args.server_port.unwrap_or(22252)))? .bind(("0.0.0.0", args.server_port.unwrap_or(22252)))?
.run() .run()

View file

@ -8,4 +8,4 @@ pub use artifact::decky_artifact;
pub use image::decky_image; pub use image::decky_image;
pub use index::decky_index; pub use index::decky_index;
pub use plugins::decky_plugins; pub use plugins::decky_plugins;
pub use stats::decky_statistics; pub use stats::{decky_statistics, decky_statistics_increment};

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

@ -1,12 +1,19 @@
use std::collections::HashMap; use std::collections::HashMap;
use actix_web::{get, web, Responder}; use actix_web::{get, post, web, Responder};
use crate::storage::IStorage; use crate::storage::IStorage;
#[get("/stats")] #[get("/stats")]
pub async fn decky_statistics(data: actix_web::web::Data<Box<dyn IStorage>>) -> impl Responder { pub async fn decky_statistics(data: actix_web::web::Data<Box<dyn IStorage>>) -> impl Responder {
println!("stats");
let plugins: HashMap<String, u64> = data.get_statistics(); let plugins: HashMap<String, u64> = data.get_statistics();
web::Json(plugins) web::Json(plugins)
} }
#[post("/plugins/{name}/versions/{version}")]
pub async fn decky_statistics_increment(data: actix_web::web::Data<Box<dyn IStorage>>, path: actix_web::web::Path<(String, String)>, query: actix_web::web::Query<decky_api::StorePluginIncrement>) -> actix_web::Result<impl Responder> {
let new_version_info = data.increment_statistic(&path.0, &path.1, &*query)
.map_err(|e| actix_web::error::ErrorNotFound(e.to_string()))?;
Ok(web::Json(new_version_info))
}

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

@ -9,7 +9,7 @@ use decky_api::{StorePlugin, StorePluginList, StorePluginVersion};
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use super::IStorage; use super::{IStorage, IQueryHandler};
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
pub struct PluginMetadata { pub struct PluginMetadata {
@ -21,6 +21,10 @@ pub struct PluginMetadata {
impl PluginMetadata { impl PluginMetadata {
fn complete(self, name: String, versions: Vec<StorePluginVersion>, image: String) -> StorePlugin { fn complete(self, name: String, versions: Vec<StorePluginVersion>, image: String) -> StorePlugin {
let downloads = versions.iter().map(|v| v.downloads.unwrap_or(0)).sum();
let updates = versions.iter().map(|v| v.updates.unwrap_or(0)).sum();
let created_time = versions.iter().map(|v| v.created.unwrap_or_default()).min().unwrap_or_default();
let updated_time = versions.iter().map(|v| v.created.unwrap_or_default()).max().unwrap_or_default();
StorePlugin { StorePlugin {
id: self.id, id: self.id,
name: name, name: name,
@ -29,6 +33,10 @@ impl PluginMetadata {
description: self.description, description: self.description,
tags: self.tags, tags: self.tags,
image_url: image, image_url: image,
downloads: Some(downloads),
updates: Some(updates),
created: Some(created_time),
updated: Some(updated_time),
} }
} }
} }
@ -37,6 +45,7 @@ pub struct FileStorage {
stats: Option<RwLock<HashMap<String, AtomicU64>>>, // TODO collect hit counts on actions stats: Option<RwLock<HashMap<String, AtomicU64>>>, // TODO collect hit counts on actions
root: PathBuf, root: PathBuf,
domain_root: String, domain_root: String,
query_handler: Box<dyn IQueryHandler>,
} }
impl FileStorage { impl FileStorage {
@ -45,6 +54,7 @@ impl FileStorage {
root: root, root: root,
domain_root: domain_root, domain_root: domain_root,
stats: if enable_stats { Some(RwLock::new(HashMap::new())) } else { None }, stats: if enable_stats { Some(RwLock::new(HashMap::new())) } else { None },
query_handler: Box::new(super::StandardPostRetrievalSort),
} }
} }
@ -70,7 +80,7 @@ impl FileStorage {
.join(format!("image.png")) .join(format!("image.png"))
} }
fn read_all_plugins(&self) -> std::io::Result<StorePluginList> { fn read_all_plugins(&self, query: Option<&str>) -> std::io::Result<StorePluginList> {
let plugins = self.plugins_path(); let plugins = self.plugins_path();
let dir_reader = plugins.read_dir()?; let dir_reader = plugins.read_dir()?;
let mut results = Vec::with_capacity(dir_reader.size_hint().1.unwrap_or(32)); let mut results = Vec::with_capacity(dir_reader.size_hint().1.unwrap_or(32));
@ -88,12 +98,64 @@ impl FileStorage {
if !lock.contains_key(&version.hash) { if !lock.contains_key(&version.hash) {
lock.insert(version.hash.clone(), AtomicU64::new(0)); lock.insert(version.hash.clone(), AtomicU64::new(0));
} }
let update_key = Self::version_update_name(&version.hash);
if !lock.contains_key(&update_key) {
lock.insert(update_key, AtomicU64::new(0));
} }
} }
} }
}
if let Some(query) = query {
let relevant_params = self.query_handler.supported_params(query);
results = self.query_handler.filter(&relevant_params, results);
}
Ok(results) Ok(results)
} }
fn version_stat_entry_name(plugin_name: &str, version_name: &str) -> String {
format!("{} {} install", plugin_name, version_name)
}
/// External stat for updates of a specific version
fn version_stat_update_entry_name(plugin_name: &str, version_name: &str) -> String {
format!("{} {} update", plugin_name, version_name)
}
/// Internal counter key for update stat of a specific version
fn version_update_name(hash: &str) -> String {
format!("{}-update", hash)
}
fn plugin_stat_entry_name(plugin_name: &str) -> String {
format!("{}", plugin_name)
}
fn artifact_url(&self, plugin_name: &str, version_name: &str, hash_str: &str) -> String {
format!("{}/plugins/{}/{}/{}.zip", self.domain_root, plugin_name, version_name, hash_str)
}
fn image_url(&self, plugin_name: &str) -> String {
format!("{}/plugins/{}.png", self.domain_root, plugin_name)
}
fn get_single_version_stats(&self, hash: &str) -> Option<u64> {
let lock = self.stats.as_ref()?.read().expect("Couldn't acquire stats read lock");
if let Some(stat_counter) = lock.get(hash) {
Some(stat_counter.load(Ordering::SeqCst))
} else {
Some(0)
}
}
fn get_single_version_update_stats(&self, hash: &str) -> Option<u64> {
let lock = self.stats.as_ref()?.read().expect("Couldn't acquire stats read lock");
if let Some(stat_counter) = lock.get(&Self::version_update_name(hash)) {
Some(stat_counter.load(Ordering::SeqCst))
} else {
Some(0)
}
}
fn read_single_plugin(&self, path: &PathBuf) -> std::io::Result<StorePlugin> { fn read_single_plugin(&self, path: &PathBuf) -> std::io::Result<StorePlugin> {
let plugin_name = path.file_name().unwrap().to_string_lossy().into_owned(); let plugin_name = path.file_name().unwrap().to_string_lossy().into_owned();
let json_path = self.plugin_json_path(path); let json_path = self.plugin_json_path(path);
@ -114,18 +176,23 @@ 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 = self.artifact_url(&plugin_name, &version_name, &hash_str);
let downloads_stat = self.get_single_version_stats(&hash_str);
let updates_stat = self.get_single_version_update_stats(&hash_str);
versions.push(StorePluginVersion { versions.push(StorePluginVersion {
name: version_name, name: version_name,
hash: hash_str, hash: hash_str,
artifact: Some(artifact_url) artifact: Some(artifact_url),
created: Some(entry.metadata()?.created()?.into()),
downloads: downloads_stat,
updates: updates_stat,
}); });
} }
} }
} }
versions.sort_by(|a, b| b.name.cmp(&a.name)); // sort e.g. v2 before v1 versions.sort_by(|a, b| b.name.cmp(&a.name)); // sort e.g. v2 before v1
let image_url = format!("{}/plugins/{}.png", self.domain_root, plugin_name); let image_url = self.image_url(&plugin_name);
Ok( Ok(
plugin_info.complete( plugin_info.complete(
plugin_name, plugin_name,
@ -137,8 +204,8 @@ impl FileStorage {
} }
impl IStorage for FileStorage { impl IStorage for FileStorage {
fn plugins(&self) -> StorePluginList { fn plugins(&self, query: &str) -> StorePluginList {
match self.read_all_plugins() { match self.read_all_plugins(Some(query)) {
Err(e) => { Err(e) => {
log::error!("Plugins read error: {}", e); log::error!("Plugins read error: {}", e);
vec![] vec![]
@ -173,7 +240,7 @@ impl IStorage for FileStorage {
fn get_statistics(&self) -> std::collections::HashMap<String, u64> { fn get_statistics(&self) -> std::collections::HashMap<String, u64> {
if let Some(stats) = &self.stats { if let Some(stats) = &self.stats {
if let Ok(plugins) = self.read_all_plugins() { if let Ok(plugins) = self.read_all_plugins(None) {
let lock = stats.read().expect("Failed to acquire stats read lock"); let lock = stats.read().expect("Failed to acquire stats read lock");
let mut map = std::collections::HashMap::with_capacity(lock.len()); let mut map = std::collections::HashMap::with_capacity(lock.len());
for plugin in plugins { for plugin in plugins {
@ -182,10 +249,15 @@ impl IStorage for FileStorage {
if let Some(count) = lock.get(&version.hash) { if let Some(count) = lock.get(&version.hash) {
let count_val = count.load(Ordering::SeqCst); let count_val = count.load(Ordering::SeqCst);
total += count_val; total += count_val;
map.insert(format!("{} {}", plugin.name, version.name), count_val); map.insert(Self::version_stat_entry_name(&plugin.name, &version.name), count_val);
}
if let Some(count) = lock.get(&Self::version_update_name(&version.hash)) {
let count_val = count.load(Ordering::SeqCst);
total += count_val;
map.insert(Self::version_stat_update_entry_name(&plugin.name, &version.name), count_val);
} }
} }
map.insert(format!("{}", plugin.name), total); map.insert(Self::plugin_stat_entry_name(&plugin.name), total);
} }
map map
} else { } else {
@ -195,4 +267,33 @@ impl IStorage for FileStorage {
std::collections::HashMap::with_capacity(0) std::collections::HashMap::with_capacity(0)
} }
} }
fn increment_statistic(&self, name: &str, version: &str, query: &decky_api::StorePluginIncrement) -> Result<decky_api::StorePluginVersion, std::io::Error> {
if let Some(stats) = &self.stats {
// to find the correct stats counter, match up name and version to find hash
if let Ok(plugins) = self.read_all_plugins(None) {
if let Some(plugin) = plugins.into_iter().filter(|p| p.name == name).next() {
if let Some(mut version) = plugin.versions.into_iter().filter(|v| v.name == version).next() {
let hash = &version.hash;
let lock = stats.read().expect("Failed to acquire stats read lock");
if query.is_update {
let key = Self::version_update_name(hash);
if let Some(counter) = lock.get(&key) {
counter.fetch_add(1, Ordering::SeqCst);
version.updates = version.updates.map(|x| x + 1);
return Ok(version)
}
} else {
if let Some(counter) = lock.get(hash) {
counter.fetch_add(1, Ordering::SeqCst);
version.downloads = version.downloads.map(|x| x + 1);
return Ok(version);
}
}
}
}
}
}
Err(std::io::Error::new(std::io::ErrorKind::NotFound, "Plugin version not found"))
}
} }

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"))
@ -12,12 +12,25 @@ pub trait IStorage: Send + Sync {
fn get_statistics(&self) -> std::collections::HashMap<String, u64> { fn get_statistics(&self) -> std::collections::HashMap<String, u64> {
std::collections::HashMap::with_capacity(0) std::collections::HashMap::with_capacity(0)
} }
fn increment_statistic(&self, _name: &str, _version: &str, _query: &decky_api::StorePluginIncrement) -> Result<decky_api::StorePluginVersion, std::io::Error> {
Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Statistics increment not supported"))
}
} }
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()
} }
} }
pub trait IQueryHandler: Send + Sync {
fn filter(&self, query: &str, plugins: decky_api::StorePluginList) -> decky_api::StorePluginList;
/// Filter out recognized query parameters from query string, returning only those that the query handler can use in IQueryHandler::filter
fn supported_params(&self, query: &str) -> String {
query.to_owned()
}
}

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

@ -3,9 +3,11 @@ mod filesystem;
mod interface; mod interface;
mod merge; mod merge;
mod proxy; mod proxy;
mod standard_sort;
pub use cache::CachedStorage; pub use cache::CachedStorage;
pub use filesystem::FileStorage; pub use filesystem::FileStorage;
pub use interface::{IStorage, EmptyStorage}; pub use interface::{IStorage, EmptyStorage, IQueryHandler};
pub use merge::MergedStorage; pub use merge::MergedStorage;
pub use proxy::ProxiedStorage; pub use proxy::ProxiedStorage;
pub use standard_sort::StandardPostRetrievalSort;

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() {

View file

@ -0,0 +1,57 @@
use decky_api::{StorePluginQuery, StorePluginQuerySortColumn, StorePluginQuerySortDirection};
use super::IQueryHandler;
pub struct StandardPostRetrievalSort;
impl IQueryHandler for StandardPostRetrievalSort {
fn filter(&self, query: &str, mut plugins: decky_api::StorePluginList) -> decky_api::StorePluginList {
log::debug!("StandardPostRetrievalSort::filter got query string `{}`", query);
let query: StorePluginQuery = match serde_urlencoded::from_str(query) {
Ok(q) => q,
Err(e) => {
log::error!("Failed to parse query string {}: {}", query, e);
Default::default()
}
};
match (query.sort_by, query.sort_direction) {
(None, _) => plugins,
(Some(StorePluginQuerySortColumn::Name), direction) => {
plugins.sort_by_key(|p| p.name.to_lowercase());
match direction {
None | Some(StorePluginQuerySortDirection::Ascending) => {},
Some(StorePluginQuerySortDirection::Descending) => plugins.reverse(),
}
plugins
},
(Some(StorePluginQuerySortColumn::Date), direction) => {
plugins.sort_by_key(
|p| p.versions.iter()
.max_by_key(
|v| v.created.unwrap_or_default())
.map(|v| v.created.unwrap_or_default())
);
match direction {
None | Some(StorePluginQuerySortDirection::Ascending) => {},
Some(StorePluginQuerySortDirection::Descending) => plugins.reverse(),
}
plugins
},
(Some(StorePluginQuerySortColumn::Downloads), direction) => {
plugins.sort_by_key(|p| p.versions.iter().map(|v| v.downloads.unwrap_or(0)).sum::<u64>());
match direction {
None | Some(StorePluginQuerySortDirection::Ascending) => {},
Some(StorePluginQuerySortDirection::Descending) => plugins.reverse(),
}
plugins
},
}
}
fn supported_params(&self, query: &str) -> String {
match serde_urlencoded::from_str::<StorePluginQuery>(query) {
Ok(q) => serde_urlencoded::to_string(&q).unwrap_or_default(),
Err(_) => "".to_owned()
}
}
}