Feature/hw filtering (#2368)

* update deno

* add proxy

* remove query params, now auto added by BE

* add hardware requirements and BE reg query params

* update query params for BE requests

* allow multiple arches in hw reqs

* explain git hash mismatch

* require lshw

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
This commit is contained in:
Aiden McClelland
2023-08-02 09:52:38 -06:00
committed by GitHub
parent 32ca91a7c9
commit 73229501c2
21 changed files with 349 additions and 160 deletions

7
backend/Cargo.lock generated
View File

@@ -4760,6 +4760,7 @@ dependencies = [
"trust-dns-server",
"typed-builder",
"url",
"urlencoding",
"uuid",
"zeroize",
]
@@ -5928,6 +5929,12 @@ dependencies = [
"serde",
]
[[package]]
name = "urlencoding"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9"
[[package]]
name = "utf-8"
version = "0.7.6"

View File

@@ -154,6 +154,7 @@ tracing-subscriber = { version = "0.3.14", features = ["env-filter"] }
trust-dns-server = "0.22.0"
typed-builder = "0.10.0"
url = { version = "2.2.2", features = ["serde"] }
urlencoding = "2.1.2"
uuid = { version = "1.1.2", features = ["v4"] }
zeroize = "1.5.7"

View File

@@ -11,7 +11,7 @@ use helpers::to_tmp_path;
use josekit::jwk::Jwk;
use patch_db::json_ptr::JsonPointer;
use patch_db::{DbHandle, LockReceipt, LockType, PatchDb};
use reqwest::Url;
use reqwest::{Client, Proxy, Url};
use rpc_toolkit::Context;
use serde::Deserialize;
use sqlx::postgres::PgConnectOptions;
@@ -34,7 +34,9 @@ use crate::net::wifi::WpaCli;
use crate::notifications::NotificationManager;
use crate::shutdown::Shutdown;
use crate::status::{MainStatus, Status};
use crate::system::get_mem_info;
use crate::util::config::load_config_from_paths;
use crate::util::lshw::{lshw, LshwDevice};
use crate::{Error, ErrorKind, ResultExt};
#[derive(Debug, Default, Deserialize)]
@@ -120,6 +122,13 @@ pub struct RpcContextSeed {
pub rpc_stream_continuations: Mutex<BTreeMap<RequestGuid, RpcContinuation>>,
pub wifi_manager: Option<Arc<RwLock<WpaCli>>>,
pub current_secret: Arc<Jwk>,
pub client: Client,
pub hardware: Hardware,
}
pub struct Hardware {
pub devices: Vec<LshwDevice>,
pub ram: u64,
}
pub struct RpcCleanReceipts {
@@ -203,6 +212,9 @@ impl RpcContext {
let metrics_cache = RwLock::new(None);
let notification_manager = NotificationManager::new(secret_store.clone());
tracing::info!("Initialized Notification Manager");
let tor_proxy_url = format!("socks5h://{tor_proxy}");
let devices = lshw().await?;
let ram = get_mem_info().await?.total.0 as u64 * 1024 * 1024;
let seed = Arc::new(RpcContextSeed {
is_closed: AtomicBool::new(false),
datadir: base.datadir().to_path_buf(),
@@ -235,6 +247,17 @@ impl RpcContext {
)
})?,
),
client: Client::builder()
.proxy(Proxy::custom(move |url| {
if url.host_str().map_or(false, |h| h.ends_with(".onion")) {
Some(tor_proxy_url.clone())
} else {
None
}
}))
.build()
.with_kind(crate::ErrorKind::ParseUrl)?,
hardware: Hardware { devices, ram },
});
let res = Self(seed);

View File

