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:
2026-09-18 20:42:17 +02:00
parent bb76e41908
commit 54cb04ba27
6 changed files with 62 additions and 93 deletions
+29 -80
View File
@@ -18,7 +18,6 @@ pub mod target;
use std::cmp::Ordering;
use std::path::{Path, PathBuf};
use indicatif::{MultiProgress, ProgressBar};
use log::info;
use serde::{Deserialize, Serialize};
@@ -27,10 +26,10 @@ use crate::debian::checksums::FileChecksums;
use crate::debian::control::ControlInfo;
use crate::debian::version::DebianVersion;
use crate::launchpad;
use crate::ui;
use crate::report::{BuildView, Prompter};
/// Everything `put` needs to run.
pub struct PutOptions {
pub struct PutOptions<'a> {
/// PPA to upload to, `user/ppa_name` format.
pub ppa: String,
/// Explicit `.changes` file to upload; when `None`, the one matching the
@@ -42,14 +41,25 @@ pub struct PutOptions {
pub force: bool,
/// Source package directory (the one containing `debian/`).
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
/// progress bars.
pub async fn put(
opts: &PutOptions,
multi: &MultiProgress,
) -> Result<(), Box<dyn std::error::Error>> {
/// Upload the package described by `opts` to its target, reporting progress
/// through the view and asking the prompter when the server is unknown.
pub async fn put(opts: &PutOptions<'_>) -> Result<(), Box<dyn std::error::Error>> {
// The display is released on every path (success and early `?` bails)
// before the outcome is logged, so no stale status line lingers above it
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 ssh_config = ssh::lookup_ssh_config(&target.fqdn);
@@ -79,21 +89,10 @@ pub async fn put(
};
let changes = changes::parse(&changes_path)?;
// The summary line stays up for the whole flow: the completion message
// 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!(
opts.view.message(&format!(
"Uploading {} {} to {}",
changes.source, changes.version, target.label
));
summary.enable_steady_tick(TICK);
bars.track(summary.clone());
changes::validate(&changes)?;
// 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));
checking.set_style(ui::spinner_style());
checking.set_prefix(format!("Checking {} on Launchpad...", target.label));
checking.enable_steady_tick(TICK);
bars.track(checking.clone());
opts.view
.message(&format!("Checking {} on Launchpad...", target.label));
launchpad::ppa_info(&opts.ppa).await?;
launchpad::check_ppa_series(&changes.distribution).await?;
clear_bar(&checking);
// Last pre-flight check: a version the PPA already publishes at or
// above the changes' one would supersede (or reject) this upload
@@ -134,14 +129,10 @@ pub async fn put(
.into());
}
let connecting = multi.add(ProgressBar::new(0));
connecting.set_style(ui::spinner_style());
connecting.set_prefix(format!("Connecting to {login}@{host}:{port}..."));
connecting.enable_steady_tick(TICK);
bars.track(connecting.clone());
let session = ssh::connect(&host, port, &login, &ssh_config)?;
opts.view
.message(&format!("Connecting to {login}@{host}:{port}..."));
let session = ssh::connect(&host, port, &login, &ssh_config, opts.prompter)?;
let sftp = ssh::sftp(&session)?;
clear_bar(&connecting);
// Payload first, the .changes file last (like dput), so the server-side
// queue processor can never pick up an incomplete upload
@@ -179,15 +170,11 @@ pub async fn put(
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 result = ssh::upload_file(&sftp, path, &remote, &host, &bar);
bar.finish_and_clear();
let label = format!("Uploading {name}");
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 {
// The failed file itself joins the cleanup: its remote `create`
// may have succeeded before the failure, leaving a partial — or,
@@ -204,9 +191,6 @@ pub async fn put(
// replaying is safe).
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!(
"Upload of {} {} to {} complete.",
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)
/// against the distribution's valid sections: a bare section or a
/// `section/subsection` is accepted. Archives reject uploads carrying an