Zillowe FoundationZillowe Documentation

Getting Started

Learn how to make a control file and run a transfer.

Edit this page on GitLab

Describing a file

ControlFile::generate reads a file once and describes it. The length is passed in rather than sought for, so the block size can be chosen before the first block is read.

use zync::{ControlFile, GenerateOptions};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut file = std::fs::File::open("image.bin")?;
    let length = file.metadata()?.len();

    let options = GenerateOptions {
        filename: Some("image.bin"),
        urls: vec!["https://example.com/image.bin".to_string()],
        ..GenerateOptions::default()
    };
    let control = ControlFile::generate(&mut file, length, &options)?;

    let mut out = std::fs::File::create("image.zsync")?;
    control.write(&mut out)?;
    Ok(())
}

Running a transfer

An Assembly owns the transfer. Local copies are submitted first, so the matcher knows what it already has before anything is fetched.

use std::path::Path;
use zync::{Assembly, ControlFile, GenerateOptions};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut file = std::fs::File::open("image.bin")?;
    let length = file.metadata()?.len();
    let control = ControlFile::generate(
        &mut file,
        length,
        &GenerateOptions {
            urls: vec!["https://example.com/image.bin".to_string()],
            ..GenerateOptions::default()
        },
    )?;

    let mut assembly = Assembly::new(control, "out.bin")?;

    let held = assembly.submit_file(Path::new("image.bin.old"))?;
    let (done, total) = assembly.progress();
    println!("{held} blocks matched, {done} of {total} bytes held");

    let stats = assembly.fetch()?;
    println!(
        "fetched {} bytes in {} requests",
        stats.downloaded, stats.requests
    );

    assembly.finish()?;
    Ok(())
}

submit_file may be called several times. Every block any of the supplied files holds is kept, and they are also the bases a sidecar patch is applied to.

Finishing

Assembly::finish verifies the assembled file against the SHA-1 the control file records, and only then puts it in place. It returns Error::Incomplete if any block is still missing and Error::DigestMismatch if the result does not hash to what was promised. Dropping an assembly without finishing removes its partial output, so a failed transfer never leaves a file behind that looks like a result.

Watching progress

fn report(assembly: &mut zync::Assembly) {
    assembly.set_progress(|done, total| {
        println!("\r{done}/{total} blocks");
    });
}

A long transfer can also be cancelled, which returns Error::Aborted at the next block boundary rather than running to completion:

use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};

fn cancellable(assembly: &mut zync::Assembly) -> Arc<AtomicBool> {
    let flag = Arc::new(AtomicBool::new(false));
    assembly.set_abort_flag(Arc::clone(&flag));
    flag
}

// Raise it from another thread, or from a signal handler:
fn request_stop(flag: &AtomicBool) {
    flag.store(true, Ordering::Relaxed);
}

Supplying your own transport

Every remote read goes through the Transport trait, so the whole transfer can be driven without a socket — which is how this crate's own tests work. An embedder that must refuse a downgrade to plain HTTP, or that pins its own trust roots, supplies an agent:

use std::sync::Arc;
use zync::{HttpTransport, Transport};

fn strict(assembly: zync::Assembly) -> zync::Assembly {
    let http = HttpTransport::with_agent(
        ureq::Agent::config_builder()
            .https_only(true)
            .build()
            .new_agent(),
    );
    assembly.with_transport(Arc::new(http))
}

See the sidecar guide for rebuilding missing regions from patches instead of fetching them.


A software organization

2026 © All Rights Reserved.

  • All the content is available under CC BY-SA 4.0, expect where otherwise stated.

Last updated on