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:
@@ -4,7 +4,7 @@ use bytes::Bytes;
|
||||
use lanspread_db::db::{Game, GameFileDescription};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const PROTOCOL_VERSION: u32 = 4;
|
||||
pub const PROTOCOL_VERSION: u32 = 5;
|
||||
|
||||
pub use lanspread_db::db::Availability;
|
||||
|
||||
@@ -67,6 +67,9 @@ pub enum Request {
|
||||
offset: u64,
|
||||
length: u64,
|
||||
},
|
||||
StreamInstall {
|
||||
game_id: String,
|
||||
},
|
||||
Hello(Hello),
|
||||
LibraryDelta {
|
||||
peer_id: String,
|
||||
@@ -94,6 +97,35 @@ pub enum Response {
|
||||
InternalPeerError(String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum StreamInstallFrame {
|
||||
ArchiveBegin {
|
||||
archive_name: String,
|
||||
solid: bool,
|
||||
},
|
||||
Directory {
|
||||
relative_path: String,
|
||||
},
|
||||
FileBegin {
|
||||
relative_path: String,
|
||||
size: u64,
|
||||
crc32: Option<u32>,
|
||||
},
|
||||
FileChunk {
|
||||
bytes: Bytes,
|
||||
},
|
||||
FileEnd {
|
||||
relative_path: String,
|
||||
},
|
||||
ArchiveEnd {
|
||||
archive_name: String,
|
||||
},
|
||||
Complete,
|
||||
Error {
|
||||
message: String,
|
||||
},
|
||||
}
|
||||
|
||||
// Add Message trait
|
||||
pub trait Message {
|
||||
fn decode(bytes: Bytes) -> Self;
|
||||
@@ -145,3 +177,29 @@ impl Message for Response {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Message for StreamInstallFrame {
|
||||
fn decode(bytes: Bytes) -> Self {
|
||||
match serde_json::from_slice(&bytes) {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
tracing::error!(?e, "StreamInstallFrame decoding error");
|
||||
StreamInstallFrame::Error {
|
||||
message: format!("stream install frame decoding error: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn encode(&self) -> Bytes {
|
||||
match serde_json::to_vec(self) {
|
||||
Ok(s) => Bytes::from(s),
|
||||
Err(e) => {
|
||||
tracing::error!(?e, "StreamInstallFrame encoding error");
|
||||
Bytes::from(format!(
|
||||
r#"{{"Error": {{"message": "encoding error: {e}"}}}}"#
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user