mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 20:14:49 +00:00
Refactor/networking (#2189)
* refactor networking and account * add interfaces from manifest automatically * use nistp256 to satisfy firefox * use ed25519 if available * fix ip signing * fix SQL error * update prettytable to fix segfault * fix migration * fix migration * bump welcome-ack * add redirect if connecting to https over http * misc rebase fixes * fix compression * bump rustc version
This commit is contained in:
@@ -4,6 +4,7 @@ use async_trait::async_trait;
|
||||
use color_eyre::eyre::eyre;
|
||||
use patch_db::DbHandle;
|
||||
use rpc_toolkit::command;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::init::InitReceipts;
|
||||
use crate::Error;
|
||||
@@ -76,8 +77,8 @@ where
|
||||
fn new() -> Self;
|
||||
fn semver(&self) -> emver::Version;
|
||||
fn compat(&self) -> &'static emver::VersionRange;
|
||||
async fn up<Db: DbHandle>(&self, db: &mut Db) -> Result<(), Error>;
|
||||
async fn down<Db: DbHandle>(&self, db: &mut Db) -> Result<(), Error>;
|
||||
async fn up<Db: DbHandle>(&self, db: &mut Db, secrets: &PgPool) -> Result<(), Error>;
|
||||
async fn down<Db: DbHandle>(&self, db: &mut Db, secrets: &PgPool) -> Result<(), Error>;
|
||||
async fn commit<Db: DbHandle>(
|
||||
&self,
|
||||
db: &mut Db,
|
||||
@@ -98,11 +99,19 @@ where
|
||||
&self,
|
||||
version: &V,
|
||||
db: &mut Db,
|
||||
secrets: &PgPool,
|
||||
receipts: &InitReceipts,
|
||||
) -> Result<(), Error> {
|
||||
match self.semver().cmp(&version.semver()) {
|
||||
Ordering::Greater => self.rollback_to_unchecked(version, db, receipts).await,
|
||||
Ordering::Less => version.migrate_from_unchecked(self, db, receipts).await,
|
||||
Ordering::Greater => {
|
||||
self.rollback_to_unchecked(version, db, secrets, receipts)
|
||||
.await
|
||||
}
|
||||
Ordering::Less => {
|
||||
version
|
||||
.migrate_from_unchecked(self, db, secrets, receipts)
|
||||
.await
|
||||
}
|
||||
Ordering::Equal => Ok(()),
|
||||
}
|
||||
}
|
||||
@@ -110,12 +119,13 @@ where
|
||||
&self,
|
||||
version: &V,
|
||||
db: &mut Db,
|
||||
secrets: &PgPool,
|
||||
receipts: &InitReceipts,
|
||||
) -> Result<(), Error> {
|
||||
let previous = Self::Previous::new();
|
||||
if version.semver() < previous.semver() {
|
||||
previous
|
||||
.migrate_from_unchecked(version, db, receipts)
|
||||
.migrate_from_unchecked(version, db, secrets, receipts)
|
||||
.await?;
|
||||
} else if version.semver() > previous.semver() {
|
||||
return Err(Error::new(
|
||||
@@ -127,7 +137,7 @@ where
|
||||
));
|
||||
}
|
||||
tracing::info!("{} -> {}", previous.semver(), self.semver(),);
|
||||
self.up(db).await?;
|
||||
self.up(db, secrets).await?;
|
||||
self.commit(db, receipts).await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -135,15 +145,16 @@ where
|
||||
&self,
|
||||
version: &V,
|
||||
db: &mut Db,
|
||||
secrets: &PgPool,
|
||||
receipts: &InitReceipts,
|
||||
) -> Result<(), Error> {
|
||||
let previous = Self::Previous::new();
|
||||
tracing::info!("{} -> {}", self.semver(), previous.semver(),);
|
||||
self.down(db).await?;
|
||||
self.down(db, secrets).await?;
|
||||
previous.commit(db, receipts).await?;
|
||||
if version.semver() < previous.semver() {
|
||||
previous
|
||||
.rollback_to_unchecked(version, db, receipts)
|
||||
.rollback_to_unchecked(version, db, secrets, receipts)
|
||||
.await?;
|
||||
} else if version.semver() > previous.semver() {
|
||||
return Err(Error::new(
|
||||
@@ -184,21 +195,55 @@ where
|
||||
|
||||
pub async fn init<Db: DbHandle>(
|
||||
db: &mut Db,
|
||||
secrets: &PgPool,
|
||||
receipts: &crate::init::InitReceipts,
|
||||
) -> Result<(), Error> {
|
||||
let version = Version::from_util_version(receipts.server_version.get(db).await?);
|
||||
match version {
|
||||
Version::V0_3_0(v) => v.0.migrate_to(&Current::new(), db, receipts).await?,
|
||||
Version::V0_3_0_1(v) => v.0.migrate_to(&Current::new(), db, receipts).await?,
|
||||
Version::V0_3_0_2(v) => v.0.migrate_to(&Current::new(), db, receipts).await?,
|
||||
Version::V0_3_0_3(v) => v.0.migrate_to(&Current::new(), db, receipts).await?,
|
||||
Version::V0_3_1(v) => v.0.migrate_to(&Current::new(), db, receipts).await?,
|
||||
Version::V0_3_1_1(v) => v.0.migrate_to(&Current::new(), db, receipts).await?,
|
||||
Version::V0_3_1_2(v) => v.0.migrate_to(&Current::new(), db, receipts).await?,
|
||||
Version::V0_3_2(v) => v.0.migrate_to(&Current::new(), db, receipts).await?,
|
||||
Version::V0_3_2_1(v) => v.0.migrate_to(&Current::new(), db, receipts).await?,
|
||||
Version::V0_3_3(v) => v.0.migrate_to(&Current::new(), db, receipts).await?,
|
||||
Version::V0_3_4(v) => v.0.migrate_to(&Current::new(), db, receipts).await?,
|
||||
Version::V0_3_0(v) => {
|
||||
v.0.migrate_to(&Current::new(), db, secrets, receipts)
|
||||
.await?
|
||||
}
|
||||
Version::V0_3_0_1(v) => {
|
||||
v.0.migrate_to(&Current::new(), db, secrets, receipts)
|
||||
.await?
|
||||
}
|
||||
Version::V0_3_0_2(v) => {
|
||||
v.0.migrate_to(&Current::new(), db, secrets, receipts)
|
||||
.await?
|
||||
}
|
||||
Version::V0_3_0_3(v) => {
|
||||
v.0.migrate_to(&Current::new(), db, secrets, receipts)
|
||||
.await?
|
||||
}
|
||||
Version::V0_3_1(v) => {
|
||||
v.0.migrate_to(&Current::new(), db, secrets, receipts)
|
||||
.await?
|
||||
}
|
||||
Version::V0_3_1_1(v) => {
|
||||
v.0.migrate_to(&Current::new(), db, secrets, receipts)
|
||||
.await?
|
||||
}
|
||||
Version::V0_3_1_2(v) => {
|
||||
v.0.migrate_to(&Current::new(), db, secrets, receipts)
|
||||
.await?
|
||||
}
|
||||
Version::V0_3_2(v) => {
|
||||
v.0.migrate_to(&Current::new(), db, secrets, receipts)
|
||||
.await?
|
||||
}
|
||||
Version::V0_3_2_1(v) => {
|
||||
v.0.migrate_to(&Current::new(), db, secrets, receipts)
|
||||
.await?
|
||||
}
|
||||
Version::V0_3_3(v) => {
|
||||
v.0.migrate_to(&Current::new(), db, secrets, receipts)
|
||||
.await?
|
||||
}
|
||||
Version::V0_3_4(v) => {
|
||||
v.0.migrate_to(&Current::new(), db, secrets, receipts)
|
||||
.await?
|
||||
}
|
||||
Version::Other(_) => {
|
||||
return Err(Error::new(
|
||||
eyre!("Cannot downgrade"),
|
||||
|
||||
@@ -28,10 +28,10 @@ impl VersionT for Version {
|
||||
fn compat(&self) -> &'static VersionRange {
|
||||
&*V0_3_0_COMPAT
|
||||
}
|
||||
async fn up<Db: DbHandle>(&self, _db: &mut Db) -> Result<(), Error> {
|
||||
async fn up<Db: DbHandle>(&self, _db: &mut Db, _secrets: &PgPool) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
async fn down<Db: DbHandle>(&self, _db: &mut Db) -> Result<(), Error> {
|
||||
async fn down<Db: DbHandle>(&self, _db: &mut Db, _secrets: &PgPool) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,10 +18,10 @@ impl VersionT for Version {
|
||||
fn compat(&self) -> &'static VersionRange {
|
||||
&*v0_3_0::V0_3_0_COMPAT
|
||||
}
|
||||
async fn up<Db: DbHandle>(&self, _db: &mut Db) -> Result<(), Error> {
|
||||
async fn up<Db: DbHandle>(&self, _db: &mut Db, _secrets: &PgPool) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
async fn down<Db: DbHandle>(&self, _db: &mut Db) -> Result<(), Error> {
|
||||
async fn down<Db: DbHandle>(&self, _db: &mut Db, _secrets: &PgPool) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,10 +18,10 @@ impl VersionT for Version {
|
||||
fn compat(&self) -> &'static VersionRange {
|
||||
&*v0_3_0::V0_3_0_COMPAT
|
||||
}
|
||||
async fn up<Db: DbHandle>(&self, _db: &mut Db) -> Result<(), Error> {
|
||||
async fn up<Db: DbHandle>(&self, _db: &mut Db, _secrets: &PgPool) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
async fn down<Db: DbHandle>(&self, _db: &mut Db) -> Result<(), Error> {
|
||||
async fn down<Db: DbHandle>(&self, _db: &mut Db, _secrets: &PgPool) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,10 +18,10 @@ impl VersionT for Version {
|
||||
fn compat(&self) -> &'static VersionRange {
|
||||
&*v0_3_0::V0_3_0_COMPAT
|
||||
}
|
||||
async fn up<Db: DbHandle>(&self, _db: &mut Db) -> Result<(), Error> {
|
||||
async fn up<Db: DbHandle>(&self, _db: &mut Db, _secrets: &PgPool) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
async fn down<Db: DbHandle>(&self, _db: &mut Db) -> Result<(), Error> {
|
||||
async fn down<Db: DbHandle>(&self, _db: &mut Db, _secrets: &PgPool) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,10 +19,10 @@ impl VersionT for Version {
|
||||
fn compat(&self) -> &'static VersionRange {
|
||||
&*V0_3_0_COMPAT
|
||||
}
|
||||
async fn up<Db: DbHandle>(&self, _db: &mut Db) -> Result<(), Error> {
|
||||
async fn up<Db: DbHandle>(&self, _db: &mut Db, _secrets: &PgPool) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
async fn down<Db: DbHandle>(&self, _db: &mut Db) -> Result<(), Error> {
|
||||
async fn down<Db: DbHandle>(&self, _db: &mut Db, _secrets: &PgPool) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,10 +19,10 @@ impl VersionT for Version {
|
||||
fn compat(&self) -> &'static VersionRange {
|
||||
&*V0_3_0_COMPAT
|
||||
}
|
||||
async fn up<Db: DbHandle>(&self, _db: &mut Db) -> Result<(), Error> {
|
||||
async fn up<Db: DbHandle>(&self, _db: &mut Db, _secrets: &PgPool) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
async fn down<Db: DbHandle>(&self, _db: &mut Db) -> Result<(), Error> {
|
||||
async fn down<Db: DbHandle>(&self, _db: &mut Db, _secrets: &PgPool) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,10 +19,10 @@ impl VersionT for Version {
|
||||
fn compat(&self) -> &'static VersionRange {
|
||||
&*V0_3_0_COMPAT
|
||||
}
|
||||
async fn up<Db: DbHandle>(&self, _db: &mut Db) -> Result<(), Error> {
|
||||
async fn up<Db: DbHandle>(&self, _db: &mut Db, _secrets: &PgPool) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
async fn down<Db: DbHandle>(&self, _db: &mut Db) -> Result<(), Error> {
|
||||
async fn down<Db: DbHandle>(&self, _db: &mut Db, _secrets: &PgPool) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ use emver::VersionRange;
|
||||
|
||||
use super::v0_3_0::V0_3_0_COMPAT;
|
||||
use super::*;
|
||||
use crate::config::util::MergeWith;
|
||||
use crate::hostname::{generate_id, sync_hostname};
|
||||
|
||||
const V0_3_2: emver::Version = emver::Version::new(0, 3, 2, 0);
|
||||
|
||||
@@ -42,19 +40,117 @@ impl VersionT for Version {
|
||||
fn compat(&self) -> &'static VersionRange {
|
||||
&*V0_3_0_COMPAT
|
||||
}
|
||||
async fn up<Db: DbHandle>(&self, db: &mut Db) -> Result<(), Error> {
|
||||
let receipts = crate::hostname::HostNameReceipt::new(db).await?;
|
||||
crate::hostname::ensure_hostname_is_set(db, &receipts).await?;
|
||||
receipts.id.set(db, generate_id()).await?;
|
||||
async fn up<Db: DbHandle>(&self, db: &mut Db, _secrets: &PgPool) -> Result<(), Error> {
|
||||
let hostname = legacy::hostname::get_hostname(db).await?;
|
||||
crate::db::DatabaseModel::new()
|
||||
.server_info()
|
||||
.hostname()
|
||||
.put(db, &Some(hostname.0))
|
||||
.await?;
|
||||
crate::db::DatabaseModel::new()
|
||||
.server_info()
|
||||
.id()
|
||||
.put(db, &legacy::hostname::generate_id())
|
||||
.await?;
|
||||
|
||||
let mut ui = crate::db::DatabaseModel::new().ui().get(db).await?.clone();
|
||||
ui.merge_with(&DEFAULT_UI);
|
||||
crate::db::DatabaseModel::new().ui().put(db, &ui).await?;
|
||||
|
||||
sync_hostname(db, &receipts).await?;
|
||||
legacy::hostname::sync_hostname(db).await?;
|
||||
Ok(())
|
||||
}
|
||||
async fn down<Db: DbHandle>(&self, _db: &mut Db) -> Result<(), Error> {
|
||||
async fn down<Db: DbHandle>(&self, _db: &mut Db, _secrets: &PgPool) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
mod legacy {
|
||||
pub mod hostname {
|
||||
use patch_db::DbHandle;
|
||||
use rand::{thread_rng, Rng};
|
||||
use tokio::process::Command;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::util::Invoke;
|
||||
use crate::{Error, ErrorKind};
|
||||
#[derive(Clone, serde::Deserialize, serde::Serialize, Debug)]
|
||||
pub struct Hostname(pub String);
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref ADJECTIVES: Vec<String> = include_str!("../assets/adjectives.txt").lines().map(|x| x.to_string()).collect();
|
||||
static ref NOUNS: Vec<String> = include_str!("../assets/nouns.txt").lines().map(|x| x.to_string()).collect();
|
||||
}
|
||||
impl AsRef<str> for Hostname {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_hostname() -> Hostname {
|
||||
let mut rng = thread_rng();
|
||||
let adjective = &ADJECTIVES[rng.gen_range(0..ADJECTIVES.len())];
|
||||
let noun = &NOUNS[rng.gen_range(0..NOUNS.len())];
|
||||
Hostname(format!("embassy-{adjective}-{noun}"))
|
||||
}
|
||||
|
||||
pub fn generate_id() -> String {
|
||||
let id = uuid::Uuid::new_v4();
|
||||
id.to_string()
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub async fn get_current_hostname() -> Result<Hostname, Error> {
|
||||
let out = Command::new("hostname")
|
||||
.invoke(ErrorKind::ParseSysInfo)
|
||||
.await?;
|
||||
let out_string = String::from_utf8(out)?;
|
||||
Ok(Hostname(out_string.trim().to_owned()))
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub async fn set_hostname(hostname: &Hostname) -> Result<(), Error> {
|
||||
let hostname: &String = &hostname.0;
|
||||
let _out = Command::new("hostnamectl")
|
||||
.arg("set-hostname")
|
||||
.arg(hostname)
|
||||
.invoke(ErrorKind::ParseSysInfo)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(handle))]
|
||||
pub async fn get_id<Db: DbHandle>(handle: &mut Db) -> Result<String, Error> {
|
||||
let id = crate::db::DatabaseModel::new()
|
||||
.server_info()
|
||||
.id()
|
||||
.get(handle)
|
||||
.await?;
|
||||
Ok(id.to_string())
|
||||
}
|
||||
|
||||
pub async fn get_hostname<Db: DbHandle>(handle: &mut Db) -> Result<Hostname, Error> {
|
||||
if let Ok(hostname) = crate::db::DatabaseModel::new()
|
||||
.server_info()
|
||||
.hostname()
|
||||
.get(handle)
|
||||
.await
|
||||
{
|
||||
if let Some(hostname) = hostname.to_owned() {
|
||||
return Ok(Hostname(hostname));
|
||||
}
|
||||
}
|
||||
let id = get_id(handle).await?;
|
||||
if id.len() != 8 {
|
||||
return Ok(generate_hostname());
|
||||
}
|
||||
return Ok(Hostname(format!("embassy-{}", id)));
|
||||
}
|
||||
#[instrument(skip(handle))]
|
||||
pub async fn sync_hostname<Db: DbHandle>(handle: &mut Db) -> Result<(), Error> {
|
||||
set_hostname(&get_hostname(handle).await?).await?;
|
||||
Command::new("systemctl")
|
||||
.arg("restart")
|
||||
.arg("avahi-daemon")
|
||||
.invoke(crate::ErrorKind::Network)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,10 +17,10 @@ impl VersionT for Version {
|
||||
fn compat(&self) -> &'static emver::VersionRange {
|
||||
&*V0_3_0_COMPAT
|
||||
}
|
||||
async fn up<Db: DbHandle>(&self, _db: &mut Db) -> Result<(), Error> {
|
||||
async fn up<Db: DbHandle>(&self, _db: &mut Db, _secrets: &PgPool) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
async fn down<Db: DbHandle>(&self, _db: &mut Db) -> Result<(), Error> {
|
||||
async fn down<Db: DbHandle>(&self, _db: &mut Db, _secrets: &PgPool) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ impl VersionT for Version {
|
||||
fn compat(&self) -> &'static VersionRange {
|
||||
&*V0_3_0_COMPAT
|
||||
}
|
||||
async fn up<Db: DbHandle>(&self, db: &mut Db) -> Result<(), Error> {
|
||||
async fn up<Db: DbHandle>(&self, db: &mut Db, _secrets: &PgPool) -> Result<(), Error> {
|
||||
let mut ui = crate::db::DatabaseModel::new().ui().get_mut(db).await?;
|
||||
|
||||
if let Some(Value::String(selected_url)) =
|
||||
@@ -65,7 +65,7 @@ impl VersionT for Version {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
async fn down<Db: DbHandle>(&self, db: &mut Db) -> Result<(), Error> {
|
||||
async fn down<Db: DbHandle>(&self, db: &mut Db, _secrets: &PgPool) -> Result<(), Error> {
|
||||
let mut ui = crate::db::DatabaseModel::new().ui().get_mut(db).await?;
|
||||
let selected_url = ui["marketplace"]["selected-url"]
|
||||
.as_str()
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
use async_trait::async_trait;
|
||||
use emver::VersionRange;
|
||||
use itertools::Itertools;
|
||||
use openssl::hash::MessageDigest;
|
||||
use serde_json::{json, Value};
|
||||
use ssh_key::public::Ed25519PublicKey;
|
||||
|
||||
use crate::account::AccountInfo;
|
||||
use crate::hostname::{generate_hostname, sync_hostname, Hostname};
|
||||
|
||||
use super::v0_3_0::V0_3_0_COMPAT;
|
||||
use super::*;
|
||||
@@ -37,7 +43,44 @@ impl VersionT for Version {
|
||||
fn compat(&self) -> &'static VersionRange {
|
||||
&*V0_3_0_COMPAT
|
||||
}
|
||||
async fn up<Db: DbHandle>(&self, db: &mut Db) -> Result<(), Error> {
|
||||
async fn up<Db: DbHandle>(&self, db: &mut Db, secrets: &PgPool) -> Result<(), Error> {
|
||||
let mut account = AccountInfo::load(secrets).await?;
|
||||
crate::db::DatabaseModel::new()
|
||||
.server_info()
|
||||
.pubkey()
|
||||
.put(
|
||||
db,
|
||||
&ssh_key::PublicKey::from(Ed25519PublicKey::from(&account.key.ssh_key()))
|
||||
.to_openssh()?,
|
||||
)
|
||||
.await?;
|
||||
crate::db::DatabaseModel::new()
|
||||
.server_info()
|
||||
.ca_fingerprint()
|
||||
.put(
|
||||
db,
|
||||
&account
|
||||
.root_ca_cert
|
||||
.digest(MessageDigest::sha256())
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|x| format!("{x:X}"))
|
||||
.join(":"),
|
||||
)
|
||||
.await?;
|
||||
let server_info = crate::db::DatabaseModel::new()
|
||||
.server_info()
|
||||
.get(db)
|
||||
.await?
|
||||
.into_owned();
|
||||
account.hostname = server_info
|
||||
.hostname
|
||||
.map(Hostname)
|
||||
.unwrap_or_else(generate_hostname);
|
||||
account.server_id = server_info.id;
|
||||
account.save(secrets).await?;
|
||||
sync_hostname(&account).await?;
|
||||
|
||||
let parsed_url = Some(COMMUNITY_URL.parse().unwrap());
|
||||
let mut ui = crate::db::DatabaseModel::new().ui().get_mut(db).await?;
|
||||
ui["marketplace"]["known-hosts"][COMMUNITY_URL] = json!({});
|
||||
@@ -66,7 +109,7 @@ impl VersionT for Version {
|
||||
ui.save(db).await?;
|
||||
Ok(())
|
||||
}
|
||||
async fn down<Db: DbHandle>(&self, db: &mut Db) -> Result<(), Error> {
|
||||
async fn down<Db: DbHandle>(&self, db: &mut Db, _secrets: &PgPool) -> Result<(), Error> {
|
||||
let mut ui = crate::db::DatabaseModel::new().ui().get_mut(db).await?;
|
||||
let parsed_url = Some(MAIN_REGISTRY.parse().unwrap());
|
||||
for package_id in crate::db::DatabaseModel::new()
|
||||
|
||||
Reference in New Issue
Block a user