Portal LogoPortal Docs

Library Usage Guide

Learn how to integrate the pxp library into your Rust applications, implement progress traits, and drive transfer streams.

Library Usage Guide

To use the pxp library, add it to your Cargo.toml dependencies, pinning the version:

[dependencies]
pxp = { version = "0.2.0" }

1. Implementing Core Traits

Before calling the sender or receiver APIs, you must implement the callbacks used for drawing progress interfaces and resolving filename conflicts.

Progress Reporting

The library uses TransferProgress for general transfer stages and ItemProgress to wrap individual read/write streams without chunking.

use pxp::{ItemProgress, TransferProgress};
use tokio::io::{AsyncRead, AsyncWrite};

// Wrap individual file/directory progress indicators
pub struct CustomItemProgress;

impl ItemProgress for CustomItemProgress {
    fn wrap_read(
        &self,
        reader: Box<dyn AsyncRead + Unpin + Send>,
    ) -> Box<dyn AsyncRead + Unpin + Send> {
        // Wrap reader and increment byte counts here
        reader
    }

    fn wrap_write(
        &self,
        writer: Box<dyn AsyncWrite + Unpin + Send>,
    ) -> Box<dyn AsyncWrite + Unpin + Send> {
        // Wrap writer and increment byte counts here
        writer
    }

    fn finish_and_clear(&self) {
        // Hide file-specific progress bars
    }
}

// Drive overall transfer stages
pub struct CustomTransferProgress;

impl TransferProgress for CustomTransferProgress {
    fn set_total_items(&self, total: usize) {
        println!("Expecting {} total top-level items...", total);
    }

    fn set_current_item(&self, current: usize, total: usize) {
        println!("Transferring item {} of {}...", current, total);
    }

    fn create_item_progress(&self, name: &str, total_bytes: u64) -> Box<dyn ItemProgress> {
        println!("Starting item '{}' ({} bytes)", name, total_bytes);
        Box::new(CustomItemProgress)
    }

    fn println(&self, msg: &str) {
        println!("{}", msg);
    }
}

Conflict Resolution

Incoming items are unpacked into a staging dir first, so name collisions are not checked while the stream is running. Once the stream has completed, you call reconcile with your implementation of ConflictResolver; it walks the staged items and resolves any collision against the target path before moving the item into place.

use pxp::{ConflictAction, ConflictResolver, PxpError};

pub struct CustomConflictResolver;

impl ConflictResolver for CustomConflictResolver {
    fn resolve(&self, item_name: &str) -> Result<ConflictAction, PxpError> {
        // In a CLI, prompt the user. In a GUI, pop a modal.
        // Return ConflictAction choice.
        Ok(ConflictAction::Overwrite)
    }
}

Global strategies (Overwrite All, Rename All, Skip All) are remembered by reconcile and applied to every remaining conflict, so your resolver is only called once per distinct item.


2. Running a Sender Session

The sender sequence consists of finding the receiver, connecting, generating the transfer manifest, and initiating the stream.

use pxp::sender::{discover_receiver, connect_to_receiver, create_global_transfer_manifest, send_manifest, send_stream};
use std::path::PathBuf;

async fn run_sender(files: Vec<PathBuf>, username: &str) -> Result<(), pxp::PxpError> {
    // 1. Discover receiver using UDP beacons
    let (ip, node_id, port) = discover_receiver(username, 7878).await?;

    // 2. Connect over TCP and verify receiver's identity proof
    let mut stream = connect_to_receiver(&ip, port, Some(&node_id)).await?;

    // 3. Create transfer metadata summary
    let manifest = create_global_transfer_manifest(
        1,       // total files
        0,       // total directories
        1024,    // total bytes
        None,    // optional description
        Some(username.to_string()),
        true,    // Gzip compressed
    ).await?;

    // 4. Send manifest first
    send_manifest(&mut stream, &manifest).await?;

    // 5. Stream items (optionally passing CustomTransferProgress)
    let progress = CustomTransferProgress;
    let items_to_send = vec![(files[0].clone(), pxp::metadata::TransferItem::File(...))];
    send_stream(stream, items_to_send, false, Some(&progress)).await?;

    Ok(())
}

3. Running a Receiver Session

The receiver binds to a port, accepts the connection, receives the manifest, stages the incoming data stream, and then reconciles the staged items into the target directory.

use pxp::receiver::{handshake::accept_and_read_manifest, stream::receive_stream, reconcile};
use std::path::PathBuf;

async fn run_receiver(port: u16, username: String, target_dir: PathBuf) -> Result<(), pxp::PxpError> {
    // 1. Start beacons, bind TCP, and accept connection
    let handshake = accept_and_read_manifest(port, username).await?;
    let socket = handshake.socket;
    let manifest = handshake.manifest;

    // 2. Process the data stream into a staging dir inside `target_dir`
    let progress = CustomTransferProgress;

    let (stream_result, staged, summary) = receive_stream(
        socket,
        manifest.compressed,
        &target_dir,
        manifest.total_files + manifest.total_directories,
        Some(&progress),
    ).await;

    // 3. Move staged items into place, resolving any filename conflicts
    let resolver = CustomConflictResolver;
    reconcile(&staged, Some(&resolver)).await?;

    stream_result?;
    println!("Successfully received {} bytes!", summary.total_bytes);
    Ok(())
}

The staged items are returned even when the stream failed part-way, so reconcile can still salvage whatever was already received before you report the error.

On this page