put: report through the view and ask the host-key question through the Prompter
put() loses its MultiProgress parameter: the summary, pre-flight and connection spinners become view messages, the per-file SFTP transfer reports determinate progress through view.progress (upload_file takes a byte-count callback instead of an indicatif bar), and the display is released through view.suspend on every exit path. The hardcoded trust-on-first-use prompt in the SSH host-key verification becomes the Prompter::accept_host_key port (fail-closed by default; the terminal prompter prints the authenticity banner and confirms), so a remote frontend can surface its own host-key dialog.
This commit is contained in:
+5
-1
@@ -564,13 +564,17 @@ fn main() {
|
|||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let view = pkh::ui::deb::DebUi::new(&multi);
|
||||||
|
let prompter = pkh::ui::prompt::TerminalPrompter;
|
||||||
let options = pkh::put::PutOptions {
|
let options = pkh::put::PutOptions {
|
||||||
ppa: ppa.to_string(),
|
ppa: ppa.to_string(),
|
||||||
changes,
|
changes,
|
||||||
force,
|
force,
|
||||||
cwd,
|
cwd,
|
||||||
|
view: &view,
|
||||||
|
prompter: &prompter,
|
||||||
};
|
};
|
||||||
if let Err(e) = rt.block_on(async { pkh::put::put(&options, &multi).await }) {
|
if let Err(e) = rt.block_on(async { pkh::put::put(&options).await }) {
|
||||||
error!("{}", e);
|
error!("{}", e);
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-80
@@ -18,7 +18,6 @@ pub mod target;
|
|||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use indicatif::{MultiProgress, ProgressBar};
|
|
||||||
use log::info;
|
use log::info;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@@ -27,10 +26,10 @@ use crate::debian::checksums::FileChecksums;
|
|||||||
use crate::debian::control::ControlInfo;
|
use crate::debian::control::ControlInfo;
|
||||||
use crate::debian::version::DebianVersion;
|
use crate::debian::version::DebianVersion;
|
||||||
use crate::launchpad;
|
use crate::launchpad;
|
||||||
use crate::ui;
|
use crate::report::{BuildView, Prompter};
|
||||||
|
|
||||||
/// Everything `put` needs to run.
|
/// Everything `put` needs to run.
|
||||||
pub struct PutOptions {
|
pub struct PutOptions<'a> {
|
||||||
/// PPA to upload to, `user/ppa_name` format.
|
/// PPA to upload to, `user/ppa_name` format.
|
||||||
pub ppa: String,
|
pub ppa: String,
|
||||||
/// Explicit `.changes` file to upload; when `None`, the one matching the
|
/// Explicit `.changes` file to upload; when `None`, the one matching the
|
||||||
@@ -42,14 +41,25 @@ pub struct PutOptions {
|
|||||||
pub force: bool,
|
pub force: bool,
|
||||||
/// Source package directory (the one containing `debian/`).
|
/// Source package directory (the one containing `debian/`).
|
||||||
pub cwd: PathBuf,
|
pub cwd: PathBuf,
|
||||||
|
/// Where the upload progress (status messages, per-file byte counts) is
|
||||||
|
/// reported.
|
||||||
|
pub view: &'a dyn BuildView,
|
||||||
|
/// Who answers the host-key question on first contact with the target
|
||||||
|
/// server (fail-closed when nobody can be asked).
|
||||||
|
pub prompter: &'a dyn Prompter,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Upload the package described by `opts` to its target through `multi`'s
|
/// Upload the package described by `opts` to its target, reporting progress
|
||||||
/// progress bars.
|
/// through the view and asking the prompter when the server is unknown.
|
||||||
pub async fn put(
|
pub async fn put(opts: &PutOptions<'_>) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
opts: &PutOptions,
|
// The display is released on every path (success and early `?` bails)
|
||||||
multi: &MultiProgress,
|
// before the outcome is logged, so no stale status line lingers above it
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
let result = put_impl(opts).await;
|
||||||
|
opts.view.suspend();
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn put_impl(opts: &PutOptions<'_>) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let target = launchpad::ppa_target(&opts.ppa)?;
|
let target = launchpad::ppa_target(&opts.ppa)?;
|
||||||
|
|
||||||
let ssh_config = ssh::lookup_ssh_config(&target.fqdn);
|
let ssh_config = ssh::lookup_ssh_config(&target.fqdn);
|
||||||
@@ -79,21 +89,10 @@ pub async fn put(
|
|||||||
};
|
};
|
||||||
let changes = changes::parse(&changes_path)?;
|
let changes = changes::parse(&changes_path)?;
|
||||||
|
|
||||||
// The summary line stays up for the whole flow: the completion message
|
opts.view.message(&format!(
|
||||||
// replaces it on success, and the `PutBars` guard clears it on every
|
|
||||||
// early `?` bail so the error logged by main is not preceded by stale
|
|
||||||
// bars
|
|
||||||
let mut bars = PutBars::default();
|
|
||||||
let summary = multi.add(ProgressBar::new(0));
|
|
||||||
// Style and prefix go in before the steady tick: otherwise the first
|
|
||||||
// tick can render one frame with the default bar template
|
|
||||||
summary.set_style(ui::spinner_style());
|
|
||||||
summary.set_prefix(format!(
|
|
||||||
"Uploading {} {} to {}",
|
"Uploading {} {} to {}",
|
||||||
changes.source, changes.version, target.label
|
changes.source, changes.version, target.label
|
||||||
));
|
));
|
||||||
summary.enable_steady_tick(TICK);
|
|
||||||
bars.track(summary.clone());
|
|
||||||
changes::validate(&changes)?;
|
changes::validate(&changes)?;
|
||||||
|
|
||||||
// Pre-flight checks for everything the upload queue only rejects after
|
// Pre-flight checks for everything the upload queue only rejects after
|
||||||
@@ -109,14 +108,10 @@ pub async fn put(
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
let checking = multi.add(ProgressBar::new(0));
|
opts.view
|
||||||
checking.set_style(ui::spinner_style());
|
.message(&format!("Checking {} on Launchpad...", target.label));
|
||||||
checking.set_prefix(format!("Checking {} on Launchpad...", target.label));
|
|
||||||
checking.enable_steady_tick(TICK);
|
|
||||||
bars.track(checking.clone());
|
|
||||||
launchpad::ppa_info(&opts.ppa).await?;
|
launchpad::ppa_info(&opts.ppa).await?;
|
||||||
launchpad::check_ppa_series(&changes.distribution).await?;
|
launchpad::check_ppa_series(&changes.distribution).await?;
|
||||||
clear_bar(&checking);
|
|
||||||
|
|
||||||
// Last pre-flight check: a version the PPA already publishes at or
|
// Last pre-flight check: a version the PPA already publishes at or
|
||||||
// above the changes' one would supersede (or reject) this upload
|
// above the changes' one would supersede (or reject) this upload
|
||||||
@@ -134,14 +129,10 @@ pub async fn put(
|
|||||||
.into());
|
.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let connecting = multi.add(ProgressBar::new(0));
|
opts.view
|
||||||
connecting.set_style(ui::spinner_style());
|
.message(&format!("Connecting to {login}@{host}:{port}..."));
|
||||||
connecting.set_prefix(format!("Connecting to {login}@{host}:{port}..."));
|
let session = ssh::connect(&host, port, &login, &ssh_config, opts.prompter)?;
|
||||||
connecting.enable_steady_tick(TICK);
|
|
||||||
bars.track(connecting.clone());
|
|
||||||
let session = ssh::connect(&host, port, &login, &ssh_config)?;
|
|
||||||
let sftp = ssh::sftp(&session)?;
|
let sftp = ssh::sftp(&session)?;
|
||||||
clear_bar(&connecting);
|
|
||||||
|
|
||||||
// Payload first, the .changes file last (like dput), so the server-side
|
// Payload first, the .changes file last (like dput), so the server-side
|
||||||
// queue processor can never pick up an incomplete upload
|
// queue processor can never pick up an incomplete upload
|
||||||
@@ -179,15 +170,11 @@ pub async fn put(
|
|||||||
return Err(error);
|
return Err(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Same transfer view as pull: prefix line, bar on its own line
|
|
||||||
let bar = multi.add(ProgressBar::new(size));
|
|
||||||
bar.enable_steady_tick(std::time::Duration::from_millis(50));
|
|
||||||
bar.set_style(ui::transfer_style());
|
|
||||||
bar.set_prefix(format!("Uploading {name}..."));
|
|
||||||
|
|
||||||
let remote = format!("{incoming}/{name}");
|
let remote = format!("{incoming}/{name}");
|
||||||
let result = ssh::upload_file(&sftp, path, &remote, &host, &bar);
|
let label = format!("Uploading {name}");
|
||||||
bar.finish_and_clear();
|
let view = opts.view;
|
||||||
|
let on_progress = |uploaded: u64| view.progress(&label, uploaded as usize, size as usize);
|
||||||
|
let result = ssh::upload_file(&sftp, path, &remote, &host, &on_progress);
|
||||||
if let Err(e) = result {
|
if let Err(e) = result {
|
||||||
// The failed file itself joins the cleanup: its remote `create`
|
// The failed file itself joins the cleanup: its remote `create`
|
||||||
// may have succeeded before the failure, leaving a partial — or,
|
// may have succeeded before the failure, leaving a partial — or,
|
||||||
@@ -204,9 +191,6 @@ pub async fn put(
|
|||||||
// replaying is safe).
|
// replaying is safe).
|
||||||
record_upload(&upload_log_path()?, &record)?;
|
record_upload(&upload_log_path()?, &record)?;
|
||||||
|
|
||||||
// The completion lines replace the summary bar; the guard's own clear
|
|
||||||
// at scope exit is a no-op for the already-finished bars
|
|
||||||
clear_bar(&summary);
|
|
||||||
info!(
|
info!(
|
||||||
"Upload of {} {} to {} complete.",
|
"Upload of {} {} to {} complete.",
|
||||||
changes.source, changes.version, target.label
|
changes.source, changes.version, target.label
|
||||||
@@ -260,41 +244,6 @@ fn cleanup_partial_upload(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Steady tick interval of every bar rendered by a `put` run
|
|
||||||
const TICK: std::time::Duration = std::time::Duration::from_millis(50);
|
|
||||||
|
|
||||||
/// Stop `bar`'s steady tick and clear it from the terminal. The tick is
|
|
||||||
/// disabled first: a tick firing right after the clear would redraw a stale
|
|
||||||
/// frame, the race [`crate::ui::deb::DebUi::suspend`] guards against too.
|
|
||||||
/// Clearing twice is harmless: finished bars stay finished.
|
|
||||||
fn clear_bar(bar: &ProgressBar) {
|
|
||||||
bar.disable_steady_tick();
|
|
||||||
bar.finish_and_clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The bars rendered by one `put` run, cleared when the guard drops. The
|
|
||||||
/// error paths bail out early through `?` and `main` logs the error
|
|
||||||
/// afterwards, so a bar left unfinished would linger on screen above it.
|
|
||||||
#[derive(Default)]
|
|
||||||
struct PutBars {
|
|
||||||
bars: Vec<ProgressBar>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PutBars {
|
|
||||||
/// Track `bar` so it is cleared with the rest when the guard drops
|
|
||||||
fn track(&mut self, bar: ProgressBar) {
|
|
||||||
self.bars.push(bar);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for PutBars {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
for bar in &self.bars {
|
|
||||||
clear_bar(bar);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Validate the source package's `Section` (debian/control source stanza)
|
/// Validate the source package's `Section` (debian/control source stanza)
|
||||||
/// against the distribution's valid sections: a bare section or a
|
/// against the distribution's valid sections: a bare section or a
|
||||||
/// `section/subsection` is accepted. Archives reject uploads carrying an
|
/// `section/subsection` is accepted. Archives reject uploads carrying an
|
||||||
|
|||||||
+9
-11
@@ -20,14 +20,13 @@ use std::net::{SocketAddr, TcpStream, ToSocketAddrs};
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use indicatif::ProgressBar;
|
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use ssh2::{CheckResult, HostKeyType, KnownHostFileKind, KnownHosts, Session};
|
use ssh2::{CheckResult, HostKeyType, KnownHostFileKind, KnownHosts, Session};
|
||||||
|
|
||||||
use crate::data::embed_data;
|
use crate::data::embed_data;
|
||||||
use crate::ui::prompt;
|
use crate::report::Prompter;
|
||||||
|
|
||||||
/// Pinned SSH host key fingerprints, loaded from the bundled
|
/// Pinned SSH host key fingerprints, loaded from the bundled
|
||||||
/// `host_keys.yml` data file (same pattern as `distro_info.yml`): data
|
/// `host_keys.yml` data file (same pattern as `distro_info.yml`): data
|
||||||
@@ -303,6 +302,7 @@ pub fn connect(
|
|||||||
port: u16,
|
port: u16,
|
||||||
login: &str,
|
login: &str,
|
||||||
config: &SshConfig,
|
config: &SshConfig,
|
||||||
|
prompter: &dyn Prompter,
|
||||||
) -> Result<Session, Box<dyn std::error::Error>> {
|
) -> Result<Session, Box<dyn std::error::Error>> {
|
||||||
let tcp = tcp_connect(host, port)?;
|
let tcp = tcp_connect(host, port)?;
|
||||||
|
|
||||||
@@ -326,7 +326,7 @@ pub fn connect(
|
|||||||
let (key, key_type) = session
|
let (key, key_type) = session
|
||||||
.host_key()
|
.host_key()
|
||||||
.ok_or_else(|| format!("{host} offered no host key"))?;
|
.ok_or_else(|| format!("{host} offered no host key"))?;
|
||||||
verify_host_key(host, port, key, key_type)?;
|
verify_host_key(host, port, key, key_type, prompter)?;
|
||||||
|
|
||||||
authenticate(&session, host, login, config)?;
|
authenticate(&session, host, login, config)?;
|
||||||
|
|
||||||
@@ -353,6 +353,7 @@ fn verify_host_key(
|
|||||||
port: u16,
|
port: u16,
|
||||||
key: &[u8],
|
key: &[u8],
|
||||||
key_type: HostKeyType,
|
key_type: HostKeyType,
|
||||||
|
prompter: &dyn Prompter,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let fingerprint = fingerprint(key);
|
let fingerprint = fingerprint(key);
|
||||||
|
|
||||||
@@ -387,12 +388,7 @@ fn verify_host_key(
|
|||||||
format!("[{host}]:{port}")
|
format!("[{host}]:{port}")
|
||||||
};
|
};
|
||||||
|
|
||||||
// The banner is plain output: the confirmation prompt itself
|
if !prompter.accept_host_key(&display, key_type_desc, &fingerprint) {
|
||||||
// must stay a single line for its redraw logic
|
|
||||||
println!("The authenticity of host '{display}' can't be established.");
|
|
||||||
println!("{key_type_desc} key fingerprint is {fingerprint}.");
|
|
||||||
let accepted = prompt::confirm("Accept and store this host key?", false)?;
|
|
||||||
if !accepted {
|
|
||||||
return Err(format!("Host key for {display} rejected, aborting upload").into());
|
return Err(format!("Host key for {display} rejected, aborting upload").into());
|
||||||
}
|
}
|
||||||
if let Some(name) = key_type_name(key_type) {
|
if let Some(name) = key_type_name(key_type) {
|
||||||
@@ -592,7 +588,7 @@ pub fn upload_file(
|
|||||||
local: &Path,
|
local: &Path,
|
||||||
remote: &str,
|
remote: &str,
|
||||||
host: &str,
|
host: &str,
|
||||||
bar: &ProgressBar,
|
on_progress: &dyn Fn(u64),
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let mut local_file =
|
let mut local_file =
|
||||||
fs::File::open(local).map_err(|e| format!("cannot open '{}': {}", local.display(), e))?;
|
fs::File::open(local).map_err(|e| format!("cannot open '{}': {}", local.display(), e))?;
|
||||||
@@ -602,6 +598,7 @@ pub fn upload_file(
|
|||||||
.map_err(|e| format!("cannot create remote file {remote} on {host}: {e}"))?;
|
.map_err(|e| format!("cannot create remote file {remote} on {host}: {e}"))?;
|
||||||
|
|
||||||
let mut buf = [0u8; 32 * 1024];
|
let mut buf = [0u8; 32 * 1024];
|
||||||
|
let mut uploaded: u64 = 0;
|
||||||
loop {
|
loop {
|
||||||
let n = local_file.read(&mut buf)?;
|
let n = local_file.read(&mut buf)?;
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
@@ -610,7 +607,8 @@ pub fn upload_file(
|
|||||||
remote_file
|
remote_file
|
||||||
.write_all(&buf[..n])
|
.write_all(&buf[..n])
|
||||||
.map_err(|e| format!("failed uploading to {remote} on {host}: {e}"))?;
|
.map_err(|e| format!("failed uploading to {remote} on {host}: {e}"))?;
|
||||||
bar.inc(n as u64);
|
uploaded += n as u64;
|
||||||
|
on_progress(uploaded);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close explicitly: quota-exceeded and similar failures only surface in
|
// Close explicitly: quota-exceeded and similar failures only surface in
|
||||||
|
|||||||
@@ -147,6 +147,15 @@ pub trait Prompter: Send + Sync {
|
|||||||
) -> Result<String, Box<dyn Error>> {
|
) -> Result<String, Box<dyn Error>> {
|
||||||
Ok(default.to_string())
|
Ok(default.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ask whether to accept and store an unverified SSH host key (trust on
|
||||||
|
/// first use): `host` is the display form (`host` or `[host]:port`),
|
||||||
|
/// `key_type` the key type name ("ssh-ed25519", ...) and `fingerprint`
|
||||||
|
/// the human-readable digest. Fail-closed: implementations that cannot
|
||||||
|
/// ask anyone answer `false`, refusing the connection.
|
||||||
|
fn accept_host_key(&self, _host: &str, _key_type: &str, _fingerprint: &str) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Inert view and prompter: drops every event and answers every question
|
/// Inert view and prompter: drops every event and answers every question
|
||||||
|
|||||||
+2
-1
@@ -83,7 +83,8 @@ impl DebUi {
|
|||||||
let pb = multi.add(ProgressBar::new(0));
|
let pb = multi.add(ProgressBar::new(0));
|
||||||
pb.enable_steady_tick(Duration::from_millis(80));
|
pb.enable_steady_tick(Duration::from_millis(80));
|
||||||
pb.set_style(spinner_style());
|
pb.set_style(spinner_style());
|
||||||
pb.set_prefix("Building package");
|
// Neutral identity until a `target` event names the build
|
||||||
|
pb.set_prefix("pkh");
|
||||||
pb.set_message("(starting…)");
|
pb.set_message("(starting…)");
|
||||||
pb
|
pb
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -159,6 +159,14 @@ impl crate::report::Prompter for TerminalPrompter {
|
|||||||
) -> Result<String, Box<dyn std::error::Error>> {
|
) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
text(label, default, validate)
|
text(label, default, validate)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn accept_host_key(&self, host: &str, key_type: &str, fingerprint: &str) -> bool {
|
||||||
|
// The banner is plain output: the confirmation prompt itself must
|
||||||
|
// stay a single line for its redraw logic
|
||||||
|
println!("The authenticity of host '{host}' can't be established.");
|
||||||
|
println!("{key_type} key fingerprint is {fingerprint}.");
|
||||||
|
confirm("Accept and store this host key?", false).unwrap_or(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run `prompt` with the terminal in raw mode, always restoring it
|
/// Run `prompt` with the terminal in raw mode, always restoring it
|
||||||
|
|||||||
Reference in New Issue
Block a user