405 lines
17 KiB
Rust
405 lines
17 KiB
Rust
use super::{
|
|
CommandResult, DeliveryDestinationSummary, FirstIssueAttempt, FirstIssueStatus,
|
|
FirstIssueSummary, OperationError, OperationExecutor, QrStatus, ReissueSummary,
|
|
RestartPreviewSummary, RestorePreviewSummary, SensitiveCommand, UiCommand,
|
|
};
|
|
use crate::app::{Application, FirstIssueDeliveryOutcome, RestorePreview};
|
|
use crate::delivery::files::PreparedDestination;
|
|
use crate::delivery::{QrCodeOutput, QrError};
|
|
use crate::domain::PublicKey;
|
|
|
|
enum DeliveryMode {
|
|
FirstIssue,
|
|
Reissue,
|
|
}
|
|
|
|
struct PreparedDestinationContext {
|
|
identity: PublicKey,
|
|
include_qr: bool,
|
|
mode: DeliveryMode,
|
|
attempt: FirstIssueAttempt,
|
|
generation: u64,
|
|
destination: PreparedDestination,
|
|
}
|
|
|
|
struct RestorePreviewContext {
|
|
_archive_name: String,
|
|
generation: u64,
|
|
preview: RestorePreview,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub struct ApplicationExecutor {
|
|
prepared_destination: Option<PreparedDestinationContext>,
|
|
restore_preview: Option<RestorePreviewContext>,
|
|
}
|
|
|
|
impl OperationExecutor for ApplicationExecutor {
|
|
fn execute(
|
|
&mut self,
|
|
app: &mut Application,
|
|
command: UiCommand,
|
|
) -> Result<CommandResult, OperationError> {
|
|
self.execute_at(app, command, 0)
|
|
}
|
|
|
|
fn execute_at(
|
|
&mut self,
|
|
app: &mut Application,
|
|
command: UiCommand,
|
|
generation: u64,
|
|
) -> Result<CommandResult, OperationError> {
|
|
let delivery_continuation = matches!(command, UiCommand::DeliverFirstIssue { .. });
|
|
let restore_continuation = matches!(
|
|
command,
|
|
UiCommand::ConfirmRestore { .. } | UiCommand::CancelRestorePreview
|
|
);
|
|
if !delivery_continuation && !matches!(&command, UiCommand::CancelPreparedDelivery) {
|
|
self.prepared_destination = None;
|
|
}
|
|
if !restore_continuation && !matches!(&command, UiCommand::PreviewRestore { .. }) {
|
|
self.restore_preview = None;
|
|
}
|
|
let result = match command {
|
|
UiCommand::CreateClient {
|
|
alias,
|
|
manual_addresses,
|
|
created_utc,
|
|
} => app
|
|
.prepare_new_client(&alias, manual_addresses, created_utc.as_deref())
|
|
.map(CommandResult::ClientCreated),
|
|
UiCommand::RenameClient { identity, alias } => app
|
|
.rename_client(identity, &alias)
|
|
.map(|()| CommandResult::ClientRenamed),
|
|
UiCommand::DeleteClient { identity } => app
|
|
.delete_client(identity)
|
|
.map(|record_removed| CommandResult::ClientDeleted { record_removed }),
|
|
UiCommand::DiscardDraft => app.discard_draft().map(|()| CommandResult::DraftDiscarded),
|
|
UiCommand::Commit => app.commit().map(CommandResult::Committed),
|
|
UiCommand::CreateBackup => app.create_manual_backup().map(CommandResult::BackupCreated),
|
|
UiCommand::ListBackups => app.list_backups().map(CommandResult::Backups),
|
|
UiCommand::ImportSelectedFile { file_name } => app
|
|
.import_selected_file(&file_name)
|
|
.map(CommandResult::SelectedFileImported),
|
|
UiCommand::PreviewRestart => app.preview_restart().map(|preview| {
|
|
CommandResult::RestartPreview(RestartPreviewSummary {
|
|
has_draft: preview.has_draft(),
|
|
interface: preview.interface().to_owned(),
|
|
systemd_unit: preview.systemd_unit().to_owned(),
|
|
})
|
|
}),
|
|
UiCommand::Restart(choice) => {
|
|
let result = app
|
|
.preview_restart()
|
|
.map(|preview| app.restart(preview, choice));
|
|
result.map(CommandResult::Restarted)
|
|
}
|
|
UiCommand::PrepareDelivery {
|
|
identity,
|
|
path,
|
|
include_qr,
|
|
reissue,
|
|
attempt,
|
|
} => app
|
|
.prepare_first_issue_destination(path)
|
|
.map_err(crate::app::AppError::FileOutput)
|
|
.map(|destination| {
|
|
let summary = DeliveryDestinationSummary {
|
|
identity,
|
|
path: destination.path().to_path_buf(),
|
|
overwrite_required: destination.overwrite_confirmation_required(),
|
|
include_qr,
|
|
reissue,
|
|
attempt,
|
|
prepared_generation: generation,
|
|
};
|
|
self.prepared_destination = Some(PreparedDestinationContext {
|
|
identity,
|
|
include_qr,
|
|
mode: if reissue {
|
|
DeliveryMode::Reissue
|
|
} else {
|
|
DeliveryMode::FirstIssue
|
|
},
|
|
attempt,
|
|
generation,
|
|
destination,
|
|
});
|
|
CommandResult::DeliveryDestination(summary)
|
|
}),
|
|
UiCommand::DeliverFirstIssue {
|
|
attempt,
|
|
overwrite_confirmed,
|
|
prepared_generation,
|
|
} => {
|
|
let prepared = self
|
|
.prepared_destination
|
|
.take()
|
|
.ok_or_else(|| OperationError {
|
|
message: "No prepared output destination is available.".to_owned(),
|
|
})?;
|
|
if !matches!(prepared.mode, DeliveryMode::FirstIssue)
|
|
|| prepared.attempt != attempt
|
|
|| !destination_generation_matches(
|
|
prepared.generation,
|
|
prepared_generation,
|
|
generation,
|
|
)
|
|
{
|
|
return Err(OperationError {
|
|
message: "Prepared output destination is stale; select it again."
|
|
.to_owned(),
|
|
});
|
|
}
|
|
let identity = prepared.identity;
|
|
let include_qr = prepared.include_qr;
|
|
let mut destination = prepared.destination;
|
|
if destination.overwrite_confirmation_required() {
|
|
if !overwrite_confirmed {
|
|
return Err(OperationError {
|
|
message: "Overwrite confirmation was not supplied.".to_owned(),
|
|
});
|
|
}
|
|
destination = destination.confirm_overwrite().map_err(operation_error)?;
|
|
}
|
|
if include_qr {
|
|
app.deliver_first_issue_file_and_qr(identity, attempt, destination)
|
|
.map(|outcome| match outcome {
|
|
FirstIssueDeliveryOutcome::Delivered(delivery) => {
|
|
let (qr, qr_output) = qr_result(delivery.qr);
|
|
CommandResult::FirstIssue(FirstIssueSummary {
|
|
identity,
|
|
status: FirstIssueStatus::Delivered,
|
|
retry: None,
|
|
activation_warning: delivery.activation_warning,
|
|
qr,
|
|
qr_output,
|
|
})
|
|
}
|
|
FirstIssueDeliveryOutcome::PublishedButBookkeepingUncertain {
|
|
value,
|
|
..
|
|
} => {
|
|
let (qr, qr_output) = qr_result(value.qr);
|
|
CommandResult::FirstIssue(FirstIssueSummary {
|
|
identity,
|
|
status: FirstIssueStatus::PublishedButBookkeepingUncertain,
|
|
retry: None,
|
|
activation_warning: value.activation_warning,
|
|
qr,
|
|
qr_output,
|
|
})
|
|
}
|
|
FirstIssueDeliveryOutcome::Failed {
|
|
error,
|
|
retry,
|
|
activation_warning,
|
|
} => CommandResult::FirstIssue(FirstIssueSummary {
|
|
identity,
|
|
status: FirstIssueStatus::Failed(error),
|
|
retry: Some(retry),
|
|
activation_warning,
|
|
qr: QrStatus::NotRequested,
|
|
qr_output: None,
|
|
}),
|
|
})
|
|
} else {
|
|
app.deliver_first_issue_file(identity, attempt, destination)
|
|
.map(|outcome| match outcome {
|
|
FirstIssueDeliveryOutcome::Delivered(delivery) => {
|
|
CommandResult::FirstIssue(FirstIssueSummary {
|
|
identity,
|
|
status: FirstIssueStatus::Delivered,
|
|
retry: None,
|
|
activation_warning: delivery.activation_warning,
|
|
qr: QrStatus::NotRequested,
|
|
qr_output: None,
|
|
})
|
|
}
|
|
FirstIssueDeliveryOutcome::PublishedButBookkeepingUncertain {
|
|
value,
|
|
..
|
|
} => CommandResult::FirstIssue(FirstIssueSummary {
|
|
identity,
|
|
status: FirstIssueStatus::PublishedButBookkeepingUncertain,
|
|
retry: None,
|
|
activation_warning: value.activation_warning,
|
|
qr: QrStatus::NotRequested,
|
|
qr_output: None,
|
|
}),
|
|
FirstIssueDeliveryOutcome::Failed {
|
|
error,
|
|
retry,
|
|
activation_warning,
|
|
} => CommandResult::FirstIssue(FirstIssueSummary {
|
|
identity,
|
|
status: FirstIssueStatus::Failed(error),
|
|
retry: Some(retry),
|
|
activation_warning,
|
|
qr: QrStatus::NotRequested,
|
|
qr_output: None,
|
|
}),
|
|
})
|
|
}
|
|
}
|
|
UiCommand::PreviewRestore { file_name } => {
|
|
app.preview_restore(&file_name).map(|preview| {
|
|
let changes = preview.change_summary(app.saved_bytes());
|
|
let summary = RestorePreviewSummary {
|
|
file_name: file_name.clone(),
|
|
created_at_utc: preview.created_at_utc().to_owned(),
|
|
reason: preview.reason(),
|
|
historical_apply: preview.historical_apply().clone(),
|
|
changes,
|
|
preview_generation: generation,
|
|
};
|
|
self.restore_preview = Some(RestorePreviewContext {
|
|
_archive_name: file_name,
|
|
generation,
|
|
preview,
|
|
});
|
|
CommandResult::RestorePreview(summary)
|
|
})
|
|
}
|
|
UiCommand::CancelPreparedDelivery => {
|
|
self.prepared_destination = None;
|
|
Ok(CommandResult::Cancelled)
|
|
}
|
|
UiCommand::CancelRestorePreview => {
|
|
self.restore_preview = None;
|
|
Ok(CommandResult::Cancelled)
|
|
}
|
|
UiCommand::ConfirmRestore { preview_generation } => {
|
|
let context = self.restore_preview.take().ok_or_else(|| OperationError {
|
|
message: "Restore preview is no longer available; select the archive again."
|
|
.to_owned(),
|
|
})?;
|
|
if !restore_preview_generation_matches(
|
|
context.generation,
|
|
preview_generation,
|
|
generation,
|
|
) {
|
|
return Err(OperationError {
|
|
message: "Restore preview is stale; select the archive again.".to_owned(),
|
|
});
|
|
}
|
|
app.confirm_restore(context.preview)
|
|
.map(CommandResult::Restored)
|
|
}
|
|
};
|
|
result.map_err(|error| OperationError {
|
|
message: error.to_string(),
|
|
})
|
|
}
|
|
|
|
fn execute_sensitive(
|
|
&mut self,
|
|
app: &mut Application,
|
|
command: SensitiveCommand,
|
|
) -> Result<CommandResult, OperationError> {
|
|
match command {
|
|
SensitiveCommand::PrepareInitialSetup(password) => app
|
|
.prepare_initial_setup_from_sources(password.expose())
|
|
.map(CommandResult::InitialSetupPrepared)
|
|
.map_err(|error| OperationError {
|
|
message: error.to_string(),
|
|
}),
|
|
SensitiveCommand::ReissueClient {
|
|
identity,
|
|
password,
|
|
overwrite_confirmed,
|
|
prepared_generation,
|
|
} => {
|
|
let prepared = self
|
|
.prepared_destination
|
|
.take()
|
|
.ok_or_else(|| OperationError {
|
|
message: "No prepared output destination is available.".to_owned(),
|
|
})?;
|
|
if prepared.identity != identity
|
|
|| !matches!(prepared.mode, DeliveryMode::Reissue)
|
|
|| prepared.generation != prepared_generation
|
|
{
|
|
return Err(OperationError {
|
|
message: "Prepared output destination is stale; select it again."
|
|
.to_owned(),
|
|
});
|
|
}
|
|
let include_qr = prepared.include_qr;
|
|
let mut destination = prepared.destination;
|
|
if destination.overwrite_confirmation_required() {
|
|
if !overwrite_confirmed {
|
|
return Err(OperationError {
|
|
message: "Overwrite confirmation was not supplied.".to_owned(),
|
|
});
|
|
}
|
|
destination = destination.confirm_overwrite().map_err(operation_error)?;
|
|
}
|
|
let delivery = app
|
|
.reissue_client_file(identity, password.expose(), destination, include_qr)
|
|
.map_err(|error| OperationError {
|
|
message: error.to_string(),
|
|
})?;
|
|
let (qr, qr_output) = match delivery.qr {
|
|
None => (QrStatus::NotRequested, None),
|
|
Some(result) => qr_result(result),
|
|
};
|
|
Ok(CommandResult::Reissued(ReissueSummary {
|
|
activation_warning: delivery.activation_warning,
|
|
qr,
|
|
qr_output,
|
|
}))
|
|
}
|
|
}
|
|
}
|
|
|
|
fn execute_sensitive_at(
|
|
&mut self,
|
|
app: &mut Application,
|
|
command: SensitiveCommand,
|
|
generation: u64,
|
|
) -> Result<CommandResult, OperationError> {
|
|
match &command {
|
|
SensitiveCommand::ReissueClient {
|
|
prepared_generation,
|
|
..
|
|
} if *prepared_generation != generation => {
|
|
return Err(OperationError {
|
|
message: "Prepared output destination is stale; select it again.".to_owned(),
|
|
});
|
|
}
|
|
_ => {}
|
|
}
|
|
if matches!(command, SensitiveCommand::PrepareInitialSetup(_)) {
|
|
self.prepared_destination = None;
|
|
self.restore_preview = None;
|
|
}
|
|
self.execute_sensitive(app, command)
|
|
}
|
|
}
|
|
|
|
fn operation_error(error: impl std::fmt::Display) -> OperationError {
|
|
OperationError {
|
|
message: error.to_string(),
|
|
}
|
|
}
|
|
|
|
pub(super) fn destination_generation_matches(stored: u64, requested: u64, current: u64) -> bool {
|
|
stored == requested && stored == current
|
|
}
|
|
|
|
pub(super) fn restore_preview_generation_matches(
|
|
stored: u64,
|
|
requested: u64,
|
|
current: u64,
|
|
) -> bool {
|
|
stored == requested && current == requested.saturating_add(1)
|
|
}
|
|
|
|
fn qr_result(result: Result<QrCodeOutput, QrError>) -> (QrStatus, Option<QrCodeOutput>) {
|
|
match result {
|
|
Ok(output) => (QrStatus::Ready, Some(output)),
|
|
Err(error) => (QrStatus::Failed(error), None),
|
|
}
|
|
}
|