@@ -40,6 +40,7 @@ use crate::dependencies::{
};
use crate::install::cleanup::{cleanup, update_dependency_errors_of_dependents};
use crate::install::progress::{InstallProgress, InstallProgressTracker};
use crate::marketplace::with_query_params;
use crate::notifications::NotificationLevel;
use crate::s9pk::manifest::{Manifest, PackageId};
use crate::s9pk::reader::S9pkReader;
@@ -136,35 +137,39 @@ pub async fn install(
let marketplace_url =
marketplace_url.unwrap_or_else(|| crate::DEFAULT_MARKETPLACE.parse().unwrap());
let version_priority = version_priority.unwrap_or_default();
let man: Manifest = reqwest::get(format!(
"{}/package/v0/manifest/{}?spec={}&version-priority={}&eos-version-compat={}&arch={}",
marketplace_url,
id,
version,
version_priority,
Current::new().compat(),
&*crate::ARCH,
))
.await
.with_kind(crate::ErrorKind::Registry)?
.error_for_status()
.with_kind(crate::ErrorKind::Registry)?
.json()
.await
.with_kind(crate::ErrorKind::Registry)?;
let s9pk = reqwest::get(format!(
"{}/package/v0/{}.s9pk?spec=={}&version-priority={}&eos-version-compat={}&arch={}",
marketplace_url,
id,
man.version,
version_priority,
Current::new().compat(),
&*crate::ARCH,
))
.await
.with_kind(crate::ErrorKind::Registry)?
.error_for_status()
.with_kind(crate::ErrorKind::Registry)?;
let man: Manifest = ctx
.client
.get(with_query_params(
&ctx,
format!(
"{}/package/v0/manifest/{}?spec={}&version-priority={}",
marketplace_url, id, version, version_priority,
)
.parse()?,
))
.send()
.await
.with_kind(crate::ErrorKind::Registry)?
.error_for_status()
.with_kind(crate::ErrorKind::Registry)?
.json()
.await
.with_kind(crate::ErrorKind::Registry)?;
let s9pk = ctx
.client
.get(with_query_params(
&ctx,
format!(
"{}/package/v0/{}.s9pk?spec=={}&version-priority={}",
marketplace_url, id, man.version, version_priority,
)
.parse()?,
))
.send()
.await
.with_kind(crate::ErrorKind::Registry)?
.error_for_status()
.with_kind(crate::ErrorKind::Registry)?;
if man.id.as_str() != id || !man.version.satisfies(&version) {
return Err(Error::new(
@@ -185,16 +190,18 @@ pub async fn install(
async {
tokio::io::copy(
&mut response_to_reader(
reqwest::get(format!(
"{}/package/v0/license/{}?spec=={}&eos-version-compat={}&arch={}",
marketplace_url,
id,
man.version,
Current::new().compat(),
&*crate::ARCH,
))
.await?
.error_for_status()?,
ctx.client
.get(with_query_params(
&ctx,
format!(
"{}/package/v0/license/{}?spec=={}",
marketplace_url, id, man.version,
)
.parse()?,
))
.send()
.await?
.error_for_status()?,
),
&mut File::create(public_dir_path.join("LICENSE.md")).await?,
)
@@ -204,16 +211,18 @@ pub async fn install(
async {
tokio::io::copy(
&mut response_to_reader(
reqwest::get(format!(
"{}/package/v0/instructions/{}?spec=={}&eos-version-compat={}&arch={}",
marketplace_url,
id,
man.version,
Current::new().compat(),
&*crate::ARCH,
))
.await?
.error_for_status()?,
ctx.client
.get(with_query_params(
&ctx,
format!(
"{}/package/v0/instructions/{}?spec=={}",
marketplace_url, id, man.version,
)
.parse()?,
))
.send()
.await?
.error_for_status()?,
),
&mut File::create(public_dir_path.join("INSTRUCTIONS.md")).await?,
)
@@ -223,16 +232,18 @@ pub async fn install(
async {
tokio::io::copy(
&mut response_to_reader(
reqwest::get(format!(
"{}/package/v0/icon/{}?spec=={}&eos-version-compat={}&arch={}",
marketplace_url,
id,
man.version,
Current::new().compat(),
&*crate::ARCH,
))
.await?
.error_for_status()?,
ctx.client
.get(with_query_params(
&ctx,
format!(
"{}/package/v0/icon/{}?spec=={}",
marketplace_url, id, man.version,
)
.parse()?,
))
.send()
.await?
.error_for_status()?,
),
&mut File::create(public_dir_path.join(format!("icon.{}", icon_type))).await?,
)
@@ -928,17 +939,20 @@ pub async fn install_s9pk<R: AsyncRead + AsyncSeek + Unpin + Send + Sync>(
{
Some(local_man)
} else if let Some(marketplace_url) = &marketplace_url {
match reqwest::get(format!(
"{}/package/v0/manifest/{}?spec={}&eos-version-compat={}&arch={}",
marketplace_url,
dep,
info.version,
Current::new().compat(),
&*crate::ARCH,
))
.await
.with_kind(crate::ErrorKind::Registry)?
.error_for_status()
match ctx
.client
.get(with_query_params(
ctx,
format!(
"{}/package/v0/manifest/{}?spec={}",
marketplace_url, dep, info.version,
)
.parse()?,
))
.send()
.await
.with_kind(crate::ErrorKind::Registry)?
.error_for_status()
{
Ok(a) => Ok(Some(
a.json()
@@ -963,16 +977,19 @@ pub async fn install_s9pk<R: AsyncRead + AsyncSeek + Unpin + Send + Sync>(
let icon_path = dir.join(format!("icon.{}", manifest.assets.icon_type()));
if tokio::fs::metadata(&icon_path).await.is_err() {
tokio::fs::create_dir_all(&dir).await?;
let icon = reqwest::get(format!(
"{}/package/v0/icon/{}?spec={}&eos-version-compat={}&arch={}",
marketplace_url,
dep,
info.version,
Current::new().compat(),
&*crate::ARCH,
))
.await
.with_kind(crate::ErrorKind::Registry)?;
let icon = ctx
.client
.get(with_query_params(
ctx,
format!(
"{}/package/v0/icon/{}?spec={}",
marketplace_url, dep, info.version,
)
.parse()?,
))
.send()
.await
.with_kind(crate::ErrorKind::Registry)?;
let mut dst = File::create(&icon_path).await?;
tokio::io::copy(&mut response_to_reader(icon), &mut dst).await?;
dst.sync_all().await?;

View File

@@ -3,6 +3,8 @@ use reqwest::{StatusCode, Url};
use rpc_toolkit::command;
use serde_json::Value;
use crate::context::RpcContext;
use crate::version::VersionT;
use crate::{Error, ResultExt};
#[command(subcommands(get))]
@@ -10,9 +12,34 @@ pub fn marketplace() -> Result<(), Error> {
Ok(())
}
pub fn with_query_params(ctx: &RpcContext, mut url: Url) -> Url {
url.query_pairs_mut()
.append_pair(
"os.version",
&crate::version::Current::new().semver().to_string(),
)
.append_pair(
"os.compat",
&crate::version::Current::new().compat().to_string(),
)
.append_pair("os.arch", crate::OS_ARCH)
.append_pair("hardware.arch", &*crate::ARCH)
.append_pair("hardware.ram", &ctx.hardware.ram.to_string());
for hw in &ctx.hardware.devices {
url.query_pairs_mut()
.append_pair(&format!("hardware.device.{}", hw.class()), hw.product());
}
url
}
#[command]
pub async fn get(#[arg] url: Url) -> Result<Value, Error> {
let mut response = reqwest::get(url)
pub async fn get(#[context] ctx: RpcContext, #[arg] url: Url) -> Result<Value, Error> {
let mut response = ctx
.client
.get(with_query_params(&ctx, url))
.send()
.await
.with_kind(crate::ErrorKind::Network)?;
let status = response.status();

View File

@@ -38,6 +38,8 @@ static NOT_AUTHORIZED: &[u8] = b"Not Authorized";
static EMBEDDED_UIS: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../frontend/dist/static");
const PROXY_STRIP_HEADERS: &[&str] = &["cookie", "host", "origin", "referer", "user-agent"];
fn status_fn(_: i32) -> StatusCode {
StatusCode::OK
}
@@ -236,15 +238,6 @@ pub async fn main_ui_server_router(ctx: RpcContext) -> Result<HttpHandler, Error
async fn alt_ui(req: Request<Body>, ui_mode: UiMode) -> Result<Response<Body>, Error> {
let (request_parts, _body) = req.into_parts();
let accept_encoding = request_parts
.headers
.get_all(ACCEPT_ENCODING)
.into_iter()
.filter_map(|h| h.to_str().ok())
.flat_map(|s| s.split(","))
.filter_map(|s| s.split(";").next())
.map(|s| s.trim())
.collect::<Vec<_>>();
match &request_parts.method {
&Method::GET => {
let uri_path = ui_mode.path(
@@ -307,6 +300,40 @@ async fn main_embassy_ui(req: Request<Body>, ctx: RpcContext) -> Result<Response
Err(e) => un_authorized(e, &format!("public/{path}")),
}
}
(&Method::GET, Some(("proxy", target))) => {
match HasValidSession::from_request_parts(&request_parts, &ctx).await {
Ok(_) => {
let target = urlencoding::decode(target)?;
let res = ctx
.client
.get(target.as_ref())
.headers(
request_parts
.headers
.iter()
.filter(|(h, _)| {
!PROXY_STRIP_HEADERS
.iter()
.any(|bad| h.as_str().eq_ignore_ascii_case(bad))
})
.map(|(h, v)| (h.clone(), v.clone()))
.collect(),
)
.send()
.await
.with_kind(crate::ErrorKind::Network)?;
let mut hres = Response::builder().status(res.status());
for (h, v) in res.headers().clone() {
if let Some(h) = h {
hres = hres.header(h, v);
}
}
hres.body(Body::wrap_stream(res.bytes_stream()))
.with_kind(crate::ErrorKind::Network)
}
Err(e) => un_authorized(e, &format!("proxy/{target}")),
}
}
(&Method::GET, Some(("eos", "local.crt"))) => {
match HasValidSession::from_request_parts(&request_parts, &ctx).await {
Ok(_) => cert_send(&ctx.account.read().await.root_ca_cert),
@@ -550,8 +577,3 @@ fn e_tag(path: &Path, metadata: Option<&Metadata>) -> String {
base32::encode(base32::Alphabet::RFC4648 { padding: false }, res.as_slice()).to_lowercase()
)
}
#[test]
fn test_packed_html() {
assert!(MainUi::get("index.html").is_some())
}

View File

@@ -1,3 +1,4 @@
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use color_eyre::eyre::eyre;
@@ -16,6 +17,7 @@ use crate::net::interface::Interfaces;
use crate::procedure::docker::DockerContainers;
use crate::procedure::PackageProcedure;
use crate::status::health_check::HealthChecks;
use crate::util::serde::Regex;
use crate::util::Version;
use crate::version::{Current, VersionT};
use crate::volume::Volumes;
@@ -79,6 +81,9 @@ pub struct Manifest {
#[serde(default)]
pub replaces: Vec<String>,
#[serde(default)]
pub hardware_requirements: HardwareRequirements,
}
impl Manifest {
@@ -109,6 +114,15 @@ impl Manifest {
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct HardwareRequirements {
#[serde(default)]
device: BTreeMap<String, Regex>,
ram: Option<u64>,
arch: Option<Vec<String>>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct Assets {

View File

@@ -251,7 +251,7 @@ impl<'de> Deserialize<'de> for Percentage {
}
#[derive(Clone, Debug)]
pub struct MebiBytes(f64);
pub struct MebiBytes(pub f64);
impl Serialize for MebiBytes {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
@@ -310,19 +310,19 @@ pub struct MetricsGeneral {
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct MetricsMemory {
#[serde(rename = "Percentage Used")]
percentage_used: Percentage,
pub percentage_used: Percentage,
#[serde(rename = "Total")]
total: MebiBytes,
pub total: MebiBytes,
#[serde(rename = "Available")]
available: MebiBytes,
pub available: MebiBytes,
#[serde(rename = "Used")]
used: MebiBytes,
pub used: MebiBytes,
#[serde(rename = "Swap Total")]
swap_total: MebiBytes,
pub swap_total: MebiBytes,
#[serde(rename = "Swap Free")]
swap_free: MebiBytes,
pub swap_free: MebiBytes,
#[serde(rename = "Swap Used")]
swap_used: MebiBytes,
pub swap_used: MebiBytes,
}
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct MetricsCpu {
@@ -698,7 +698,7 @@ pub struct MemInfo {
swap_free: Option<u64>,
}
#[instrument(skip_all)]
async fn get_mem_info() -> Result<MetricsMemory, Error> {
pub async fn get_mem_info() -> Result<MetricsMemory, Error> {
let contents = tokio::fs::read_to_string("/proc/meminfo").await?;
let mut mem_info = MemInfo {
mem_total: None,

View File

@@ -19,6 +19,7 @@ use crate::db::model::UpdateProgress;
use crate::disk::mount::filesystem::bind::Bind;
use crate::disk::mount::filesystem::ReadWrite;
use crate::disk::mount::guard::MountGuard;
use crate::marketplace::with_query_params;
use crate::notifications::NotificationLevel;
use crate::sound::{
CIRCLE_OF_5THS_SHORT, UPDATE_FAILED_1, UPDATE_FAILED_2, UPDATE_FAILED_3, UPDATE_FAILED_4,
@@ -81,18 +82,19 @@ async fn maybe_do_update(
marketplace_url: Url,
) -> Result<Option<Arc<Revision>>, Error> {
let mut db = ctx.db.handle();
let latest_version: Version = reqwest::get(format!(
"{}/eos/v0/latest?eos-version={}&arch={}",
marketplace_url,
Current::new().semver(),
OS_ARCH,
))
.await
.with_kind(ErrorKind::Network)?
.json::<LatestInformation>()
.await
.with_kind(ErrorKind::Network)?
.version;
let latest_version: Version = ctx
.client
.get(with_query_params(
&ctx,
format!("{}/eos/v0/latest", marketplace_url,).parse()?,
))
.send()
.await
.with_kind(ErrorKind::Network)?
.json::<LatestInformation>()
.await
.with_kind(ErrorKind::Network)?
.version;
crate::db::DatabaseModel::new()
.server_info()
.lock(&mut db, LockType::Write)

49
backend/src/util/lshw.rs Normal file
View File

@@ -0,0 +1,49 @@
use models::{Error, ResultExt};
use serde::{Deserialize, Serialize};
use tokio::process::Command;
use crate::util::Invoke;
const KNOWN_CLASSES: &[&str] = &["processor", "display"];
#[derive(Debug, Deserialize, Serialize)]
#[serde(tag = "class")]
#[serde(rename_all = "kebab-case")]
pub enum LshwDevice {
Processor(LshwProcessor),
Display(LshwDisplay),
}
impl LshwDevice {
pub fn class(&self) -> &'static str {
match self {
Self::Processor(_) => "processor",
Self::Display(_) => "display",
}
}
pub fn product(&self) -> &str {
match self {
Self::Processor(hw) => hw.product.as_str(),
Self::Display(hw) => hw.product.as_str(),
}
}
}
#[derive(Debug, Deserialize, Serialize)]
pub struct LshwProcessor {
pub product: String,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct LshwDisplay {
pub product: String,
}
pub async fn lshw() -> Result<Vec<LshwDevice>, Error> {
let mut cmd = Command::new("lshw");
cmd.arg("-json");
for class in KNOWN_CLASSES {
cmd.arg("-class").arg(*class);
}
serde_json::from_slice(&cmd.invoke(crate::ErrorKind::Lshw).await?)
.with_kind(crate::ErrorKind::Deserialization)
}

View File

@@ -27,6 +27,7 @@ pub mod config;
pub mod http_reader;
pub mod io;
pub mod logger;
pub mod lshw;
pub mod serde;
#[derive(Clone, Copy, Debug)]

View File

@@ -793,3 +793,42 @@ impl<T: AsRef<[u8]>> Serialize for Base64<T> {
serializer.serialize_str(&base64::encode(self.0.as_ref()))
}
}
#[derive(Clone, Debug)]
pub struct Regex(regex::Regex);
impl From<Regex> for regex::Regex {
fn from(value: Regex) -> Self {
value.0
}
}
impl From<regex::Regex> for Regex {
fn from(value: regex::Regex) -> Self {
Regex(value)
}
}
impl AsRef<regex::Regex> for Regex {
fn as_ref(&self) -> &regex::Regex {
&self.0
}
}
impl AsMut<regex::Regex> for Regex {
fn as_mut(&mut self) -> &mut regex::Regex {
&mut self.0
}
}
impl<'de> Deserialize<'de> for Regex {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserialize_from_str(deserializer).map(Self)
}
}
impl Serialize for Regex {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serialize_display(&self.0, serializer)
}
}

View File

@@ -24,6 +24,7 @@ iw
jq
libavahi-client3
lm-sensors
lshw
lvm2
magic-wormhole
man-db

View File

@@ -71,6 +71,7 @@ sudo losetup -d $OUTPUT_DEVICE
if [ "$ALLOW_VERSION_MISMATCH" != 1 ]; then
if [ "$(cat GIT_HASH.txt)" != "$REAL_GIT_HASH" ]; then
>&2 echo "startos.raspberrypi.squashfs GIT_HASH.txt mismatch"
>&2 echo "expected $REAL_GIT_HASH (dpkg) found $(cat GIT_HASH.txt) (repo)"
exit 1
fi
if [ "$(cat VERSION.txt)" != "$REAL_VERSION" ]; then

View File

@@ -259,19 +259,14 @@ export module RR {
// marketplace
export type EnvInfo = {
'server-id': string
'eos-version': string
}
export type GetMarketplaceInfoReq = EnvInfo
export type GetMarketplaceInfoReq = { 'server-id': string }
export type GetMarketplaceInfoRes = StoreInfo
export type GetMarketplaceEosReq = EnvInfo
export type GetMarketplaceEosReq = { 'server-id': string }
export type GetMarketplaceEosRes = MarketplaceEOS
export type GetMarketplacePackagesReq = {
ids?: { id: string; version: string }[]
'eos-version-compat': string
// iff !ids
category?: string
query?: string

View File

@@ -125,7 +125,6 @@ export abstract class ApiService {
path: string,
params: Record<string, unknown>,
url: string,
arch?: string,
): Promise<T>
abstract getEos(): Promise<RR.GetMarketplaceEosRes>

View File

@@ -212,10 +212,7 @@ export class LiveApiService extends ApiService {
path: string,
qp: Record<string, string>,
baseUrl: string,
arch: string = this.config.packageArch,
): Promise<T> {
// Object.assign(qp, { arch })
qp['arch'] = arch
const fullUrl = `${baseUrl}${path}?${new URLSearchParams(qp).toString()}`
return this.rpcRequest({
method: 'marketplace.get',
@@ -224,17 +221,13 @@ export class LiveApiService extends ApiService {
}
async getEos(): Promise<RR.GetMarketplaceEosRes> {
const { id, version } = await getServerInfo(this.patch)
const qp: RR.GetMarketplaceEosReq = {
'server-id': id,
'eos-version': version,
}
const { id } = await getServerInfo(this.patch)
const qp: RR.GetMarketplaceEosReq = { 'server-id': id }
return this.marketplaceProxy(
'/eos/v0/latest',
qp,
this.config.marketplace.start9,
this.config.osArch,
)
}

View File

@@ -352,7 +352,6 @@ export class MockApiService extends ApiService {
path: string,
params: Record<string, string>,
url: string,
arch = '',
): Promise<any> {
await pauseFor(2000)

View File

@@ -217,10 +217,7 @@ export class MarketplaceService implements AbstractMarketplaceService {
return this.patch.watch$('server-info').pipe(
take(1),
switchMap(serverInfo => {
const qp: RR.GetMarketplaceInfoReq = {
'server-id': serverInfo.id,
'eos-version': serverInfo.version,
}
const qp: RR.GetMarketplaceInfoReq = { 'server-id': serverInfo.id }
return this.api.marketplaceProxy<RR.GetMarketplaceInfoRes>(
'/package/v0/info',
qp,
@@ -274,28 +271,21 @@ export class MarketplaceService implements AbstractMarketplaceService {
private fetchPackages$(
url: string,
params: Omit<
RR.GetMarketplacePackagesReq,
'eos-version-compat' | 'page' | 'per-page'
> = {},
params: Omit<RR.GetMarketplacePackagesReq, 'page' | 'per-page'> = {},
): Observable<MarketplacePkg[]> {
return this.patch.watch$('server-info', 'eos-version-compat').pipe(
take(1),
switchMap(versionCompat => {
const qp: RR.GetMarketplacePackagesReq = {
...params,
'eos-version-compat': versionCompat,
page: 1,
'per-page': 100,
}
if (qp.ids) qp.ids = JSON.stringify(qp.ids)
const qp: RR.GetMarketplacePackagesReq = {
...params,
page: 1,
'per-page': 100,
}
if (qp.ids) qp.ids = JSON.stringify(qp.ids)
return this.api.marketplaceProxy<RR.GetMarketplacePackagesRes>(
'/package/v0/index',
qp,
url,
)
}),
return from(
this.api.marketplaceProxy<RR.GetMarketplacePackagesRes>(
'/package/v0/index',
qp,
url,
),
)
}

View File

@@ -76,6 +76,7 @@ pub enum ErrorKind {
Systemd = 65,
OpenSsh = 66,
Zram = 67,
Lshw = 68,
}
impl ErrorKind {
pub fn as_str(&self) -> &'static str {
@@ -148,6 +149,7 @@ impl ErrorKind {
Systemd => "Systemd Error",
OpenSsh => "OpenSSH Error",
Zram => "Zram Error",
Lshw => "LSHW Error",
}
}
}

View File

@@ -4296,6 +4296,7 @@ dependencies = [
"trust-dns-server",
"typed-builder",
"url",
"urlencoding",
"uuid 1.2.2",
"zeroize",
]
@@ -5021,6 +5022,12 @@ dependencies = [
"serde",
]
[[package]]
name = "urlencoding"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9"
[[package]]
name = "utf-8"
version = "0.7.6"