Add image and artifact proxying
This commit is contained in:
parent
19b63f5a15
commit
a5d2a4615a
3 changed files with 166 additions and 18 deletions
80
src/cli.rs
80
src/cli.rs
|
@ -112,7 +112,7 @@ impl FilesystemArgs {
|
||||||
for_variable = Some(value);
|
for_variable = Some(value);
|
||||||
}
|
}
|
||||||
} else { buffer.push('=') },
|
} else { buffer.push('=') },
|
||||||
',' => if !in_string && !buffer.is_empty() {
|
',' => if !in_string {
|
||||||
let value: String = buffer.drain(..).collect();
|
let value: String = buffer.drain(..).collect();
|
||||||
if let Some(var) = for_variable.take() {
|
if let Some(var) = for_variable.take() {
|
||||||
let var_trimmed = var.trim();
|
let var_trimmed = var.trim();
|
||||||
|
@ -142,6 +142,14 @@ pub struct ProxyArgs {
|
||||||
/// Proxy offerings from another store
|
/// Proxy offerings from another store
|
||||||
#[arg(name = "store", long, default_value_t = {"https://plugins.deckbrew.xyz".into()})]
|
#[arg(name = "store", long, default_value_t = {"https://plugins.deckbrew.xyz".into()})]
|
||||||
pub proxy_store: String,
|
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 {
|
impl ProxyArgs {
|
||||||
|
@ -154,22 +162,61 @@ impl ProxyArgs {
|
||||||
return Err(format!("Proxy descriptor too short"));
|
return Err(format!("Proxy descriptor too short"));
|
||||||
}
|
}
|
||||||
let mut buffer = Vec::new();
|
let mut buffer = Vec::new();
|
||||||
|
let mut for_variable: Option<String> = None;
|
||||||
|
let mut in_string = false;
|
||||||
let mut escaped = 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 {
|
for c in chars {
|
||||||
match c {
|
match c {
|
||||||
'}' => if escaped {
|
'}' if !escaped && !in_string => {
|
||||||
buffer.push('}')
|
|
||||||
} else {
|
|
||||||
return
|
return
|
||||||
Ok(Self {
|
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 => {
|
c => {
|
||||||
if escaped {
|
if escaped {
|
||||||
escaped = false;
|
escaped = false;
|
||||||
buffer.push('\\');
|
//buffer.push('\\');
|
||||||
}
|
}
|
||||||
buffer.push(c)
|
buffer.push(c)
|
||||||
},
|
},
|
||||||
|
@ -179,7 +226,13 @@ impl ProxyArgs {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_descriptor(self) -> String {
|
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]
|
#[test]
|
||||||
fn storage_descriptor() {
|
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());
|
let parsed = StorageArgs::from_descriptor(&mut descriptor.chars());
|
||||||
parsed.expect("StorageArgs parse error");
|
parsed.expect("StorageArgs parse error");
|
||||||
let descriptor = "p{}";
|
let descriptor = "p{}";
|
||||||
|
@ -272,7 +325,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn filesys_descriptor() {
|
fn filesys_descriptor() {
|
||||||
let descriptor = "{root='',domain='',stats:0}";
|
let descriptor = "{root=\"\",domain=\"\",stats=0,}";
|
||||||
let parsed = FilesystemArgs::from_descriptor(&mut descriptor.chars());
|
let parsed = FilesystemArgs::from_descriptor(&mut descriptor.chars());
|
||||||
parsed.expect("FilesystemArgs parse error");
|
parsed.expect("FilesystemArgs parse error");
|
||||||
}
|
}
|
||||||
|
@ -284,6 +337,13 @@ mod tests {
|
||||||
parsed.expect("ProxyArgs parse error");
|
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]
|
#[test]
|
||||||
fn merge_descriptor() {
|
fn merge_descriptor() {
|
||||||
let descriptor = "[(f{}),(p{}),( )]";
|
let descriptor = "[(f{}),(p{}),( )]";
|
||||||
|
|
|
@ -26,6 +26,9 @@ fn build_storage_box(storage: &cli::StorageArgs) -> Box<dyn storage::IStorage> {
|
||||||
)),
|
)),
|
||||||
cli::StorageArgs::Proxy(px) => Box::new(storage::ProxiedStorage::new(
|
cli::StorageArgs::Proxy(px) => Box::new(storage::ProxiedStorage::new(
|
||||||
px.proxy_store.clone(),
|
px.proxy_store.clone(),
|
||||||
|
px.intercept_artifacts,
|
||||||
|
px.intercept_images,
|
||||||
|
px.domain_root.clone(),
|
||||||
)),
|
)),
|
||||||
cli::StorageArgs::Empty => Box::new(storage::EmptyStorage),
|
cli::StorageArgs::Empty => Box::new(storage::EmptyStorage),
|
||||||
cli::StorageArgs::Merge(ls) => Box::new(storage::MergedStorage::new(
|
cli::StorageArgs::Merge(ls) => Box::new(storage::MergedStorage::new(
|
||||||
|
|
|
@ -2,15 +2,23 @@ use decky_api::{StorePluginList, StorePluginVersion};
|
||||||
|
|
||||||
use super::IStorage;
|
use super::IStorage;
|
||||||
|
|
||||||
|
const MAX_PROXY_RESPONSE_SIZE: u64 = 10_000_000;
|
||||||
|
|
||||||
pub struct ProxiedStorage {
|
pub struct ProxiedStorage {
|
||||||
store_url: String,
|
store_url: String,
|
||||||
|
intercept_artifacts: bool,
|
||||||
|
intercept_images: bool,
|
||||||
|
domain_root: String,
|
||||||
agent: ureq::Agent,
|
agent: ureq::Agent,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProxiedStorage {
|
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 {
|
Self {
|
||||||
store_url: target_store,
|
store_url: target_store,
|
||||||
|
intercept_artifacts: intercept_artifacts,
|
||||||
|
intercept_images: intercept_images,
|
||||||
|
domain_root: domain_root,
|
||||||
agent: ureq::AgentBuilder::new()
|
agent: ureq::AgentBuilder::new()
|
||||||
.tls_connector(std::sync::Arc::new(native_tls::TlsConnector::new().expect("Native TLS init failed")))
|
.tls_connector(std::sync::Arc::new(native_tls::TlsConnector::new().expect("Native TLS init failed")))
|
||||||
.build(),
|
.build(),
|
||||||
|
@ -22,7 +30,31 @@ impl ProxiedStorage {
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
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 {
|
fn proxy_plugins(&self, query: &str) -> StorePluginList {
|
||||||
|
@ -49,23 +81,76 @@ impl IStorage for ProxiedStorage {
|
||||||
fn plugins(&self, query: &str) -> StorePluginList {
|
fn plugins(&self, query: &str) -> StorePluginList {
|
||||||
let mut proxy = self.proxy_plugins(query);
|
let mut proxy = self.proxy_plugins(query);
|
||||||
for plugin in &mut proxy {
|
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 {
|
for version in &mut plugin.versions {
|
||||||
if version.artifact.is_none() {
|
if self.intercept_artifacts {
|
||||||
version.artifact = Some(Self::default_artifact_url(version));
|
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
|
proxy
|
||||||
}
|
}
|
||||||
|
|
||||||
/*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> {
|
||||||
self.fallback.get_artifact(name, version, hash)
|
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> {
|
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> {
|
fn get_statistics(&self) -> std::collections::HashMap<String, u64> {
|
||||||
self.fallback.get_statistics()
|
self.fallback.get_statistics()
|
||||||
}*/
|
}*/
|
||||||
|
|
Loading…
Reference in a new issue