Add image and artifact proxying

This commit is contained in:
NGnius (Graham) 2024-05-10 22:10:53 -04:00
parent 19b63f5a15
commit a5d2a4615a
3 changed files with 166 additions and 18 deletions

View file

@ -112,7 +112,7 @@ impl FilesystemArgs {
for_variable = Some(value);
}
} else { buffer.push('=') },
',' => if !in_string && !buffer.is_empty() {
',' => if !in_string {
let value: String = buffer.drain(..).collect();
if let Some(var) = for_variable.take() {
let var_trimmed = var.trim();
@ -142,6 +142,14 @@ pub struct ProxyArgs {
/// Proxy offerings from another store
#[arg(name = "store", long, default_value_t = {"https://plugins.deckbrew.xyz".into()})]
pub proxy_store: String,
/// Proxy artifact (plugin download zips) urls
#[arg(name = "artifacts", short, long)]
pub intercept_artifacts: bool,
/// Proxy preview image urls
#[arg(name = "images", short, long)]
pub intercept_images: bool,
#[arg(name = "domain", default_value_t = {"http://localhost:22252".into()})]
pub domain_root: String,
}
impl ProxyArgs {
@ -154,22 +162,61 @@ impl ProxyArgs {
return Err(format!("Proxy descriptor too short"));
}
let mut buffer = Vec::new();
let mut for_variable: Option<String> = None;
let mut in_string = false;
let mut escaped = false;
let mut proxy_store = None;
let mut intercept_artifacts = false;
let mut intercept_images = false;
let mut domain = None;
for c in chars {
match c {
'}' => if escaped {
buffer.push('}')
} else {
'}' if !escaped && !in_string => {
return
Ok(Self {
proxy_store: if buffer.is_empty() { "https://plugins.deckbrew.xyz".into() } else { buffer.iter().collect() }
proxy_store: if let Some(url) = proxy_store {
url
} else if buffer.is_empty() {
"https://plugins.deckbrew.xyz".into()
} else {
buffer.iter().collect()
},
intercept_artifacts,
intercept_images,
domain_root: domain.unwrap_or_else(|| "http://localhost:22252".into()),
})
}
'\\' => escaped = true,
'"' if !escaped => in_string = !in_string,
'=' if !escaped && !in_string => {
let value: String = buffer.drain(..).collect();
if for_variable.is_some() {
return Err("Unexpected = in filesystem descriptor".to_owned());
} else {
for_variable = Some(value);
}
}
',' if !escaped && !in_string => {
let value: String = buffer.drain(..).collect();
if let Some(var) = for_variable.take() {
let var_trimmed = var.trim();
match &var_trimmed as &str {
"intercept" => {
intercept_images = true;
intercept_artifacts = true;
},
"img" | "images" | "intercept_images" => intercept_images = true,
"dl" | "downloads" | "artifacts" | "intercept_artifacts" => intercept_artifacts = true,
"url" | "store" | "proxy_store" => proxy_store = Some(value),
"d" | "domain" => domain = Some(value),
v => return Err(format!("Unexpected variable name {} in filesystem descriptor", v)),
}
}
},
'\\' if !escaped => escaped = true,
c => {
if escaped {
escaped = false;
buffer.push('\\');
//buffer.push('\\');
}
buffer.push(c)
},
@ -179,7 +226,13 @@ impl ProxyArgs {
}
fn to_descriptor(self) -> String {
format!("{{{}}}", self.proxy_store)
match (self.intercept_artifacts, self.intercept_images) {
(true, true) => format!("{{url=\"{}\",intercept,}}", self.proxy_store),
(true, false) => format!("{{url=\"{}\",intercept_artifacts,}}", self.proxy_store),
(false, true) => format!("{{url=\"{}\",intercept_images,}}", self.proxy_store),
(false, false) => format!("{{{}}}", self.proxy_store),
}
}
}
@ -256,7 +309,7 @@ mod tests {
#[test]
fn storage_descriptor() {
let descriptor = "f{root=\"\",domain=\"\",stats=0}";
let descriptor = "f{root=\"\",domain=\"\",stats=0,}";
let parsed = StorageArgs::from_descriptor(&mut descriptor.chars());
parsed.expect("StorageArgs parse error");
let descriptor = "p{}";
@ -272,7 +325,7 @@ mod tests {
#[test]
fn filesys_descriptor() {
let descriptor = "{root='',domain='',stats:0}";
let descriptor = "{root=\"\",domain=\"\",stats=0,}";
let parsed = FilesystemArgs::from_descriptor(&mut descriptor.chars());
parsed.expect("FilesystemArgs parse error");
}
@ -284,6 +337,13 @@ mod tests {
parsed.expect("ProxyArgs parse error");
}
#[test]
fn proxy_descriptor_complex() {
let descriptor = "{url=\"https://plugins.example.com\", intercept, intercept_image,}";
let parsed = ProxyArgs::from_descriptor(&mut descriptor.chars());
parsed.expect("ProxyArgs parse error");
}
#[test]
fn merge_descriptor() {
let descriptor = "[(f{}),(p{}),( )]";

View file

@ -26,6 +26,9 @@ fn build_storage_box(storage: &cli::StorageArgs) -> Box<dyn storage::IStorage> {
)),
cli::StorageArgs::Proxy(px) => Box::new(storage::ProxiedStorage::new(
px.proxy_store.clone(),
px.intercept_artifacts,
px.intercept_images,
px.domain_root.clone(),
)),
cli::StorageArgs::Empty => Box::new(storage::EmptyStorage),
cli::StorageArgs::Merge(ls) => Box::new(storage::MergedStorage::new(

View file

@ -2,15 +2,23 @@ use decky_api::{StorePluginList, StorePluginVersion};
use super::IStorage;
const MAX_PROXY_RESPONSE_SIZE: u64 = 10_000_000;
pub struct ProxiedStorage {
store_url: String,
intercept_artifacts: bool,
intercept_images: bool,
domain_root: String,
agent: ureq::Agent,
}
impl ProxiedStorage {
pub fn new(target_store: String) -> Self {
pub fn new(target_store: String, intercept_artifacts: bool, intercept_images: bool, domain_root: String) -> Self {
Self {
store_url: target_store,
intercept_artifacts: intercept_artifacts,
intercept_images: intercept_images,
domain_root: domain_root,
agent: ureq::AgentBuilder::new()
.tls_connector(std::sync::Arc::new(native_tls::TlsConnector::new().expect("Native TLS init failed")))
.build(),
@ -22,7 +30,31 @@ impl ProxiedStorage {
}
fn default_artifact_url(ver: &StorePluginVersion) -> String {
format!("https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/versions/{}.zip", ver.hash)
Self::default_artifact_url_by_hash(&ver.hash)
}
fn default_artifact_url_by_hash(hash: &str) -> String {
format!("https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/versions/{}.zip", hash)
}
fn proxied_artifact_url(ver: &StorePluginVersion, plugin_name: &str, domain_root: &str) -> String {
// /plugins/{name}/{version}/{hash}.zip
format!("{}/plugins/{}/{}/{}.zip", domain_root, plugin_name, ver.name, ver.hash)
}
fn image_url_to_name(image_url: &str) -> &str {
image_url
.trim_start_matches("https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/artifact_images/")
.trim_end_matches(".png")
}
fn default_image_url_by_name(image_name: &str) -> String {
format!("https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/artifact_images/{}.png", image_name)
}
fn proxied_image_url(image_name: &str, domain_root: &str) -> String {
// /plugins/{name}.png
format!("{}/plugins/{}.png", domain_root, image_name)
}
fn proxy_plugins(&self, query: &str) -> StorePluginList {
@ -49,23 +81,76 @@ impl IStorage for ProxiedStorage {
fn plugins(&self, query: &str) -> StorePluginList {
let mut proxy = self.proxy_plugins(query);
for plugin in &mut proxy {
if self.intercept_images {
plugin.image_url = Self::proxied_image_url(Self::image_url_to_name(&plugin.image_url), &self.domain_root);
}
for version in &mut plugin.versions {
if version.artifact.is_none() {
version.artifact = Some(Self::default_artifact_url(version));
if self.intercept_artifacts {
version.artifact = Some(Self::proxied_artifact_url(&version, &plugin.name, &self.domain_root));
} else {
if version.artifact.is_none() {
version.artifact = Some(Self::default_artifact_url(version));
}
}
}
}
proxy
}
/*fn get_artifact(&self, name: &str, version: &str, hash: &str) -> Result<bytes::Bytes, std::io::Error> {
self.fallback.get_artifact(name, version, hash)
fn get_artifact(&self, _name: &str, _version: &str, hash: &str) -> Result<bytes::Bytes, std::io::Error> {
let url = Self::default_artifact_url_by_hash(hash);
match self.agent.get(&url).call() {
Err(e) => {
log::error!("Plugins proxy error for {}: {}", url, e);
Err(std::io::Error::new(std::io::ErrorKind::ConnectionAborted, e))
},
Ok(resp) => {
let len: usize = resp.header("Content-Length")
.unwrap()
.parse()
.unwrap_or(MAX_PROXY_RESPONSE_SIZE as usize / 4);
let mut buffer = Vec::with_capacity(len);
use std::io::Read;
match resp.into_reader()
.take(MAX_PROXY_RESPONSE_SIZE)
.read_to_end(&mut buffer) {
Err(e) => {
log::error!("Plugins json error for {}: {}", url, e);
Err(e)
}
Ok(_) => Ok(buffer.into()),
}
}
}
}
fn get_image(&self, name: &str) -> Result<bytes::Bytes, std::io::Error> {
self.fallback.get_image(name)
let url = Self::default_image_url_by_name(name);
match self.agent.get(&url).call() {
Err(e) => {
log::error!("Plugins image proxy error for {}: {}", url, e);
Err(std::io::Error::new(std::io::ErrorKind::ConnectionAborted, e))
},
Ok(resp) => {
let len: usize = resp.header("Content-Length")
.unwrap()
.parse()
.unwrap_or(MAX_PROXY_RESPONSE_SIZE as usize / 4);
let mut buffer = Vec::with_capacity(len);
use std::io::Read;
match resp.into_reader()
.take(MAX_PROXY_RESPONSE_SIZE)
.read_to_end(&mut buffer) {
Err(e) => {
log::error!("Plugins json error for {}: {}", url, e);
Err(e)
}
Ok(_) => Ok(buffer.into()),
}
}
}
}
/*
fn get_statistics(&self) -> std::collections::HashMap<String, u64> {
self.fallback.get_statistics()
}*/