feat(peer): prototype streamed installs
Add a streamed-install prototype that can receive archive-derived install bytes straight into local/ without first storing the peer-owned root archive payload. This is intended for low-disk clients that want to install a game but opt out of becoming a downloadable peer source for that game. The protocol gains a current-version-only StreamInstall request and framed StreamInstallFrame responses. The peer core owns the generic transport, transaction, path validation, size checks, CRC32 verification, and lifecycle state. The archive-specific work is hidden behind StreamInstallProvider so the prototype can use unrar while the final implementation can swap in a better provider without rewriting the peer command path. The receiver writes into .local.installing and only promotes to local/ after the full stream verifies. It deliberately does not write the root version.ini or archive files, so the settled local state is installed=true, downloaded=false, and availability=LocalOnly. That preserves the existing rule that local/ is not served to peers and makes streamed receivers non-sources by construction. The CLI is the only caller for now. It exposes stream-install and provides the prototype unrar implementation with unrar lt for entry metadata and unrar p for file bytes. This is simple and good enough to prove non-solid archive streaming, but it is not the production provider shape for solid archives because per-file unrar p would repeatedly decompress prefixes. The Tauri app explicitly passes stream_install_provider: None, so the GUI behavior stays unchanged until a real product path is designed. Document the production-readiness work in NEXT_STEPS.md. The main follow-up is to make the provider abstraction final-ish and replace the per-file CLI unrar provider with a one-pass archive provider, then wire a deliberate GUI low-disk mode, retry semantics, and broader failure scenarios. Test Plan: - just fmt - RUSTC_WRAPPER= CARGO_BUILD_RUSTC_WRAPPER= just test - python3 crates/lanspread-peer-cli/scripts/run_extended_scenarios.py \ S39 S40 --build-image - RUSTC_WRAPPER= CARGO_BUILD_RUSTC_WRAPPER= just clippy - git diff --check - git diff --cached --check Follow-up: NEXT_STEPS.md
This commit is contained in:
@@ -32,6 +32,7 @@ mod remote_peer;
|
||||
mod services;
|
||||
mod startup;
|
||||
mod state_paths;
|
||||
mod stream_install;
|
||||
#[cfg(test)]
|
||||
mod test_support;
|
||||
|
||||
@@ -82,6 +83,7 @@ pub use crate::{
|
||||
launch_settings::{LaunchSettingsOutcome, apply_launch_settings_once},
|
||||
startup::PeerRuntimeHandle,
|
||||
state_paths::{launch_settings_applied_path, setup_done_path},
|
||||
stream_install::{NoopStreamInstallProvider, StreamInstallFuture, StreamInstallProvider},
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
@@ -243,6 +245,8 @@ pub enum PeerCommand {
|
||||
file_descriptions: Vec<GameFileDescription>,
|
||||
install_after_download: bool,
|
||||
},
|
||||
/// Stream archive-expanded bytes directly into `local/` without keeping root archives.
|
||||
StreamInstallGame { id: String },
|
||||
/// Install already-downloaded archives into `local/`.
|
||||
InstallGame { id: String },
|
||||
/// Remove only the `local/` install for a game.
|
||||
@@ -260,11 +264,29 @@ pub enum PeerCommand {
|
||||
}
|
||||
|
||||
/// Optional startup settings for non-GUI callers and tests.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[derive(Clone, Default)]
|
||||
pub struct PeerStartOptions {
|
||||
/// Directory used for peer identity and other state.
|
||||
pub state_dir: Option<PathBuf>,
|
||||
pub active_outbound_transfers: Option<crate::context::OutboundTransfers>,
|
||||
/// Provider used to stream archive entries for low-disk streamed installs.
|
||||
pub stream_install_provider: Option<Arc<dyn StreamInstallProvider>>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for PeerStartOptions {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("PeerStartOptions")
|
||||
.field("state_dir", &self.state_dir)
|
||||
.field(
|
||||
"active_outbound_transfers",
|
||||
&self.active_outbound_transfers.as_ref().map(|_| "..."),
|
||||
)
|
||||
.field(
|
||||
"stream_install_provider",
|
||||
&self.stream_install_provider.as_ref().map(|_| "..."),
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -314,11 +336,14 @@ pub fn start_peer_with_options(
|
||||
let PeerStartOptions {
|
||||
state_dir,
|
||||
active_outbound_transfers,
|
||||
stream_install_provider,
|
||||
} = options;
|
||||
let state_dir = resolve_state_dir(state_dir.as_deref());
|
||||
let game_dir = game_dir.into();
|
||||
let active_outbound_transfers = active_outbound_transfers
|
||||
.unwrap_or_else(|| Arc::new(RwLock::new(std::collections::HashMap::new())));
|
||||
let stream_install_provider =
|
||||
stream_install_provider.unwrap_or_else(|| Arc::new(NoopStreamInstallProvider));
|
||||
log::info!(
|
||||
"Starting peer system with game directory: {}",
|
||||
game_dir.display()
|
||||
@@ -338,6 +363,7 @@ pub fn start_peer_with_options(
|
||||
unpacker,
|
||||
catalog,
|
||||
active_outbound_transfers,
|
||||
stream_install_provider,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -355,6 +381,7 @@ async fn run_peer(
|
||||
task_tracker: TaskTracker,
|
||||
catalog: Arc<RwLock<GameCatalog>>,
|
||||
active_outbound_transfers: crate::context::OutboundTransfers,
|
||||
stream_install_provider: Arc<dyn StreamInstallProvider>,
|
||||
) -> eyre::Result<()> {
|
||||
let ctx = Ctx::new(
|
||||
peer_game_db,
|
||||
@@ -366,6 +393,7 @@ async fn run_peer(
|
||||
task_tracker,
|
||||
catalog,
|
||||
active_outbound_transfers,
|
||||
stream_install_provider,
|
||||
);
|
||||
if let Err(err) = load_local_library(&ctx, &tx_notify_ui).await {
|
||||
log::error!("Failed to load initial local game database: {err}");
|
||||
@@ -439,6 +467,9 @@ async fn handle_peer_commands(
|
||||
)
|
||||
.await;
|
||||
}
|
||||
PeerCommand::StreamInstallGame { id } => {
|
||||
handlers::handle_stream_install_game_command(ctx, tx_notify_ui, id).await;
|
||||
}
|
||||
PeerCommand::InstallGame { id } => {
|
||||
handle_install_game_command(ctx, tx_notify_ui, id).await;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user