mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
working setup flow + manifest localization
This commit is contained in:
@@ -319,6 +319,7 @@ export function makeEffects(context: EffectContext): Effects {
|
||||
}
|
||||
if (context.callbacks?.onLeaveContext)
|
||||
self.onLeaveContext(() => {
|
||||
self.constRetry = undefined
|
||||
self.isInContext = false
|
||||
self.onLeaveContext = () => {
|
||||
console.warn(
|
||||
|
||||
@@ -163,11 +163,11 @@ setup.no-backup-found:
|
||||
pl_PL: "Nie znaleziono kopii zapasowej"
|
||||
|
||||
setup.couldnt-decode-startos-password:
|
||||
en_US: "Couldn't decode startOsPassword"
|
||||
de_DE: "startOsPassword konnte nicht dekodiert werden"
|
||||
es_ES: "No se pudo decodificar startOsPassword"
|
||||
fr_FR: "Impossible de décoder startOsPassword"
|
||||
pl_PL: "Nie można zdekodować startOsPassword"
|
||||
en_US: "Couldn't decode password"
|
||||
de_DE: "Passwort konnte nicht dekodiert werden"
|
||||
es_ES: "No se pudo decodificar la contraseña"
|
||||
fr_FR: "Impossible de décoder le mot de passe"
|
||||
pl_PL: "Nie można zdekodować hasła"
|
||||
|
||||
setup.couldnt-decode-recovery-password:
|
||||
en_US: "Couldn't decode recoveryPassword"
|
||||
@@ -183,13 +183,6 @@ setup.execute-not-completed:
|
||||
fr_FR: "setup.execute ne s'est pas terminé avec succès"
|
||||
pl_PL: "setup.execute nie zakończyło się pomyślnie"
|
||||
|
||||
setup.formatting-data-drive:
|
||||
en_US: "Formatting data drive"
|
||||
de_DE: "Datenlaufwerk wird formatiert"
|
||||
es_ES: "Formateando unidad de datos"
|
||||
fr_FR: "Formatage du disque de données"
|
||||
pl_PL: "Formatowanie dysku danych"
|
||||
|
||||
setup.restoring-backup:
|
||||
en_US: "Restoring backup"
|
||||
de_DE: "Sicherung wird wiederhergestellt"
|
||||
@@ -3472,6 +3465,13 @@ help.arg.keyboard-variant:
|
||||
fr_FR: "Variante de disposition du clavier"
|
||||
pl_PL: "Wariant układu klawiatury"
|
||||
|
||||
help.arg.keyboard-keymap:
|
||||
en_US: "Console keymap for vconsole"
|
||||
de_DE: "Konsolen-Tastaturbelegung für vconsole"
|
||||
es_ES: "Mapa de teclado de consola para vconsole"
|
||||
fr_FR: "Disposition clavier de console pour vconsole"
|
||||
pl_PL: "Mapa klawiszy konsoli dla vconsole"
|
||||
|
||||
help.arg.language-code:
|
||||
en_US: "Language code"
|
||||
de_DE: "Sprachcode"
|
||||
@@ -5125,6 +5125,20 @@ about.set-gateway-enabled-for-binding:
|
||||
fr_FR: "Définir la passerelle activée pour la liaison"
|
||||
pl_PL: "Ustaw bramę jako włączoną dla powiązania"
|
||||
|
||||
about.set-keyboard:
|
||||
en_US: "Set the keyboard layout"
|
||||
de_DE: "Das Tastaturlayout festlegen"
|
||||
es_ES: "Establecer la distribución del teclado"
|
||||
fr_FR: "Définir la disposition du clavier"
|
||||
pl_PL: "Ustaw układ klawiatury"
|
||||
|
||||
about.set-language:
|
||||
en_US: "Set the system language"
|
||||
de_DE: "Die Systemsprache festlegen"
|
||||
es_ES: "Establecer el idioma del sistema"
|
||||
fr_FR: "Définir la langue du système"
|
||||
pl_PL: "Ustaw język systemu"
|
||||
|
||||
about.set-listen-address-for-webserver:
|
||||
en_US: "Set the listen address for the webserver"
|
||||
de_DE: "Adresse festlegen, auf der der Webserver lauscht"
|
||||
|
||||
@@ -23,8 +23,8 @@ use crate::progress::ProgressUnits;
|
||||
use crate::s9pk::S9pk;
|
||||
use crate::service::service_map::DownloadInstallFuture;
|
||||
use crate::setup::SetupExecuteProgress;
|
||||
use crate::system::sync_kiosk;
|
||||
use crate::util::serde::IoFormat;
|
||||
use crate::system::{save_language, sync_kiosk};
|
||||
use crate::util::serde::{IoFormat, Pem};
|
||||
use crate::{PLATFORM, PackageId};
|
||||
|
||||
#[derive(Deserialize, Serialize, Parser, TS)]
|
||||
@@ -66,7 +66,10 @@ pub async fn restore_packages_rpc(
|
||||
match async { res.await?.await }.await {
|
||||
Ok(_) => (),
|
||||
Err(err) => {
|
||||
tracing::error!("{}", t!("backup.restore.package-error", id = id, error = err));
|
||||
tracing::error!(
|
||||
"{}",
|
||||
t!("backup.restore.package-error", id = id, error = err)
|
||||
);
|
||||
tracing::debug!("{:?}", err);
|
||||
}
|
||||
}
|
||||
@@ -81,7 +84,7 @@ pub async fn restore_packages_rpc(
|
||||
pub async fn recover_full_server(
|
||||
ctx: &SetupContext,
|
||||
disk_guid: InternedString,
|
||||
start_os_password: String,
|
||||
password: String,
|
||||
recovery_source: TmpMountGuard,
|
||||
server_id: &str,
|
||||
recovery_password: &str,
|
||||
@@ -105,7 +108,7 @@ pub async fn recover_full_server(
|
||||
)?;
|
||||
|
||||
os_backup.account.password = argon2::hash_encoded(
|
||||
start_os_password.as_bytes(),
|
||||
password.as_bytes(),
|
||||
&rand::random::<[u8; 16]>()[..],
|
||||
&argon2::Config::rfc9106_low_mem(),
|
||||
)
|
||||
@@ -114,9 +117,23 @@ pub async fn recover_full_server(
|
||||
let kiosk = Some(kiosk.unwrap_or(true)).filter(|_| &*PLATFORM != "raspberrypi");
|
||||
sync_kiosk(kiosk).await?;
|
||||
|
||||
let language = ctx.language.peek(|a| a.clone());
|
||||
let keyboard = ctx.keyboard.peek(|a| a.clone());
|
||||
|
||||
if let Some(language) = &language {
|
||||
save_language(&**language).await?;
|
||||
}
|
||||
|
||||
if let Some(keyboard) = &keyboard {
|
||||
keyboard.save().await?;
|
||||
}
|
||||
|
||||
let db = ctx.db().await?;
|
||||
db.put(&ROOT, &Database::init(&os_backup.account, kiosk)?)
|
||||
.await?;
|
||||
db.put(
|
||||
&ROOT,
|
||||
&Database::init(&os_backup.account, kiosk, language, keyboard)?,
|
||||
)
|
||||
.await?;
|
||||
drop(db);
|
||||
|
||||
let config = ctx.config.peek(|c| c.clone());
|
||||
@@ -150,7 +167,10 @@ pub async fn recover_full_server(
|
||||
match async { res.await?.await }.await {
|
||||
Ok(_) => (),
|
||||
Err(err) => {
|
||||
tracing::error!("{}", t!("backup.restore.package-error", id = id, error = err));
|
||||
tracing::error!(
|
||||
"{}",
|
||||
t!("backup.restore.package-error", id = id, error = err)
|
||||
);
|
||||
tracing::debug!("{:?}", err);
|
||||
}
|
||||
}
|
||||
@@ -160,7 +180,14 @@ pub async fn recover_full_server(
|
||||
.await;
|
||||
restore_phase.lock().await.complete();
|
||||
|
||||
Ok(((&os_backup.account).try_into()?, rpc_ctx))
|
||||
Ok((
|
||||
SetupResult {
|
||||
hostname: os_backup.account.hostname,
|
||||
root_ca: Pem(os_backup.account.root_ca_cert),
|
||||
needs_restart: ctx.install_rootfs.peek(|a| a.is_some()),
|
||||
},
|
||||
rpc_ctx,
|
||||
))
|
||||
}
|
||||
|
||||
#[instrument(skip(ctx, backup_guard))]
|
||||
|
||||
@@ -12,22 +12,27 @@ pub mod start_init;
|
||||
pub mod startd;
|
||||
pub mod tunnel;
|
||||
|
||||
pub fn set_locale() {
|
||||
pub fn set_locale_from_env() {
|
||||
let lang = std::env::var("LANG").ok();
|
||||
let lang = lang
|
||||
.as_deref()
|
||||
.map_or("C", |l| l.strip_suffix(".UTF-8").unwrap_or(l));
|
||||
let mut set_lang = lang;
|
||||
set_locale(lang)
|
||||
}
|
||||
|
||||
pub fn set_locale(lang: &str) {
|
||||
let mut best = None;
|
||||
let prefix = lang.split_inclusive("_").next().unwrap();
|
||||
for l in rust_i18n::available_locales!() {
|
||||
if l == lang {
|
||||
set_lang = l;
|
||||
best = Some(l);
|
||||
break;
|
||||
}
|
||||
if l.split("_").next().unwrap() == lang.split("_").next().unwrap() {
|
||||
set_lang = l;
|
||||
if best.is_none() && l.starts_with(prefix) {
|
||||
best = Some(l);
|
||||
}
|
||||
}
|
||||
rust_i18n::set_locale(set_lang);
|
||||
rust_i18n::set_locale(best.unwrap_or(lang));
|
||||
}
|
||||
|
||||
pub fn translate_cli(mut cmd: clap::Command) -> clap::Command {
|
||||
@@ -144,7 +149,7 @@ impl MultiExecutable {
|
||||
}
|
||||
|
||||
pub fn execute(&self) {
|
||||
set_locale();
|
||||
set_locale_from_env();
|
||||
|
||||
let mut popped = Vec::with_capacity(2);
|
||||
let mut args = std::env::args_os().collect::<VecDeque<_>>();
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::time::Duration;
|
||||
use futures::{Future, StreamExt};
|
||||
use imbl_value::InternedString;
|
||||
use josekit::jwk::Jwk;
|
||||
use openssl::x509::X509;
|
||||
use patch_db::PatchDb;
|
||||
use rpc_toolkit::Context;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -15,10 +16,9 @@ use tracing::instrument;
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::MAIN_DATA;
|
||||
use crate::account::AccountInfo;
|
||||
use crate::context::RpcContext;
|
||||
use crate::context::config::ServerConfig;
|
||||
use crate::disk::mount::guard::TmpMountGuard;
|
||||
use crate::disk::mount::guard::{MountGuard, TmpMountGuard};
|
||||
use crate::hostname::Hostname;
|
||||
use crate::net::gateway::UpgradableListener;
|
||||
use crate::net::web_server::{WebServer, WebServerAcceptorSetter};
|
||||
@@ -27,7 +27,9 @@ use crate::progress::FullProgressTracker;
|
||||
use crate::rpc_continuations::{Guid, RpcContinuation, RpcContinuations};
|
||||
use crate::setup::SetupProgress;
|
||||
use crate::shutdown::Shutdown;
|
||||
use crate::system::KeyboardOptions;
|
||||
use crate::util::future::NonDetachingJoinHandle;
|
||||
use crate::util::serde::Pem;
|
||||
use crate::util::sync::SyncMutex;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
@@ -42,27 +44,10 @@ lazy_static::lazy_static! {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct SetupResult {
|
||||
pub tor_addresses: Vec<String>,
|
||||
#[ts(type = "string")]
|
||||
pub hostname: Hostname,
|
||||
#[ts(type = "string")]
|
||||
pub lan_address: InternedString,
|
||||
pub root_ca: String,
|
||||
}
|
||||
impl TryFrom<&AccountInfo> for SetupResult {
|
||||
type Error = Error;
|
||||
fn try_from(value: &AccountInfo) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
tor_addresses: value
|
||||
.tor_keys
|
||||
.iter()
|
||||
.map(|tor_key| format!("https://{}", tor_key.onion_address()))
|
||||
.collect(),
|
||||
hostname: value.hostname.clone(),
|
||||
lan_address: value.hostname.lan_address(),
|
||||
root_ca: String::from_utf8(value.root_ca_cert.to_pem()?)?,
|
||||
})
|
||||
}
|
||||
pub root_ca: Pem<X509>,
|
||||
pub needs_restart: bool,
|
||||
}
|
||||
|
||||
pub struct SetupContextSeed {
|
||||
@@ -75,7 +60,9 @@ pub struct SetupContextSeed {
|
||||
pub disk_guid: OnceCell<InternedString>,
|
||||
pub shutdown: Sender<Option<Shutdown>>,
|
||||
pub rpc_continuations: RpcContinuations,
|
||||
pub install_rootfs: SyncMutex<Option<TmpMountGuard>>,
|
||||
pub install_rootfs: SyncMutex<Option<(TmpMountGuard, MountGuard)>>,
|
||||
pub keyboard: SyncMutex<Option<KeyboardOptions>>,
|
||||
pub language: SyncMutex<Option<InternedString>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -100,6 +87,8 @@ impl SetupContext {
|
||||
shutdown,
|
||||
rpc_continuations: RpcContinuations::new(),
|
||||
install_rootfs: SyncMutex::new(None),
|
||||
language: SyncMutex::new(None),
|
||||
keyboard: SyncMutex::new(None),
|
||||
})))
|
||||
}
|
||||
#[instrument(skip_all)]
|
||||
@@ -129,7 +118,10 @@ impl SetupContext {
|
||||
Ok(res)
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("{}", t!("context.setup.setup-failed", error = e));
|
||||
tracing::error!(
|
||||
"{}",
|
||||
t!("context.setup.setup-failed", error = e)
|
||||
);
|
||||
tracing::debug!("{e:?}");
|
||||
Err(e)
|
||||
}
|
||||
@@ -142,7 +134,10 @@ impl SetupContext {
|
||||
)
|
||||
.map_err(|_| {
|
||||
if self.result.initialized() {
|
||||
Error::new(eyre!("{}", t!("context.setup.setup-already-complete")), ErrorKind::InvalidRequest)
|
||||
Error::new(
|
||||
eyre!("{}", t!("context.setup.setup-already-complete")),
|
||||
ErrorKind::InvalidRequest,
|
||||
)
|
||||
} else {
|
||||
Error::new(
|
||||
eyre!("{}", t!("context.setup.setup-already-in-progress")),
|
||||
|
||||
@@ -14,6 +14,7 @@ use crate::notifications::Notifications;
|
||||
use crate::prelude::*;
|
||||
use crate::sign::AnyVerifyingKey;
|
||||
use crate::ssh::SshKeys;
|
||||
use crate::system::KeyboardOptions;
|
||||
use crate::util::serde::Pem;
|
||||
|
||||
pub mod package;
|
||||
@@ -28,9 +29,14 @@ pub struct Database {
|
||||
pub private: Private,
|
||||
}
|
||||
impl Database {
|
||||
pub fn init(account: &AccountInfo, kiosk: Option<bool>) -> Result<Self, Error> {
|
||||
pub fn init(
|
||||
account: &AccountInfo,
|
||||
kiosk: Option<bool>,
|
||||
language: Option<InternedString>,
|
||||
keyboard: Option<KeyboardOptions>,
|
||||
) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
public: Public::init(account, kiosk)?,
|
||||
public: Public::init(account, kiosk, language, keyboard)?,
|
||||
private: Private {
|
||||
key_store: KeyStore::new(account)?,
|
||||
password: account.password.clone(),
|
||||
|
||||
@@ -417,8 +417,7 @@ impl Map for CurrentDependencies {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[model = "Model<Self>"]
|
||||
pub struct CurrentDependencyInfo {
|
||||
#[ts(type = "string | null")]
|
||||
pub title: Option<InternedString>,
|
||||
pub title: Option<String>,
|
||||
pub icon: Option<DataUrl<'static>>,
|
||||
#[serde(flatten)]
|
||||
pub kind: CurrentDependencyKind,
|
||||
|
||||
@@ -45,7 +45,12 @@ pub struct Public {
|
||||
pub ui: Value,
|
||||
}
|
||||
impl Public {
|
||||
pub fn init(account: &AccountInfo, kiosk: Option<bool>) -> Result<Self, Error> {
|
||||
pub fn init(
|
||||
account: &AccountInfo,
|
||||
kiosk: Option<bool>,
|
||||
language: Option<InternedString>,
|
||||
keyboard: Option<KeyboardOptions>,
|
||||
) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
server_info: ServerInfo {
|
||||
arch: get_arch(),
|
||||
@@ -139,8 +144,8 @@ impl Public {
|
||||
ram: 0,
|
||||
devices: Vec::new(),
|
||||
kiosk,
|
||||
language: None,
|
||||
keyboard: None,
|
||||
language,
|
||||
keyboard,
|
||||
},
|
||||
package_data: AllPackageData::default(),
|
||||
ui: serde_json::from_str(*DB_UI_SEED_CELL.get().unwrap_or(&"null"))
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
|
||||
use imbl_value::InternedString;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::s9pk::manifest::LocaleString;
|
||||
use crate::util::PathOrUrl;
|
||||
use crate::{Error, PackageId};
|
||||
|
||||
@@ -28,7 +28,7 @@ impl Map for Dependencies {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[model = "Model<Self>"]
|
||||
pub struct DepInfo {
|
||||
pub description: Option<String>,
|
||||
pub description: Option<LocaleString>,
|
||||
pub optional: bool,
|
||||
#[serde(flatten)]
|
||||
pub metadata: Option<MetadataSrc>,
|
||||
@@ -73,7 +73,7 @@ pub enum MetadataSrc {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct Metadata {
|
||||
pub title: InternedString,
|
||||
pub title: LocaleString,
|
||||
pub icon: PathOrUrl,
|
||||
}
|
||||
|
||||
@@ -82,5 +82,5 @@ pub struct Metadata {
|
||||
#[model = "Model<Self>"]
|
||||
pub struct DependencyMetadata {
|
||||
#[ts(type = "string")]
|
||||
pub title: InternedString,
|
||||
pub title: LocaleString,
|
||||
}
|
||||
|
||||
@@ -377,6 +377,20 @@ pub fn server<C: Context>() -> ParentHandler<C> {
|
||||
"host",
|
||||
net::host::server_host_api::<C>().with_about("about.commands-host-system-ui"),
|
||||
)
|
||||
.subcommand(
|
||||
"set-keyboard",
|
||||
from_fn_async(system::set_keyboard)
|
||||
.no_display()
|
||||
.with_about("about.set-keyboard")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"set-language",
|
||||
from_fn_async(system::set_language)
|
||||
.no_display()
|
||||
.with_about("about.set-language")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn package<C: Context>() -> ParentHandler<C> {
|
||||
|
||||
@@ -78,37 +78,6 @@ pub async fn partition(
|
||||
|
||||
gpt.update_partitions(Default::default())?;
|
||||
|
||||
// Calculate where the OS partitions will end
|
||||
// EFI/BIOS: 100MB or 8MB, Boot: 1GB, Root: 15GB
|
||||
let efi_size_sectors = if use_efi {
|
||||
100 * 1024 * 1024 / 512
|
||||
} else {
|
||||
8 * 1024 * 1024 / 512
|
||||
};
|
||||
let boot_size_sectors = 1024 * 1024 * 1024 / 512;
|
||||
let root_size_sectors = 15 * 1024 * 1024 * 1024 / 512;
|
||||
// GPT typically starts partitions at sector 2048
|
||||
let os_partitions_end_sector =
|
||||
2048 + efi_size_sectors + boot_size_sectors + root_size_sectors;
|
||||
|
||||
// Check if protected partition would be overwritten
|
||||
if let Some((first_lba, _, ref path)) = protected_partition_info {
|
||||
if first_lba < os_partitions_end_sector {
|
||||
return Err(Error::new(
|
||||
eyre!(
|
||||
concat!(
|
||||
"Protected partition {} starts at sector {}",
|
||||
" which would be overwritten by OS partitions ending at sector {}"
|
||||
),
|
||||
path.display(),
|
||||
first_lba,
|
||||
os_partitions_end_sector
|
||||
),
|
||||
crate::ErrorKind::DiskManagement,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let efi = if use_efi {
|
||||
gpt.add_partition("efi", 100 * 1024 * 1024, gpt::partition_types::EFI, 0, None)?;
|
||||
true
|
||||
@@ -141,6 +110,30 @@ pub async fn partition(
|
||||
None,
|
||||
)?;
|
||||
|
||||
// Check if protected partition would be overwritten by OS partitions
|
||||
if let Some((first_lba, _, ref path)) = protected_partition_info {
|
||||
// Get the actual end sector of the last OS partition (root = partition 3)
|
||||
let os_partitions_end_sector = gpt
|
||||
.partitions()
|
||||
.get(&3)
|
||||
.map(|p| p.last_lba)
|
||||
.unwrap_or(0);
|
||||
if first_lba <= os_partitions_end_sector {
|
||||
return Err(Error::new(
|
||||
eyre!(
|
||||
concat!(
|
||||
"Protected partition {} starts at sector {}",
|
||||
" which would be overwritten by OS partitions ending at sector {}"
|
||||
),
|
||||
path.display(),
|
||||
first_lba,
|
||||
os_partitions_end_sector
|
||||
),
|
||||
crate::ErrorKind::DiskManagement,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let data_part = if let Some((first_lba, last_lba, path)) = protected_partition_info {
|
||||
// Re-create the data partition entry at the same location
|
||||
let length_lba = last_lba - first_lba + 1;
|
||||
|
||||
@@ -454,20 +454,25 @@ pub async fn install_os(
|
||||
)
|
||||
})?;
|
||||
}
|
||||
if let Some(guid) = disks.iter().find_map(|d| {
|
||||
d.guid
|
||||
.as_ref()
|
||||
.filter(|_| &d.logicalname == logicalname)
|
||||
.cloned()
|
||||
.or_else(|| {
|
||||
d.partitions.iter().find_map(|p| {
|
||||
p.guid
|
||||
.as_ref()
|
||||
.filter(|_| &p.logicalname == logicalname)
|
||||
.cloned()
|
||||
if let Some(guid) = (!data_drive.wipe)
|
||||
.then(|| disks.iter())
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.find_map(|d| {
|
||||
d.guid
|
||||
.as_ref()
|
||||
.filter(|_| &d.logicalname == logicalname)
|
||||
.cloned()
|
||||
.or_else(|| {
|
||||
d.partitions.iter().find_map(|p| {
|
||||
p.guid
|
||||
.as_ref()
|
||||
.filter(|_| &p.logicalname == logicalname)
|
||||
.cloned()
|
||||
})
|
||||
})
|
||||
})
|
||||
}) {
|
||||
})
|
||||
{
|
||||
setup_info.guid = Some(guid);
|
||||
setup_info.attach = true;
|
||||
} else {
|
||||
@@ -476,13 +481,20 @@ pub async fn install_os(
|
||||
}
|
||||
}
|
||||
|
||||
let config = MountGuard::mount(
|
||||
&Bind::new(rootfs.path().join("config")),
|
||||
"/media/startos/config",
|
||||
ReadWrite,
|
||||
)
|
||||
.await?;
|
||||
|
||||
write_file_atomic(
|
||||
rootfs.path().join("config/setup.json"),
|
||||
"/media/startos/config/setup.json",
|
||||
IoFormat::JsonPretty.to_vec(&setup_info)?,
|
||||
)
|
||||
.await?;
|
||||
|
||||
ctx.install_rootfs.replace(Some(rootfs));
|
||||
ctx.install_rootfs.replace(Some((rootfs, config)));
|
||||
|
||||
Ok(setup_info)
|
||||
}
|
||||
|
||||
@@ -44,28 +44,44 @@ impl DeviceInfo {
|
||||
impl DeviceInfo {
|
||||
pub fn to_header_value(&self) -> HeaderValue {
|
||||
let mut url: Url = "http://localhost".parse().unwrap();
|
||||
url.query_pairs_mut()
|
||||
.append_pair("os.version", &self.os.version.to_string())
|
||||
let mut qp = url.query_pairs_mut();
|
||||
qp.append_pair("os.version", &self.os.version.to_string())
|
||||
.append_pair("os.compat", &self.os.compat.to_string())
|
||||
.append_pair("os.platform", &*self.os.platform);
|
||||
if let Some(lang) = self.os.language.as_deref() {
|
||||
qp.append_pair("os.language", lang);
|
||||
}
|
||||
drop(qp);
|
||||
|
||||
HeaderValue::from_str(url.query().unwrap_or_default()).unwrap()
|
||||
}
|
||||
pub fn from_header_value(header: &HeaderValue) -> Result<Self, Error> {
|
||||
let query: BTreeMap<_, _> = form_urlencoded::parse(header.as_bytes()).collect();
|
||||
let has_hw_info = query.keys().any(|k| k.starts_with("hardware."));
|
||||
let version = query
|
||||
.get("os.version")
|
||||
.or_not_found("os.version")?
|
||||
.parse()?;
|
||||
Ok(Self {
|
||||
os: OsInfo {
|
||||
version: query
|
||||
.get("os.version")
|
||||
.or_not_found("os.version")?
|
||||
.parse()?,
|
||||
compat: query.get("os.compat").or_not_found("os.compat")?.parse()?,
|
||||
platform: query
|
||||
.get("os.platform")
|
||||
.or_not_found("os.platform")?
|
||||
.deref()
|
||||
.into(),
|
||||
language: query
|
||||
.get("os.language")
|
||||
.map(|v| v.deref())
|
||||
.map(InternedString::intern)
|
||||
.or_else(|| {
|
||||
if version < "0.4.0-alpha.18".parse().ok()? {
|
||||
Some(rust_i18n::locale().deref().into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}),
|
||||
version,
|
||||
},
|
||||
hardware: has_hw_info
|
||||
.then(|| {
|
||||
@@ -190,8 +206,8 @@ pub struct OsInfo {
|
||||
pub version: Version,
|
||||
#[ts(type = "string")]
|
||||
pub compat: VersionRange,
|
||||
#[ts(type = "string")]
|
||||
pub platform: InternedString,
|
||||
pub language: Option<InternedString>,
|
||||
}
|
||||
impl From<&RpcContext> for OsInfo {
|
||||
fn from(_: &RpcContext) -> Self {
|
||||
@@ -199,6 +215,7 @@ impl From<&RpcContext> for OsInfo {
|
||||
version: crate::version::Current::default().semver(),
|
||||
compat: crate::version::Current::default().compat().clone(),
|
||||
platform: InternedString::intern(&*crate::PLATFORM),
|
||||
language: Some(InternedString::intern(&*rust_i18n::locale())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ use crate::context::CliContext;
|
||||
use crate::prelude::*;
|
||||
use crate::registry::context::RegistryContext;
|
||||
use crate::registry::package::index::Category;
|
||||
use crate::s9pk::manifest::LocaleString;
|
||||
use crate::util::serde::{HandlerExtSerde, WithIoFormat, display_serializable};
|
||||
|
||||
pub fn category_api<C: Context>() -> ParentHandler<C> {
|
||||
@@ -66,7 +67,7 @@ pub fn category_api<C: Context>() -> ParentHandler<C> {
|
||||
pub struct AddCategoryParams {
|
||||
#[ts(type = "string")]
|
||||
pub id: InternedString,
|
||||
pub name: String,
|
||||
pub name: LocaleString,
|
||||
}
|
||||
|
||||
pub async fn add_category(
|
||||
@@ -196,7 +197,7 @@ pub fn display_categories<T>(
|
||||
"NAME",
|
||||
]);
|
||||
for (id, info) in categories {
|
||||
table.add_row(row![&*id, &info.name]);
|
||||
table.add_row(row![&*id, &info.name.localized()]);
|
||||
}
|
||||
table.print_tty(false)?;
|
||||
Ok(())
|
||||
|
||||
@@ -79,20 +79,20 @@ pub struct GetPackageResponse {
|
||||
pub other_versions: Option<BTreeMap<VersionString, PackageInfoShort>>,
|
||||
}
|
||||
impl GetPackageResponse {
|
||||
pub fn tables(&self) -> Vec<prettytable::Table> {
|
||||
pub fn tables(self) -> Vec<prettytable::Table> {
|
||||
use prettytable::*;
|
||||
|
||||
let mut res = Vec::with_capacity(self.best.len());
|
||||
|
||||
for (version, info) in &self.best {
|
||||
let mut table = info.table(version);
|
||||
for (version, info) in self.best {
|
||||
let mut table = info.table(&version);
|
||||
|
||||
let lesser_versions: BTreeMap<_, _> = self
|
||||
.other_versions
|
||||
.as_ref()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter(|(v, _)| ***v < **version)
|
||||
.filter(|(v, _)| ***v < *version)
|
||||
.collect();
|
||||
|
||||
if !lesser_versions.is_empty() {
|
||||
@@ -121,13 +121,17 @@ pub struct GetPackageResponseFull {
|
||||
pub other_versions: BTreeMap<VersionString, PackageVersionInfo>,
|
||||
}
|
||||
impl GetPackageResponseFull {
|
||||
pub fn tables(&self) -> Vec<prettytable::Table> {
|
||||
pub fn tables(self) -> Vec<prettytable::Table> {
|
||||
let mut res = Vec::with_capacity(self.best.len());
|
||||
|
||||
let all: BTreeMap<_, _> = self.best.iter().chain(self.other_versions.iter()).collect();
|
||||
let all: BTreeMap<_, _> = self
|
||||
.best
|
||||
.into_iter()
|
||||
.chain(self.other_versions.into_iter())
|
||||
.collect();
|
||||
|
||||
for (version, info) in all {
|
||||
res.push(info.table(version));
|
||||
res.push(info.table(&version));
|
||||
}
|
||||
|
||||
res
|
||||
@@ -444,7 +448,11 @@ pub async fn cli_download(
|
||||
return Err(Error::new(
|
||||
eyre!(
|
||||
"{}",
|
||||
t!("registry.package.get.version-not-found", id = id, version = target_version.unwrap_or(VersionRange::Any))
|
||||
t!(
|
||||
"registry.package.get.version-not-found",
|
||||
id = id,
|
||||
version = target_version.unwrap_or(VersionRange::Any)
|
||||
)
|
||||
),
|
||||
ErrorKind::NotFound,
|
||||
));
|
||||
@@ -465,7 +473,11 @@ pub async fn cli_download(
|
||||
return Err(Error::new(
|
||||
eyre!(
|
||||
"{}",
|
||||
t!("registry.package.get.version-not-found", id = id, version = target_version.unwrap_or(VersionRange::Any))
|
||||
t!(
|
||||
"registry.package.get.version-not-found",
|
||||
id = id,
|
||||
version = target_version.unwrap_or(VersionRange::Any)
|
||||
)
|
||||
),
|
||||
ErrorKind::NotFound,
|
||||
));
|
||||
|
||||
@@ -17,7 +17,7 @@ use crate::registry::device_info::DeviceInfo;
|
||||
use crate::rpc_continuations::Guid;
|
||||
use crate::s9pk::S9pk;
|
||||
use crate::s9pk::git_hash::GitHash;
|
||||
use crate::s9pk::manifest::{Alerts, Description, HardwareRequirements};
|
||||
use crate::s9pk::manifest::{Alerts, Description, HardwareRequirements, LocaleString};
|
||||
use crate::s9pk::merkle_archive::source::FileSource;
|
||||
use crate::sign::commitment::merkle_archive::MerkleArchiveCommitment;
|
||||
use crate::sign::{AnySignature, AnyVerifyingKey};
|
||||
@@ -49,22 +49,27 @@ pub struct PackageInfo {
|
||||
#[model = "Model<Self>"]
|
||||
#[ts(export)]
|
||||
pub struct Category {
|
||||
pub name: String,
|
||||
pub name: LocaleString,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, HasModel, TS, PartialEq, Eq)]
|
||||
#[derive(Debug, Deserialize, Serialize, HasModel, TS, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[model = "Model<Self>"]
|
||||
#[ts(export)]
|
||||
pub struct DependencyMetadata {
|
||||
#[ts(type = "string | null")]
|
||||
pub title: Option<InternedString>,
|
||||
pub title: Option<LocaleString>,
|
||||
pub icon: Option<DataUrl<'static>>,
|
||||
pub description: Option<String>,
|
||||
pub description: Option<LocaleString>,
|
||||
pub optional: bool,
|
||||
}
|
||||
impl DependencyMetadata {
|
||||
pub fn localize_for(&mut self, locale: &str) {
|
||||
self.title.as_mut().map(|t| t.localize_for(locale));
|
||||
self.description.as_mut().map(|d| d.localize_for(locale));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, HasModel, TS, PartialEq, Eq)]
|
||||
#[derive(Debug, Deserialize, Serialize, HasModel, TS, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[model = "Model<Self>"]
|
||||
pub struct PackageMetadata {
|
||||
@@ -72,7 +77,7 @@ pub struct PackageMetadata {
|
||||
pub title: InternedString,
|
||||
pub icon: DataUrl<'static>,
|
||||
pub description: Description,
|
||||
pub release_notes: String,
|
||||
pub release_notes: LocaleString,
|
||||
pub git_hash: Option<GitHash>,
|
||||
#[ts(type = "string")]
|
||||
pub license: InternedString,
|
||||
@@ -199,20 +204,20 @@ impl PackageVersionInfo {
|
||||
self.s9pks.sort_by_key(|(h, _)| h.specificity_desc());
|
||||
Ok(())
|
||||
}
|
||||
pub fn table(&self, version: &VersionString) -> prettytable::Table {
|
||||
pub fn table(self, version: &VersionString) -> prettytable::Table {
|
||||
use prettytable::*;
|
||||
|
||||
let mut table = Table::new();
|
||||
|
||||
table.add_row(row![bc => &self.metadata.title]);
|
||||
table.add_row(row![br -> "VERSION", AsRef::<str>::as_ref(version)]);
|
||||
table.add_row(row![br -> "RELEASE NOTES", &self.metadata.release_notes]);
|
||||
table.add_row(row![br -> "RELEASE NOTES", &self.metadata.release_notes.localized()]);
|
||||
table.add_row(
|
||||
row![br -> "ABOUT", &textwrap::wrap(&self.metadata.description.short, 80).join("\n")],
|
||||
row![br -> "ABOUT", &textwrap::wrap(&self.metadata.description.short.localized(), 80).join("\n")],
|
||||
);
|
||||
table.add_row(row![
|
||||
br -> "DESCRIPTION",
|
||||
&textwrap::wrap(&self.metadata.description.long, 80).join("\n")
|
||||
&textwrap::wrap(&self.metadata.description.long.localized(), 80).join("\n")
|
||||
]);
|
||||
table.add_row(row![br -> "GIT HASH", self.metadata.git_hash.as_deref().unwrap_or("N/A")]);
|
||||
table.add_row(row![br -> "LICENSE", &self.metadata.license]);
|
||||
@@ -280,6 +285,24 @@ impl Model<PackageVersionInfo> {
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if let Some(locale) = device_info.os.language.as_deref() {
|
||||
let metadata = self.as_metadata_mut();
|
||||
metadata
|
||||
.as_alerts_mut()
|
||||
.mutate(|a| Ok(a.localize_for(locale)));
|
||||
metadata
|
||||
.as_dependency_metadata_mut()
|
||||
.as_entries_mut()?
|
||||
.into_iter()
|
||||
.try_for_each(|(_, d)| d.mutate(|d| Ok(d.localize_for(locale))));
|
||||
metadata
|
||||
.as_description_mut()
|
||||
.mutate(|d| Ok(d.localize_for(locale)))?;
|
||||
metadata
|
||||
.as_release_notes_mut()
|
||||
.mutate(|r| Ok(r.localize_for(locale)))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
|
||||
@@ -9,7 +9,7 @@ use tokio::process::Command;
|
||||
|
||||
use crate::dependencies::{DepInfo, Dependencies};
|
||||
use crate::prelude::*;
|
||||
use crate::s9pk::manifest::{DeviceFilter, Manifest};
|
||||
use crate::s9pk::manifest::{DeviceFilter, LocaleString, Manifest};
|
||||
use crate::s9pk::merkle_archive::directory_contents::DirectoryContents;
|
||||
use crate::s9pk::merkle_archive::source::TmpSource;
|
||||
use crate::s9pk::merkle_archive::{Entry, MerkleArchive};
|
||||
@@ -198,7 +198,7 @@ impl TryFrom<ManifestV1> for Manifest {
|
||||
title: format!("{} (Legacy)", value.title).into(),
|
||||
version: version.into(),
|
||||
satisfies: BTreeSet::new(),
|
||||
release_notes: value.release_notes,
|
||||
release_notes: LocaleString::Translated(value.release_notes),
|
||||
can_migrate_from: VersionRange::any(),
|
||||
can_migrate_to: VersionRange::none(),
|
||||
license: value.license.into(),
|
||||
@@ -226,7 +226,7 @@ impl TryFrom<ManifestV1> for Manifest {
|
||||
(
|
||||
id,
|
||||
DepInfo {
|
||||
description: value.description,
|
||||
description: value.description.map(LocaleString::Translated),
|
||||
optional: !value.requirement.required(),
|
||||
metadata: None,
|
||||
},
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::path::Path;
|
||||
|
||||
use clap::builder::ValueParserFactory;
|
||||
use color_eyre::eyre::eyre;
|
||||
use exver::{Version, VersionRange};
|
||||
use imbl_value::InternedString;
|
||||
use imbl_value::{InOMap, InternedString};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
use url::Url;
|
||||
@@ -17,7 +18,7 @@ use crate::s9pk::merkle_archive::expected::{Expected, Filter};
|
||||
use crate::s9pk::v2::pack::ImageConfig;
|
||||
use crate::util::lshw::{LshwDevice, LshwDisplay, LshwProcessor};
|
||||
use crate::util::serde::Regex;
|
||||
use crate::util::{VersionString, mime};
|
||||
use crate::util::{FromStrParser, VersionString, mime};
|
||||
use crate::version::{Current, VersionT};
|
||||
use crate::{ImageId, VolumeId};
|
||||
|
||||
@@ -35,7 +36,7 @@ pub struct Manifest {
|
||||
pub title: InternedString,
|
||||
pub version: VersionString,
|
||||
pub satisfies: BTreeSet<VersionString>,
|
||||
pub release_notes: String,
|
||||
pub release_notes: LocaleString,
|
||||
#[ts(type = "string")]
|
||||
pub can_migrate_to: VersionRange,
|
||||
#[ts(type = "string")]
|
||||
@@ -190,6 +191,118 @@ impl HardwareRequirements {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, TS)]
|
||||
#[ts(type = "string | Record<string, string>")]
|
||||
pub enum LocaleString {
|
||||
Translated(String),
|
||||
LanguageMap(InOMap<InternedString, String>),
|
||||
}
|
||||
impl std::str::FromStr for LocaleString {
|
||||
type Err = std::convert::Infallible;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
// Try JSON parse first (for maps or quoted strings)
|
||||
if let Ok(parsed) = serde_json::from_str::<LocaleString>(s) {
|
||||
return Ok(parsed);
|
||||
}
|
||||
// Fall back to plain string
|
||||
Ok(LocaleString::Translated(s.to_owned()))
|
||||
}
|
||||
}
|
||||
impl LocaleString {
|
||||
pub fn localize_for(&mut self, locale: &str) {
|
||||
if let Self::LanguageMap(map) = self {
|
||||
if let Some(translated) = map.remove(locale) {
|
||||
*self = Self::Translated(translated);
|
||||
return;
|
||||
}
|
||||
let prefix = locale.split_inclusive("_").next().unwrap();
|
||||
let mut first = None;
|
||||
for (lang, translated) in std::mem::take(map) {
|
||||
if lang.starts_with(prefix) {
|
||||
*self = Self::Translated(translated);
|
||||
return;
|
||||
}
|
||||
if first.is_none() {
|
||||
first = Some(translated);
|
||||
}
|
||||
}
|
||||
*self = Self::Translated(first.unwrap_or_default())
|
||||
}
|
||||
}
|
||||
pub fn localized_for(mut self, locale: &str) -> String {
|
||||
self.localize_for(locale);
|
||||
if let Self::Translated(s) = self {
|
||||
s
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
pub fn localize(&mut self) {
|
||||
self.localize_for(&*rust_i18n::locale());
|
||||
}
|
||||
pub fn localized(mut self) -> String {
|
||||
self.localized_for(&*rust_i18n::locale())
|
||||
}
|
||||
}
|
||||
impl<'de> Deserialize<'de> for LocaleString {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
struct LocaleStringVisitor;
|
||||
|
||||
impl<'de> serde::de::Visitor<'de> for LocaleStringVisitor {
|
||||
type Value = LocaleString;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("a string or a map of language codes to strings")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
Ok(LocaleString::Translated(value.to_owned()))
|
||||
}
|
||||
|
||||
fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
Ok(LocaleString::Translated(value))
|
||||
}
|
||||
|
||||
fn visit_map<M>(self, map: M) -> Result<Self::Value, M::Error>
|
||||
where
|
||||
M: serde::de::MapAccess<'de>,
|
||||
{
|
||||
let language_map =
|
||||
InOMap::deserialize(serde::de::value::MapAccessDeserializer::new(map))?;
|
||||
Ok(LocaleString::LanguageMap(language_map))
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(LocaleStringVisitor)
|
||||
}
|
||||
}
|
||||
impl Serialize for LocaleString {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
match self {
|
||||
LocaleString::Translated(s) => serializer.serialize_str(s),
|
||||
LocaleString::LanguageMap(map) => map.serialize(serializer),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl ValueParserFactory for LocaleString {
|
||||
type Parser = FromStrParser<Self>;
|
||||
fn value_parser() -> Self::Parser {
|
||||
FromStrParser::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
@@ -294,21 +407,32 @@ impl DeviceFilter {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, TS, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, TS, PartialEq)]
|
||||
#[ts(export)]
|
||||
pub struct Description {
|
||||
pub short: String,
|
||||
pub long: String,
|
||||
pub short: LocaleString,
|
||||
pub long: LocaleString,
|
||||
}
|
||||
impl Description {
|
||||
pub fn localize_for(&mut self, locale: &str) {
|
||||
self.short.localize_for(locale);
|
||||
self.long.localize_for(locale);
|
||||
}
|
||||
|
||||
pub fn validate(&self) -> Result<(), Error> {
|
||||
if self.short.chars().skip(160).next().is_some() {
|
||||
if match &self.short {
|
||||
LocaleString::Translated(s) => s.len() > 160,
|
||||
LocaleString::LanguageMap(map) => map.values().any(|s| s.len() > 160),
|
||||
} {
|
||||
return Err(Error::new(
|
||||
eyre!("Short description must be 160 characters or less."),
|
||||
crate::ErrorKind::ValidateS9pk,
|
||||
));
|
||||
}
|
||||
if self.long.chars().skip(5000).next().is_some() {
|
||||
if match &self.short {
|
||||
LocaleString::Translated(s) => s.len() > 5000,
|
||||
LocaleString::LanguageMap(map) => map.values().any(|s| s.len() > 5000),
|
||||
} {
|
||||
return Err(Error::new(
|
||||
eyre!("Long description must be 5000 characters or less."),
|
||||
crate::ErrorKind::ValidateS9pk,
|
||||
@@ -318,13 +442,22 @@ impl Description {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize, TS, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize, TS, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct Alerts {
|
||||
pub install: Option<String>,
|
||||
pub uninstall: Option<String>,
|
||||
pub restore: Option<String>,
|
||||
pub start: Option<String>,
|
||||
pub stop: Option<String>,
|
||||
pub install: Option<LocaleString>,
|
||||
pub uninstall: Option<LocaleString>,
|
||||
pub restore: Option<LocaleString>,
|
||||
pub start: Option<LocaleString>,
|
||||
pub stop: Option<LocaleString>,
|
||||
}
|
||||
impl Alerts {
|
||||
pub fn localize_for(&mut self, locale: &str) {
|
||||
self.install.as_mut().map(|s| s.localize_for(locale));
|
||||
self.uninstall.as_mut().map(|s| s.localize_for(locale));
|
||||
self.restore.as_mut().map(|s| s.localize_for(locale));
|
||||
self.start.as_mut().map(|s| s.localize_for(locale));
|
||||
self.stop.as_mut().map(|s| s.localize_for(locale));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ use crate::prelude::*;
|
||||
use crate::rpc_continuations::Guid;
|
||||
use crate::s9pk::S9pk;
|
||||
use crate::s9pk::git_hash::GitHash;
|
||||
use crate::s9pk::manifest::Manifest;
|
||||
use crate::s9pk::manifest::{LocaleString, Manifest};
|
||||
use crate::s9pk::merkle_archive::directory_contents::DirectoryContents;
|
||||
use crate::s9pk::merkle_archive::source::http::HttpSource;
|
||||
use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile;
|
||||
@@ -756,7 +756,7 @@ pub async fn pack(ctx: CliContext, params: PackParams) -> Result<(), Error> {
|
||||
}
|
||||
};
|
||||
Some((
|
||||
s9pk.as_manifest().title.clone(),
|
||||
LocaleString::Translated(s9pk.as_manifest().title.to_string()),
|
||||
s9pk.icon_data_url().await?,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ use std::str::FromStr;
|
||||
|
||||
use clap::builder::ValueParserFactory;
|
||||
use exver::VersionRange;
|
||||
use imbl_value::InternedString;
|
||||
use rust_i18n::t;
|
||||
|
||||
use crate::db::model::package::{
|
||||
@@ -149,13 +148,25 @@ impl FromStr for DependencyRequirement {
|
||||
.map(|id| id.parse().map_err(Error::from))
|
||||
.collect(),
|
||||
Some((kind, _)) => Err(Error::new(
|
||||
eyre!("{}", t!("service.effects.dependency.unknown-dependency-kind", kind = kind)),
|
||||
eyre!(
|
||||
"{}",
|
||||
t!(
|
||||
"service.effects.dependency.unknown-dependency-kind",
|
||||
kind = kind
|
||||
)
|
||||
),
|
||||
ErrorKind::InvalidRequest,
|
||||
)),
|
||||
None => match rest {
|
||||
"r" | "running" => Ok(BTreeSet::new()),
|
||||
kind => Err(Error::new(
|
||||
eyre!("{}", t!("service.effects.dependency.unknown-dependency-kind", kind = kind)),
|
||||
eyre!(
|
||||
"{}",
|
||||
t!(
|
||||
"service.effects.dependency.unknown-dependency-kind",
|
||||
kind = kind
|
||||
)
|
||||
),
|
||||
ErrorKind::InvalidRequest,
|
||||
)),
|
||||
},
|
||||
@@ -217,7 +228,7 @@ pub async fn set_dependencies(
|
||||
.s9pk
|
||||
.dependency_metadata(&dep_id)
|
||||
.await?
|
||||
.map(|m| m.title),
|
||||
.map(|m| m.title.localized()),
|
||||
icon: context
|
||||
.seed
|
||||
.persistent_container
|
||||
@@ -294,8 +305,7 @@ pub struct CheckDependenciesParam {
|
||||
#[ts(export)]
|
||||
pub struct CheckDependenciesResult {
|
||||
package_id: PackageId,
|
||||
#[ts(type = "string | null")]
|
||||
title: Option<InternedString>,
|
||||
title: Option<String>,
|
||||
installed_version: Option<VersionString>,
|
||||
satisfies: BTreeSet<VersionString>,
|
||||
is_running: bool,
|
||||
|
||||
@@ -19,6 +19,7 @@ use crate::account::AccountInfo;
|
||||
use crate::auth::write_shadow;
|
||||
use crate::backup::restore::recover_full_server;
|
||||
use crate::backup::target::BackupTargetFS;
|
||||
use crate::bins::set_locale;
|
||||
use crate::context::rpc::InitRpcContextPhases;
|
||||
use crate::context::setup::SetupResult;
|
||||
use crate::context::{RpcContext, SetupContext};
|
||||
@@ -36,11 +37,11 @@ use crate::prelude::*;
|
||||
use crate::progress::{FullProgress, PhaseProgressTrackerHandle, ProgressUnits};
|
||||
use crate::rpc_continuations::Guid;
|
||||
use crate::shutdown::Shutdown;
|
||||
use crate::system::sync_kiosk;
|
||||
use crate::system::{KeyboardOptions, SetLanguageParams, save_language, sync_kiosk};
|
||||
use crate::util::Invoke;
|
||||
use crate::util::crypto::EncryptedWire;
|
||||
use crate::util::io::{Counter, create_file, dir_copy, dir_size, read_file_to_string};
|
||||
use crate::util::serde::IoFormat;
|
||||
use crate::util::serde::{IoFormat, Pem};
|
||||
use crate::{DATA_DIR, Error, ErrorKind, MAIN_DATA, PACKAGE_DATA, PLATFORM, ResultExt};
|
||||
|
||||
pub fn setup<C: Context>() -> ParentHandler<C> {
|
||||
@@ -75,6 +76,9 @@ pub fn setup<C: Context>() -> ParentHandler<C> {
|
||||
.with_about("about.display-os-logs"),
|
||||
)
|
||||
.subcommand("restart", from_fn_async(restart).no_cli())
|
||||
.subcommand("shutdown", from_fn_async(shutdown).no_cli())
|
||||
.subcommand("set-language", from_fn_async(set_language).no_cli())
|
||||
.subcommand("set-keyboard", from_fn_async(set_keyboard).no_cli())
|
||||
}
|
||||
|
||||
pub fn disk<C: Context>() -> ParentHandler<C> {
|
||||
@@ -103,6 +107,8 @@ async fn setup_init(
|
||||
init_phases: InitPhases,
|
||||
) -> Result<(AccountInfo, InitResult), Error> {
|
||||
let init_result = init(&ctx.webserver, &ctx.config.peek(|c| c.clone()), init_phases).await?;
|
||||
let language = ctx.language.peek(|a| a.clone());
|
||||
let keyboard = ctx.keyboard.peek(|a| a.clone());
|
||||
|
||||
let account = init_result
|
||||
.net_ctrl
|
||||
@@ -118,6 +124,12 @@ async fn setup_init(
|
||||
if let Some(kiosk) = kiosk {
|
||||
info.as_kiosk_mut().ser(&Some(kiosk))?;
|
||||
}
|
||||
if let Some(language) = language.clone() {
|
||||
info.as_language_mut().ser(&Some(language))?;
|
||||
}
|
||||
if let Some(keyboard) = keyboard.clone() {
|
||||
info.as_keyboard_mut().ser(&Some(keyboard))?;
|
||||
}
|
||||
|
||||
Ok(account)
|
||||
})
|
||||
@@ -126,6 +138,13 @@ async fn setup_init(
|
||||
|
||||
sync_kiosk(kiosk).await?;
|
||||
|
||||
if let Some(language) = language {
|
||||
save_language(&*language).await?;
|
||||
}
|
||||
if let Some(keyboard) = keyboard {
|
||||
keyboard.save().await?;
|
||||
}
|
||||
|
||||
if let Some(password) = &password {
|
||||
write_shadow(&password).await?;
|
||||
}
|
||||
@@ -137,7 +156,6 @@ async fn setup_init(
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct AttachParams {
|
||||
#[serde(rename = "startOsPassword")]
|
||||
pub password: Option<EncryptedWire>,
|
||||
pub guid: InternedString,
|
||||
#[ts(optional)]
|
||||
@@ -155,63 +173,81 @@ pub async fn attach(
|
||||
) -> Result<SetupProgress, Error> {
|
||||
let setup_ctx = ctx.clone();
|
||||
ctx.run_setup(move || async move {
|
||||
let progress = &setup_ctx.progress;
|
||||
let mut disk_phase = progress.add_phase(t!("setup.opening-data-drive").into(), Some(10));
|
||||
let init_phases = InitPhases::new(&progress);
|
||||
let rpc_ctx_phases = InitRpcContextPhases::new(&progress);
|
||||
let progress = &setup_ctx.progress;
|
||||
let mut disk_phase = progress.add_phase(t!("setup.opening-data-drive").into(), Some(10));
|
||||
let init_phases = InitPhases::new(&progress);
|
||||
let rpc_ctx_phases = InitRpcContextPhases::new(&progress);
|
||||
|
||||
let password: Option<String> = match password {
|
||||
Some(a) => match a.decrypt(&setup_ctx) {
|
||||
a @ Some(_) => a,
|
||||
None => {
|
||||
return Err(Error::new(
|
||||
color_eyre::eyre::eyre!("{}", t!("setup.couldnt-decode-password")),
|
||||
crate::ErrorKind::Unknown,
|
||||
));
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
let password: Option<String> = match password {
|
||||
Some(a) => match a.decrypt(&setup_ctx) {
|
||||
a @ Some(_) => a,
|
||||
None => {
|
||||
return Err(Error::new(
|
||||
color_eyre::eyre::eyre!("{}", t!("setup.couldnt-decode-password")),
|
||||
crate::ErrorKind::Unknown,
|
||||
));
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
|
||||
disk_phase.start();
|
||||
let requires_reboot = crate::disk::main::import(
|
||||
&*disk_guid,
|
||||
DATA_DIR,
|
||||
if tokio::fs::metadata(REPAIR_DISK_PATH).await.is_ok() {
|
||||
RepairStrategy::Aggressive
|
||||
} else {
|
||||
RepairStrategy::Preen
|
||||
},
|
||||
if disk_guid.ends_with("_UNENC") { None } else { Some(DEFAULT_PASSWORD) },
|
||||
)
|
||||
.await?;
|
||||
let _ = setup_ctx.disk_guid.set(disk_guid.clone());
|
||||
disk_phase.start();
|
||||
let requires_reboot = crate::disk::main::import(
|
||||
&*disk_guid,
|
||||
DATA_DIR,
|
||||
if tokio::fs::metadata(REPAIR_DISK_PATH).await.is_ok() {
|
||||
tokio::fs::remove_file(REPAIR_DISK_PATH)
|
||||
.await
|
||||
.with_ctx(|_| (ErrorKind::Filesystem, REPAIR_DISK_PATH))?;
|
||||
}
|
||||
if requires_reboot.0 {
|
||||
crate::disk::main::export(&*disk_guid, DATA_DIR).await?;
|
||||
return Err(Error::new(
|
||||
eyre!("{}", t!("setup.disk-errors-corrected-restart-required")),
|
||||
ErrorKind::DiskManagement,
|
||||
));
|
||||
}
|
||||
disk_phase.complete();
|
||||
RepairStrategy::Aggressive
|
||||
} else {
|
||||
RepairStrategy::Preen
|
||||
},
|
||||
if disk_guid.ends_with("_UNENC") {
|
||||
None
|
||||
} else {
|
||||
Some(DEFAULT_PASSWORD)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let _ = setup_ctx.disk_guid.set(disk_guid.clone());
|
||||
if tokio::fs::metadata(REPAIR_DISK_PATH).await.is_ok() {
|
||||
tokio::fs::remove_file(REPAIR_DISK_PATH)
|
||||
.await
|
||||
.with_ctx(|_| (ErrorKind::Filesystem, REPAIR_DISK_PATH))?;
|
||||
}
|
||||
if requires_reboot.0 {
|
||||
crate::disk::main::export(&*disk_guid, DATA_DIR).await?;
|
||||
return Err(Error::new(
|
||||
eyre!("{}", t!("setup.disk-errors-corrected-restart-required")),
|
||||
ErrorKind::DiskManagement,
|
||||
));
|
||||
}
|
||||
disk_phase.complete();
|
||||
|
||||
let (account, net_ctrl) = setup_init(&setup_ctx, password, kiosk, init_phases).await?;
|
||||
let (account, net_ctrl) = setup_init(&setup_ctx, password, kiosk, init_phases).await?;
|
||||
|
||||
let rpc_ctx = RpcContext::init(&setup_ctx.webserver, &setup_ctx.config.peek(|c| c.clone()), disk_guid, Some(net_ctrl), rpc_ctx_phases).await?;
|
||||
let rpc_ctx = RpcContext::init(
|
||||
&setup_ctx.webserver,
|
||||
&setup_ctx.config.peek(|c| c.clone()),
|
||||
disk_guid,
|
||||
Some(net_ctrl),
|
||||
rpc_ctx_phases,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(((&account).try_into()?, rpc_ctx))
|
||||
})?;
|
||||
Ok((
|
||||
SetupResult {
|
||||
hostname: account.hostname,
|
||||
root_ca: Pem(account.root_ca_cert),
|
||||
needs_restart: setup_ctx.install_rootfs.peek(|a| a.is_some()),
|
||||
},
|
||||
rpc_ctx,
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(ctx.progress().await)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "status")]
|
||||
pub enum SetupStatusRes {
|
||||
@@ -246,17 +282,10 @@ pub async fn status(ctx: SetupContext) -> Result<SetupStatusRes, Error> {
|
||||
if ctx.task.initialized() {
|
||||
Ok(SetupStatusRes::Running(ctx.progress().await))
|
||||
} else {
|
||||
let path = if tokio::fs::metadata("/run/live/medium").await.is_ok() {
|
||||
let Some(path) = ctx
|
||||
.install_rootfs
|
||||
.peek(|fs| fs.as_ref().map(|fs| fs.path().join("config/setup.json")))
|
||||
else {
|
||||
return Ok(SetupStatusRes::NeedsInstall);
|
||||
};
|
||||
path
|
||||
} else {
|
||||
Path::new("/media/startos/config/setup.json").to_path_buf()
|
||||
};
|
||||
let path = Path::new("/media/startos/config/setup.json");
|
||||
if tokio::fs::metadata(path).await.is_err() {
|
||||
return Ok(SetupStatusRes::NeedsInstall);
|
||||
}
|
||||
IoFormat::Json
|
||||
.from_slice(read_file_to_string(path).await?.as_bytes())
|
||||
.map(SetupStatusRes::Incomplete)
|
||||
@@ -361,8 +390,8 @@ pub async fn setup_data_drive(
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct SetupExecuteParams {
|
||||
start_os_logicalname: PathBuf,
|
||||
start_os_password: EncryptedWire,
|
||||
guid: InternedString,
|
||||
password: EncryptedWire,
|
||||
recovery_source: Option<RecoverySource<EncryptedWire>>,
|
||||
#[ts(optional)]
|
||||
kiosk: Option<bool>,
|
||||
@@ -372,13 +401,13 @@ pub struct SetupExecuteParams {
|
||||
pub async fn execute(
|
||||
ctx: SetupContext,
|
||||
SetupExecuteParams {
|
||||
start_os_logicalname,
|
||||
start_os_password,
|
||||
guid,
|
||||
password,
|
||||
recovery_source,
|
||||
kiosk,
|
||||
}: SetupExecuteParams,
|
||||
) -> Result<SetupProgress, Error> {
|
||||
let start_os_password = match start_os_password.decrypt(&ctx) {
|
||||
let password = match password.decrypt(&ctx) {
|
||||
Some(a) => a,
|
||||
None => {
|
||||
return Err(Error::new(
|
||||
@@ -407,15 +436,7 @@ pub async fn execute(
|
||||
};
|
||||
|
||||
let setup_ctx = ctx.clone();
|
||||
ctx.run_setup(move || {
|
||||
execute_inner(
|
||||
setup_ctx,
|
||||
start_os_logicalname,
|
||||
start_os_password,
|
||||
recovery,
|
||||
kiosk,
|
||||
)
|
||||
})?;
|
||||
ctx.run_setup(move || execute_inner(setup_ctx, guid, password, recovery, kiosk))?;
|
||||
|
||||
Ok(ctx.progress().await)
|
||||
}
|
||||
@@ -447,12 +468,27 @@ pub async fn complete(ctx: SetupContext) -> Result<SetupResult, Error> {
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn exit(ctx: SetupContext) -> Result<(), Error> {
|
||||
ctx.shutdown.send(None).expect("failed to shutdown");
|
||||
let shutdown = if let Some((rootfs, config)) = ctx.install_rootfs.replace(None) {
|
||||
config.unmount(false).await?;
|
||||
rootfs.unmount().await?;
|
||||
Some(Shutdown {
|
||||
disk_guid: ctx.disk_guid.get().cloned(),
|
||||
restart: true,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
ctx.shutdown.send(shutdown).expect("failed to shutdown");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn restart(ctx: SetupContext) -> Result<(), Error> {
|
||||
if let Some((rootfs, config)) = ctx.install_rootfs.replace(None) {
|
||||
config.unmount(false).await?;
|
||||
rootfs.unmount().await?;
|
||||
}
|
||||
ctx.shutdown
|
||||
.send(Some(Shutdown {
|
||||
disk_guid: ctx.disk_guid.get().cloned(),
|
||||
@@ -462,16 +498,30 @@ pub async fn restart(ctx: SetupContext) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn shutdown(ctx: SetupContext) -> Result<(), Error> {
|
||||
if let Some((rootfs, config)) = ctx.install_rootfs.replace(None) {
|
||||
config.unmount(false).await?;
|
||||
rootfs.unmount().await?;
|
||||
}
|
||||
ctx.shutdown
|
||||
.send(Some(Shutdown {
|
||||
disk_guid: ctx.disk_guid.get().cloned(),
|
||||
restart: false,
|
||||
}))
|
||||
.expect("failed to shutdown");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn execute_inner(
|
||||
ctx: SetupContext,
|
||||
start_os_logicalname: PathBuf,
|
||||
start_os_password: String,
|
||||
guid: InternedString,
|
||||
password: String,
|
||||
recovery_source: Option<RecoverySource<String>>,
|
||||
kiosk: Option<bool>,
|
||||
) -> Result<(SetupResult, RpcContext), Error> {
|
||||
let progress = &ctx.progress;
|
||||
let mut disk_phase = progress.add_phase(t!("setup.formatting-data-drive").into(), Some(10));
|
||||
let restore_phase = match recovery_source.as_ref() {
|
||||
Some(RecoverySource::Backup { .. }) => {
|
||||
Some(progress.add_phase(t!("setup.restoring-backup").into(), Some(100)))
|
||||
@@ -484,10 +534,6 @@ pub async fn execute_inner(
|
||||
let init_phases = InitPhases::new(&progress);
|
||||
let rpc_ctx_phases = InitRpcContextPhases::new(&progress);
|
||||
|
||||
disk_phase.start();
|
||||
let guid = setup_data_drive(&ctx, &start_os_logicalname).await?;
|
||||
disk_phase.complete();
|
||||
|
||||
let progress = SetupExecuteProgress {
|
||||
init_phases,
|
||||
restore_phase,
|
||||
@@ -497,25 +543,25 @@ pub async fn execute_inner(
|
||||
match recovery_source {
|
||||
Some(RecoverySource::Backup {
|
||||
target,
|
||||
password,
|
||||
password: recovery_password,
|
||||
server_id,
|
||||
}) => {
|
||||
recover(
|
||||
&ctx,
|
||||
guid,
|
||||
start_os_password,
|
||||
password,
|
||||
target,
|
||||
server_id,
|
||||
password,
|
||||
recovery_password,
|
||||
kiosk,
|
||||
progress,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Some(RecoverySource::Migrate { guid: old_guid }) => {
|
||||
migrate(&ctx, guid, &old_guid, start_os_password, kiosk, progress).await
|
||||
migrate(&ctx, guid, &old_guid, password, kiosk, progress).await
|
||||
}
|
||||
None => fresh_setup(&ctx, guid, &start_os_password, kiosk, progress).await,
|
||||
None => fresh_setup(&ctx, guid, &password, kiosk, progress).await,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -528,7 +574,7 @@ pub struct SetupExecuteProgress {
|
||||
async fn fresh_setup(
|
||||
ctx: &SetupContext,
|
||||
guid: InternedString,
|
||||
start_os_password: &str,
|
||||
password: &str,
|
||||
kiosk: Option<bool>,
|
||||
SetupExecuteProgress {
|
||||
init_phases,
|
||||
@@ -536,11 +582,24 @@ async fn fresh_setup(
|
||||
..
|
||||
}: SetupExecuteProgress,
|
||||
) -> Result<(SetupResult, RpcContext), Error> {
|
||||
let account = AccountInfo::new(start_os_password, root_ca_start_time().await)?;
|
||||
let account = AccountInfo::new(password, root_ca_start_time().await)?;
|
||||
let db = ctx.db().await?;
|
||||
let kiosk = Some(kiosk.unwrap_or(true)).filter(|_| &*PLATFORM != "raspberrypi");
|
||||
sync_kiosk(kiosk).await?;
|
||||
db.put(&ROOT, &Database::init(&account, kiosk)?).await?;
|
||||
|
||||
let language = ctx.language.peek(|a| a.clone());
|
||||
let keyboard = ctx.keyboard.peek(|a| a.clone());
|
||||
|
||||
if let Some(language) = &language {
|
||||
save_language(&**language).await?;
|
||||
}
|
||||
|
||||
if let Some(keyboard) = &keyboard {
|
||||
keyboard.save().await?;
|
||||
}
|
||||
|
||||
db.put(&ROOT, &Database::init(&account, kiosk, language, keyboard)?)
|
||||
.await?;
|
||||
drop(db);
|
||||
|
||||
let config = ctx.config.peek(|c| c.clone());
|
||||
@@ -556,16 +615,23 @@ async fn fresh_setup(
|
||||
)
|
||||
.await?;
|
||||
|
||||
write_shadow(start_os_password).await?;
|
||||
write_shadow(password).await?;
|
||||
|
||||
Ok(((&account).try_into()?, rpc_ctx))
|
||||
Ok((
|
||||
SetupResult {
|
||||
hostname: account.hostname,
|
||||
root_ca: Pem(account.root_ca_cert),
|
||||
needs_restart: ctx.install_rootfs.peek(|a| a.is_some()),
|
||||
},
|
||||
rpc_ctx,
|
||||
))
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn recover(
|
||||
ctx: &SetupContext,
|
||||
guid: InternedString,
|
||||
start_os_password: String,
|
||||
password: String,
|
||||
recovery_source: BackupTargetFS,
|
||||
server_id: String,
|
||||
recovery_password: String,
|
||||
@@ -576,7 +642,7 @@ async fn recover(
|
||||
recover_full_server(
|
||||
ctx,
|
||||
guid.clone(),
|
||||
start_os_password,
|
||||
password,
|
||||
recovery_source,
|
||||
&server_id,
|
||||
&recovery_password,
|
||||
@@ -591,7 +657,7 @@ async fn migrate(
|
||||
ctx: &SetupContext,
|
||||
guid: InternedString,
|
||||
old_guid: &str,
|
||||
start_os_password: String,
|
||||
password: String,
|
||||
kiosk: Option<bool>,
|
||||
SetupExecuteProgress {
|
||||
init_phases,
|
||||
@@ -671,7 +737,7 @@ async fn migrate(
|
||||
crate::disk::main::export(&old_guid, "/media/startos/migrate").await?;
|
||||
restore_phase.complete();
|
||||
|
||||
let (account, net_ctrl) = setup_init(&ctx, Some(start_os_password), kiosk, init_phases).await?;
|
||||
let (account, net_ctrl) = setup_init(&ctx, Some(password), kiosk, init_phases).await?;
|
||||
|
||||
let rpc_ctx = RpcContext::init(
|
||||
&ctx.webserver,
|
||||
@@ -682,5 +748,27 @@ async fn migrate(
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(((&account).try_into()?, rpc_ctx))
|
||||
Ok((
|
||||
SetupResult {
|
||||
hostname: account.hostname,
|
||||
root_ca: Pem(account.root_ca_cert),
|
||||
needs_restart: ctx.install_rootfs.peek(|a| a.is_some()),
|
||||
},
|
||||
rpc_ctx,
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn set_language(
|
||||
ctx: SetupContext,
|
||||
SetLanguageParams { language }: SetLanguageParams,
|
||||
) -> Result<(), Error> {
|
||||
set_locale(&*language);
|
||||
ctx.language.replace(Some(language));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_keyboard(ctx: SetupContext, options: KeyboardOptions) -> Result<(), Error> {
|
||||
options.apply_to_session().await?;
|
||||
ctx.keyboard.replace(Some(options));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ use tokio::sync::broadcast::Receiver;
|
||||
use tracing::instrument;
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::bins::set_locale;
|
||||
use crate::context::{CliContext, RpcContext};
|
||||
use crate::disk::util::{get_available, get_used};
|
||||
use crate::logs::{LogSource, LogsParams, SYSTEM_UNIT};
|
||||
@@ -1140,9 +1141,11 @@ pub async fn test_smtp(
|
||||
pub struct KeyboardOptions {
|
||||
#[arg(help = "help.arg.keyboard-layout")]
|
||||
pub layout: InternedString,
|
||||
#[arg(long, help = "help.arg.keyboard-model")]
|
||||
#[arg(short, long, help = "help.arg.keyboard-keymap")]
|
||||
pub keymap: Option<InternedString>,
|
||||
#[arg(short, long, help = "help.arg.keyboard-model")]
|
||||
pub model: Option<InternedString>,
|
||||
#[arg(long, help = "help.arg.keyboard-variant")]
|
||||
#[arg(short, long, help = "help.arg.keyboard-variant")]
|
||||
pub variant: Option<InternedString>,
|
||||
#[arg(short, long = "option", help = "help.arg.keyboard-option")]
|
||||
#[serde(default)]
|
||||
@@ -1166,7 +1169,18 @@ impl KeyboardOptions {
|
||||
}
|
||||
|
||||
pub async fn save(&self) -> Result<(), Error> {
|
||||
// TODO: set console keyboard
|
||||
write_file_atomic(
|
||||
"/media/startos/config/overlay/etc/vconsole.conf",
|
||||
format!(
|
||||
include_str!("./vconsole.conf.template"),
|
||||
model = self.model.as_deref().unwrap_or_default(),
|
||||
layout = &*self.layout,
|
||||
variant = self.variant.as_deref().unwrap_or_default(),
|
||||
options = self.options.join(","),
|
||||
keymap = self.keymap.as_deref().unwrap_or(&*self.layout),
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
write_file_atomic(
|
||||
"/media/startos/config/overlay/etc/X11/xorg.conf.d/00-keyboard.conf",
|
||||
format!(
|
||||
@@ -1203,10 +1217,7 @@ pub struct SetLanguageParams {
|
||||
pub language: InternedString,
|
||||
}
|
||||
|
||||
pub async fn set_language(
|
||||
ctx: RpcContext,
|
||||
SetLanguageParams { language }: SetLanguageParams,
|
||||
) -> Result<(), Error> {
|
||||
pub async fn save_language(language: &str) -> Result<(), Error> {
|
||||
write_file_atomic(
|
||||
"/etc/locale.gen",
|
||||
format!("{language}.UTF-8 UTF-8\n").as_bytes(),
|
||||
@@ -1225,6 +1236,15 @@ pub async fn set_language(
|
||||
format!("LANG={language}.UTF-8\n").as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_language(
|
||||
ctx: RpcContext,
|
||||
SetLanguageParams { language }: SetLanguageParams,
|
||||
) -> Result<(), Error> {
|
||||
set_locale(&*language);
|
||||
save_language(&*language).await?;
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
db.as_public_mut()
|
||||
@@ -1234,7 +1254,6 @@ pub async fn set_language(
|
||||
})
|
||||
.await
|
||||
.result?;
|
||||
rust_i18n::set_locale(&*language);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
7
core/src/system/vconsole.conf.template
Normal file
7
core/src/system/vconsole.conf.template
Normal file
@@ -0,0 +1,7 @@
|
||||
XKBMODEL="{model}"
|
||||
XKBLAYOUT="{layout}"
|
||||
XKBVARIANT="{variant}"
|
||||
XKBOPTIONS="{options}"
|
||||
|
||||
KEYMAP="{keymap}"
|
||||
|
||||
@@ -25,6 +25,7 @@ impl LshwDevice {
|
||||
Self::Display(_) => "display",
|
||||
}
|
||||
}
|
||||
#[instrument(skip_all)]
|
||||
pub fn from_value(value: &Value) -> Option<Self> {
|
||||
match value["class"].as_str() {
|
||||
Some("processor") => Some(LshwDevice::Processor(LshwProcessor::from_value(value))),
|
||||
@@ -41,6 +42,7 @@ pub struct LshwProcessor {
|
||||
pub capabilities: BTreeSet<InternedString>,
|
||||
}
|
||||
impl LshwProcessor {
|
||||
#[instrument(skip_all)]
|
||||
fn from_value(value: &Value) -> Self {
|
||||
Self {
|
||||
product: value["product"].as_str().map(From::from),
|
||||
@@ -63,6 +65,7 @@ pub struct LshwDisplay {
|
||||
pub driver: Option<InternedString>,
|
||||
}
|
||||
impl LshwDisplay {
|
||||
#[instrument(skip_all)]
|
||||
fn from_value(value: &Value) -> Self {
|
||||
Self {
|
||||
product: value["product"].as_str().map(From::from),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { LocaleString } from "./LocaleString"
|
||||
|
||||
export type AddCategoryParams = { id: string; name: string }
|
||||
export type AddCategoryParams = { id: string; name: LocaleString }
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { LocaleString } from "./LocaleString"
|
||||
|
||||
export type Alerts = {
|
||||
install: string | null
|
||||
uninstall: string | null
|
||||
restore: string | null
|
||||
start: string | null
|
||||
stop: string | null
|
||||
install: LocaleString | null
|
||||
uninstall: LocaleString | null
|
||||
restore: LocaleString | null
|
||||
start: LocaleString | null
|
||||
stop: LocaleString | null
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { EncryptedWire } from "./EncryptedWire"
|
||||
|
||||
export type AttachParams = {
|
||||
startOsPassword: EncryptedWire | null
|
||||
password: EncryptedWire | null
|
||||
guid: string
|
||||
kiosk?: boolean
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { LocaleString } from "./LocaleString"
|
||||
|
||||
export type Category = { name: string }
|
||||
export type Category = { name: LocaleString }
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DataUrl } from "./DataUrl"
|
||||
import type { LocaleString } from "./LocaleString"
|
||||
|
||||
export type DependencyMetadata = {
|
||||
title: string | null
|
||||
title: LocaleString | null
|
||||
icon: DataUrl | null
|
||||
description: string | null
|
||||
description: LocaleString | null
|
||||
optional: boolean
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { LocaleString } from "./LocaleString"
|
||||
|
||||
export type Description = { short: string; long: string }
|
||||
export type Description = { short: LocaleString; long: LocaleString }
|
||||
|
||||
9
sdk/base/lib/osBindings/KeyboardOptions.ts
Normal file
9
sdk/base/lib/osBindings/KeyboardOptions.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type KeyboardOptions = {
|
||||
layout: string
|
||||
keymap: string | null
|
||||
model: string | null
|
||||
variant: string | null
|
||||
options: Array<string>
|
||||
}
|
||||
3
sdk/base/lib/osBindings/LocaleString.ts
Normal file
3
sdk/base/lib/osBindings/LocaleString.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type LocaleString = string | Record<string, string>
|
||||
@@ -6,6 +6,7 @@ import type { GitHash } from "./GitHash"
|
||||
import type { HardwareRequirements } from "./HardwareRequirements"
|
||||
import type { ImageConfig } from "./ImageConfig"
|
||||
import type { ImageId } from "./ImageId"
|
||||
import type { LocaleString } from "./LocaleString"
|
||||
import type { PackageId } from "./PackageId"
|
||||
import type { Version } from "./Version"
|
||||
import type { VolumeId } from "./VolumeId"
|
||||
@@ -15,7 +16,7 @@ export type Manifest = {
|
||||
title: string
|
||||
version: Version
|
||||
satisfies: Array<Version>
|
||||
releaseNotes: string
|
||||
releaseNotes: LocaleString
|
||||
canMigrateTo: string
|
||||
canMigrateFrom: string
|
||||
license: string
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { LocaleString } from "./LocaleString"
|
||||
import type { PathOrUrl } from "./PathOrUrl"
|
||||
|
||||
export type Metadata = { title: string; icon: PathOrUrl }
|
||||
export type Metadata = { title: LocaleString; icon: PathOrUrl }
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { DependencyMetadata } from "./DependencyMetadata"
|
||||
import type { Description } from "./Description"
|
||||
import type { GitHash } from "./GitHash"
|
||||
import type { HardwareRequirements } from "./HardwareRequirements"
|
||||
import type { LocaleString } from "./LocaleString"
|
||||
import type { MerkleArchiveCommitment } from "./MerkleArchiveCommitment"
|
||||
import type { PackageId } from "./PackageId"
|
||||
import type { RegistryAsset } from "./RegistryAsset"
|
||||
@@ -15,7 +16,7 @@ export type PackageVersionInfo = {
|
||||
title: string
|
||||
icon: DataUrl
|
||||
description: Description
|
||||
releaseNotes: string
|
||||
releaseNotes: LocaleString
|
||||
gitHash: GitHash | null
|
||||
license: string
|
||||
wrapperRepo: string
|
||||
|
||||
3
sdk/base/lib/osBindings/Pem.ts
Normal file
3
sdk/base/lib/osBindings/Pem.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type Pem = string
|
||||
@@ -1,5 +1,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { Governor } from "./Governor"
|
||||
import type { KeyboardOptions } from "./KeyboardOptions"
|
||||
import type { LshwDevice } from "./LshwDevice"
|
||||
import type { NetworkInfo } from "./NetworkInfo"
|
||||
import type { ServerStatus } from "./ServerStatus"
|
||||
@@ -27,4 +28,6 @@ export type ServerInfo = {
|
||||
ram: number
|
||||
devices: Array<LshwDevice>
|
||||
kiosk: boolean | null
|
||||
language: string | null
|
||||
keyboard: KeyboardOptions | null
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ import type { EncryptedWire } from "./EncryptedWire"
|
||||
import type { RecoverySource } from "./RecoverySource"
|
||||
|
||||
export type SetupExecuteParams = {
|
||||
startOsLogicalname: string
|
||||
startOsPassword: EncryptedWire
|
||||
guid: string
|
||||
password: EncryptedWire
|
||||
recoverySource: RecoverySource<EncryptedWire> | null
|
||||
kiosk?: boolean
|
||||
}
|
||||
|
||||
3
sdk/base/lib/osBindings/SetupInfo.ts
Normal file
3
sdk/base/lib/osBindings/SetupInfo.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type SetupInfo = { guid: string | null; attach: boolean }
|
||||
@@ -1,8 +1,8 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { Pem } from "./Pem"
|
||||
|
||||
export type SetupResult = {
|
||||
torAddresses: Array<string>
|
||||
hostname: string
|
||||
lanAddress: string
|
||||
rootCa: string
|
||||
rootCa: Pem
|
||||
needsRestart: boolean
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { SetupInfo } from "./SetupInfo"
|
||||
import type { SetupProgress } from "./SetupProgress"
|
||||
import type { SetupResult } from "./SetupResult"
|
||||
|
||||
export type SetupStatusRes =
|
||||
| ({ status: "complete" } & SetupResult)
|
||||
| { status: "needs-install" }
|
||||
| ({ status: "incomplete" } & SetupInfo)
|
||||
| ({ status: "running" } & SetupProgress)
|
||||
| ({ status: "complete" } & SetupResult)
|
||||
|
||||
@@ -121,9 +121,11 @@ export { InstallingState } from "./InstallingState"
|
||||
export { InstallParams } from "./InstallParams"
|
||||
export { IpHostname } from "./IpHostname"
|
||||
export { IpInfo } from "./IpInfo"
|
||||
export { KeyboardOptions } from "./KeyboardOptions"
|
||||
export { ListPackageSignersParams } from "./ListPackageSignersParams"
|
||||
export { ListServiceInterfacesParams } from "./ListServiceInterfacesParams"
|
||||
export { ListVersionSignersParams } from "./ListVersionSignersParams"
|
||||
export { LocaleString } from "./LocaleString"
|
||||
export { LoginParams } from "./LoginParams"
|
||||
export { LshwDevice } from "./LshwDevice"
|
||||
export { LshwDisplay } from "./LshwDisplay"
|
||||
@@ -161,6 +163,7 @@ export { PackageState } from "./PackageState"
|
||||
export { PackageVersionInfo } from "./PackageVersionInfo"
|
||||
export { PasswordType } from "./PasswordType"
|
||||
export { PathOrUrl } from "./PathOrUrl"
|
||||
export { Pem } from "./Pem"
|
||||
export { Percentage } from "./Percentage"
|
||||
export { Progress } from "./Progress"
|
||||
export { ProgressUnits } from "./ProgressUnits"
|
||||
@@ -199,6 +202,7 @@ export { SetMainStatusStatus } from "./SetMainStatusStatus"
|
||||
export { SetMainStatus } from "./SetMainStatus"
|
||||
export { SetNameParams } from "./SetNameParams"
|
||||
export { SetupExecuteParams } from "./SetupExecuteParams"
|
||||
export { SetupInfo } from "./SetupInfo"
|
||||
export { SetupProgress } from "./SetupProgress"
|
||||
export { SetupResult } from "./SetupResult"
|
||||
export { SetupStatusRes } from "./SetupStatusRes"
|
||||
|
||||
@@ -48,9 +48,9 @@ export type SDKManifest = {
|
||||
readonly docsUrl: string
|
||||
readonly description: {
|
||||
/** Short description to display on the marketplace list page. Max length 80 chars. */
|
||||
readonly short: string
|
||||
readonly short: T.LocaleString
|
||||
/** Long description to display on the marketplace details page for this service. Max length 500 chars. */
|
||||
readonly long: string
|
||||
readonly long: T.LocaleString
|
||||
}
|
||||
/**
|
||||
* override the StartOS version this package was made for
|
||||
@@ -96,17 +96,17 @@ export type SDKManifest = {
|
||||
|
||||
readonly alerts?: {
|
||||
/** An warning alert requiring user confirmation before proceeding with initial installation of this service. */
|
||||
readonly install?: string | null
|
||||
readonly install?: T.LocaleString | null
|
||||
/** An warning alert requiring user confirmation before updating this service. */
|
||||
readonly update?: string | null
|
||||
readonly update?: T.LocaleString | null
|
||||
/** An warning alert requiring user confirmation before uninstalling this service. */
|
||||
readonly uninstall?: string | null
|
||||
readonly uninstall?: T.LocaleString | null
|
||||
/** An warning alert requiring user confirmation before restoring this service from backup. */
|
||||
readonly restore?: string | null
|
||||
readonly restore?: T.LocaleString | null
|
||||
/** An warning alert requiring user confirmation before starting this service. */
|
||||
readonly start?: string | null
|
||||
readonly start?: T.LocaleString | null
|
||||
/** An warning alert requiring user confirmation before stopping this service. */
|
||||
readonly stop?: string | null
|
||||
readonly stop?: T.LocaleString | null
|
||||
}
|
||||
/**
|
||||
* @description A mapping of service dependencies to be displayed to users when viewing the Marketplace
|
||||
|
||||
@@ -355,10 +355,13 @@ export class GetServiceInterface<Mapped = ServiceInterfaceFilled | null> {
|
||||
const watch = this.watch(abort.signal)
|
||||
const res = await watch.next()
|
||||
if (this.effects.constRetry) {
|
||||
watch.next().then(() => {
|
||||
abort.abort()
|
||||
this.effects.constRetry && this.effects.constRetry()
|
||||
})
|
||||
watch
|
||||
.next()
|
||||
.then(() => {
|
||||
abort.abort()
|
||||
this.effects.constRetry && this.effects.constRetry()
|
||||
})
|
||||
.catch()
|
||||
}
|
||||
return res.value
|
||||
}
|
||||
|
||||
@@ -55,10 +55,13 @@ export class GetServiceInterfaces<Mapped = ServiceInterfaceFilled[]> {
|
||||
const watch = this.watch(abort.signal)
|
||||
const res = await watch.next()
|
||||
if (this.effects.constRetry) {
|
||||
watch.next().then(() => {
|
||||
abort.abort()
|
||||
this.effects.constRetry && this.effects.constRetry()
|
||||
})
|
||||
watch
|
||||
.next()
|
||||
.then(() => {
|
||||
abort.abort()
|
||||
this.effects.constRetry && this.effects.constRetry()
|
||||
})
|
||||
.catch()
|
||||
}
|
||||
return res.value
|
||||
}
|
||||
|
||||
@@ -19,10 +19,13 @@ export class GetServiceManifest<Mapped = Manifest> {
|
||||
const watch = this.watch(abort.signal)
|
||||
const res = await watch.next()
|
||||
if (this.effects.constRetry) {
|
||||
watch.next().then(() => {
|
||||
abort.abort()
|
||||
this.effects.constRetry && this.effects.constRetry()
|
||||
})
|
||||
watch
|
||||
.next()
|
||||
.then(() => {
|
||||
abort.abort()
|
||||
this.effects.constRetry && this.effects.constRetry()
|
||||
})
|
||||
.catch()
|
||||
}
|
||||
return res.value
|
||||
}
|
||||
|
||||
@@ -230,10 +230,13 @@ export class FileHelper<A> {
|
||||
eq,
|
||||
]
|
||||
this.consts.push(record)
|
||||
watch.next().then(() => {
|
||||
this.consts = this.consts.filter((r) => r !== record)
|
||||
effects.constRetry && effects.constRetry()
|
||||
})
|
||||
watch
|
||||
.next()
|
||||
.then(() => {
|
||||
this.consts = this.consts.filter((r) => r !== record)
|
||||
effects.constRetry && effects.constRetry()
|
||||
})
|
||||
.catch()
|
||||
}
|
||||
return res.value
|
||||
}
|
||||
@@ -263,6 +266,7 @@ export class FileHelper<A> {
|
||||
})
|
||||
.catch((e) => console.error(asError(e)))
|
||||
if (!prev || !eq(prev.value, newRes)) {
|
||||
console.error("yielding", JSON.stringify({ prev: prev, newRes }))
|
||||
yield newRes
|
||||
}
|
||||
prev = { value: newRes }
|
||||
|
||||
52
web/package-lock.json
generated
52
web/package-lock.json
generated
@@ -386,6 +386,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-20.3.13.tgz",
|
||||
"integrity": "sha512-/D84T1Caxll3I2sRihPDR9UaWBhF50M+tAX15PdP6uSh/TxwAlLl9p7Rm1bD0mPjPercqaEKA+h9a9qLP16hug==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ajv": "8.17.1",
|
||||
"ajv-formats": "3.0.1",
|
||||
@@ -413,6 +414,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-20.3.13.tgz",
|
||||
"integrity": "sha512-hdMKY4rUTko8xqeWYGnwwDYDomkeOoLsYsP6SdaHWK7hpGvzWsT6Q/aIv8J8NrCYkLu+M+5nLiKOooweUZu3GQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@angular-devkit/core": "20.3.13",
|
||||
"jsonc-parser": "3.3.1",
|
||||
@@ -449,6 +451,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-20.3.15.tgz",
|
||||
"integrity": "sha512-ikyKfhkxoqQA6JcBN0B9RaN6369sM1XYX81Id0lI58dmWCe7gYfrTp8ejqxxKftl514psQO3pkW8Gn1nJ131Gw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -563,6 +566,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-20.2.14.tgz",
|
||||
"integrity": "sha512-7bZxc01URbiPiIBWThQ69XwOxVduqEKN4PhpbF2AAyfMc/W8Hcr4VoIJOwL0O1Nkq5beS8pCAqoOeIgFyXd/kg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"parse5": "^8.0.0",
|
||||
"tslib": "^2.3.0"
|
||||
@@ -579,6 +583,7 @@
|
||||
"integrity": "sha512-G78I/HDJULloS2LSqfUfbmBlhDCbcWujIRWfuMnGsRf82TyGA2OEPe3IA/F8MrJfeOzPQim2fMyn24MqHL40Vg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@angular-devkit/architect": "0.2003.13",
|
||||
"@angular-devkit/core": "20.3.13",
|
||||
@@ -613,6 +618,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@angular/common/-/common-20.3.15.tgz",
|
||||
"integrity": "sha512-k4mCXWRFiOHK3bUKfWkRQQ8KBPxW8TAJuKLYCsSHPCpMz6u0eA1F0VlrnOkZVKWPI792fOaEAWH2Y4PTaXlUHw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -629,6 +635,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.15.tgz",
|
||||
"integrity": "sha512-lMicIAFAKZXa+BCZWs3soTjNQPZZXrF/WMVDinm8dQcggNarnDj4UmXgKSyXkkyqK5SLfnLsXVzrX6ndVT6z7A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -642,6 +649,7 @@
|
||||
"integrity": "sha512-8sJoxodxsfyZ8eJ5r6Bx7BCbazXYgsZ1+dE8t5u5rTQ6jNggwNtYEzkyReoD5xvP+MMtRkos3xpwq4rtFnpI6A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "7.28.3",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14",
|
||||
@@ -674,6 +682,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.15.tgz",
|
||||
"integrity": "sha512-NMbX71SlTZIY9+rh/SPhRYFJU0pMJYW7z/TBD4lqiO+b0DTOIg1k7Pg9ydJGqSjFO1Z4dQaA6TteNuF99TJCNw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -699,6 +708,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-20.3.15.tgz",
|
||||
"integrity": "sha512-gS5hQkinq52pm/7mxz4yHPCzEcmRWjtUkOVddPH0V1BW/HMni/p4Y6k2KqKBeGb9p8S5EAp6PDxDVLOPukp3mg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -727,6 +737,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.3.15.tgz",
|
||||
"integrity": "sha512-TxRM/wTW/oGXv/3/Iohn58yWoiYXOaeEnxSasiGNS1qhbkcKtR70xzxW6NjChBUYAixz2ERkLURkpx3pI8Q6Dw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -791,6 +802,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@angular/router/-/router-20.3.15.tgz",
|
||||
"integrity": "sha512-6+qgk8swGSoAu7ISSY//GatAyCP36hEvvUgvjbZgkXLLH9yUQxdo77ij05aJ5s0OyB25q/JkqS8VTY0z1yE9NQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -809,6 +821,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-20.3.15.tgz",
|
||||
"integrity": "sha512-HCptODPVWg30XJwSueOz2zqsJjQ1chSscTs7FyIQcfuCTTthO35Lvz2Gtct8/GNHel9QNvvVwA5jrLjsU4dt1A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -854,6 +867,7 @@
|
||||
"integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
@@ -1818,6 +1832,7 @@
|
||||
"integrity": "sha512-nqhDw2ZcAUrKNPwhjinJny903bRhI0rQhiDz1LksjeRxqa36i3l75+4iXbOy0rlDpLJGxqtgoPavQjmmyS5UJw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@inquirer/checkbox": "^4.2.1",
|
||||
"@inquirer/confirm": "^5.1.14",
|
||||
@@ -3436,6 +3451,7 @@
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
@@ -3955,6 +3971,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-commerce/-/addon-commerce-4.66.0.tgz",
|
||||
"integrity": "sha512-tRWyuqK5j5nEjlk0x5HaeLArgVpAIJZNeMiPy//95v4/8tlHdQLM4gh3qcvwS70GN5fnlFXINWhnblvxSDv2dw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": ">=2.8.1"
|
||||
},
|
||||
@@ -4020,6 +4037,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/cdk/-/cdk-4.66.0.tgz",
|
||||
"integrity": "sha512-5DFbwHo7JHKBjgizbGTaIRJsai20+ZknhOQ1SRYwRTc9+6C1HbY/gGC+cjJTLmEQvk14rOoz8qbeWzJx88BU2Q==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
@@ -4051,6 +4069,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/core/-/core-4.66.0.tgz",
|
||||
"integrity": "sha512-AjjH+xhgonjf9Xnx3SHNrP5VbsS9jdtGB3BCTQbicYd6QuujQBKldK0fnYMjCY3L0+lboI2OPCVg9PTliOdJ8A==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": ">=2.8.1"
|
||||
},
|
||||
@@ -4090,6 +4109,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/event-plugins/-/event-plugins-4.7.0.tgz",
|
||||
"integrity": "sha512-j3HPRPR7XxKxgMeytb+r/CNUoLBMVrfdfL8KJr1XiFO9jyEvoC4chFXDXWlkGyUHJIC6wy5VIXlIlI/kpqOiGg==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -4148,6 +4168,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/kit/-/kit-4.66.0.tgz",
|
||||
"integrity": "sha512-uqY3wslMs7KiBceaHPwCyWVrP8IPqb3OgAy1zd5DHosoUj/ciUl4JWVdx+QdsDypV/Cs4EZrqcIUtMDKQ/Zk0g==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": ">=2.8.1"
|
||||
},
|
||||
@@ -4176,6 +4197,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/layout/-/layout-4.66.0.tgz",
|
||||
"integrity": "sha512-D6REwySoaPGZlkdqTfrWahMqziXOY7GGTm1pXWVYDi5kEcSP9+F8ojo6saHDlwhN+V4/2jlMrkseSPlfXbmngQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": ">=2.8.1"
|
||||
},
|
||||
@@ -4194,6 +4216,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/polymorpheus/-/polymorpheus-4.9.0.tgz",
|
||||
"integrity": "sha512-TbIIwslbEnxunKuL9OyPZdmefrvJEK6HYiADEKQHUMUs4Pk2UbhMckUieURo83yPDamk/Mww+Nu/g60J/4uh2w==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
@@ -4320,6 +4343,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz",
|
||||
"integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/trusted-types": "*"
|
||||
}
|
||||
@@ -4369,8 +4393,9 @@
|
||||
"version": "22.19.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz",
|
||||
"integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
@@ -4839,6 +4864,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -5116,6 +5142,7 @@
|
||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"readdirp": "^4.0.1"
|
||||
},
|
||||
@@ -5761,7 +5788,8 @@
|
||||
"version": "3.1.7",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz",
|
||||
"integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)"
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/domutils": {
|
||||
"version": "3.2.2",
|
||||
@@ -5833,6 +5861,7 @@
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
|
||||
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -5843,6 +5872,7 @@
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -6125,6 +6155,7 @@
|
||||
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"accepts": "^2.0.0",
|
||||
"body-parser": "^2.2.1",
|
||||
@@ -7668,6 +7699,7 @@
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"copy-anything": "^2.0.1",
|
||||
"parse-node-version": "^1.0.1",
|
||||
@@ -8025,6 +8057,7 @@
|
||||
"integrity": "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cli-truncate": "^4.0.0",
|
||||
"colorette": "^2.0.20",
|
||||
@@ -8864,6 +8897,7 @@
|
||||
"integrity": "sha512-yW5ME0hqTz38r/th/7zVwX5oSIw1FviSA2PUlGZdVjghDme/KX6iiwmOBmlt9E9whNmwijEC6Gn3KKbrsBx8ig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.3.0",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
@@ -10670,6 +10704,7 @@
|
||||
"integrity": "sha512-RIDh866U8agLgiIcdpB+COKnlCreHJLfIhWC3LVflku5YHfpnsIKigRZeFfMfCc4dVcqNVfQQ5gO/afOck064A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
@@ -10815,6 +10850,7 @@
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
||||
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
@@ -10838,6 +10874,7 @@
|
||||
"integrity": "sha512-9GUyuksjw70uNpb1MTYWsH9MQHOHY6kwfnkafC24+7aOMZn9+rVMBxRbLvw756mrBFbIsFg6Xw9IkR2Fnn3k+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.0",
|
||||
"immutable": "^5.0.2",
|
||||
@@ -11754,7 +11791,8 @@
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
"license": "0BSD",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/tslint": {
|
||||
"version": "6.1.3",
|
||||
@@ -12046,6 +12084,7 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -12058,7 +12097,7 @@
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/union": {
|
||||
@@ -12206,6 +12245,7 @@
|
||||
"integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -12673,6 +12713,7 @@
|
||||
"integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
@@ -12691,7 +12732,8 @@
|
||||
"version": "0.15.1",
|
||||
"resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz",
|
||||
"integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ export class StateService {
|
||||
async attachDrive(password: string | null): Promise<void> {
|
||||
await this.api.attach({
|
||||
guid: this.dataDriveGuid,
|
||||
startOsPassword: password ? await this.api.encrypt(password) : null,
|
||||
password: password ? await this.api.encrypt(password) : null,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -77,8 +77,8 @@ export class StateService {
|
||||
}
|
||||
|
||||
await this.api.execute({
|
||||
startOsLogicalname: this.dataDriveGuid,
|
||||
startOsPassword: password ? await this.api.encrypt(password) : null,
|
||||
guid: this.dataDriveGuid,
|
||||
password: password ? await this.api.encrypt(password) : null,
|
||||
recoverySource,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -43,15 +43,15 @@ export interface InstallOsRes {
|
||||
// === Attach ===
|
||||
|
||||
export interface AttachParams {
|
||||
startOsPassword: T.EncryptedWire | null
|
||||
password: T.EncryptedWire | null
|
||||
guid: string // data drive
|
||||
}
|
||||
|
||||
// === Execute ===
|
||||
|
||||
export interface SetupExecuteParams {
|
||||
startOsLogicalname: string
|
||||
startOsPassword: T.EncryptedWire | null // null = keep existing password (for restore/transfer)
|
||||
guid: string
|
||||
password: T.EncryptedWire | null // null = keep existing password (for restore/transfer)
|
||||
recoverySource:
|
||||
| {
|
||||
type: 'migrate'
|
||||
|
||||
Reference in New Issue
Block a user