dw-rs 0.2.0

A blazingly fast, parallel download accelerator written in Rust.
Documentation
use std::fs::File;
use std::io;

/// Write the entire buffer at an absolute file offset, independent of the
/// file's current cursor.
///
/// Positioned I/O is what makes lock-free parallel writes safe: every worker
/// writes its piece at the correct offset of a single shared `File` handle
/// without seeking or coordinating, because the offset is passed per call
/// rather than stored in the handle.
#[cfg(unix)]
pub fn pwrite_all(file: &File, offset: u64, buf: &[u8]) -> io::Result<()> {
    use std::os::unix::fs::FileExt;
    file.write_all_at(buf, offset)
}

/// Windows has no `write_all_at`; `seek_write` is the positioned-write
/// primitive and may write short, so we loop until the buffer is drained.
#[cfg(windows)]
pub fn pwrite_all(file: &File, mut offset: u64, mut buf: &[u8]) -> io::Result<()> {
    use std::os::windows::fs::FileExt;
    while !buf.is_empty() {
        match file.seek_write(buf, offset) {
            Ok(0) => {
                return Err(io::Error::new(
                    io::ErrorKind::WriteZero,
                    "failed to write whole buffer",
                ));
            }
            Ok(n) => {
                buf = &buf[n..];
                offset += n as u64;
            }
            Err(ref e) if e.kind() == io::ErrorKind::Interrupted => {}
            Err(e) => return Err(e),
        }
    }
    Ok(())
}

/// Derive a filename from the last path segment of a URL.
pub fn extract_filename(url: &str) -> Option<String> {
    url::Url::parse(url).ok().and_then(|u| {
        u.path_segments()
            .and_then(|mut segments| segments.next_back())
            .and_then(|s| {
                if s.is_empty() {
                    None
                } else {
                    Some(s.to_string())
                }
            })
    })
}

/// Parse the total length out of a `Content-Range: bytes START-END/TOTAL`
/// header value. Returns `None` for the unknown-length form (`.../*`).
pub fn parse_content_range_total(value: &str) -> Option<u64> {
    let total = value.rsplit('/').next()?.trim();
    total.parse::<u64>().ok()
}

/// Human-readable byte rate, e.g. `123.4 MB/s`.
pub fn format_speed(bytes_per_sec: f64) -> String {
    const UNITS: [&str; 5] = ["B/s", "KB/s", "MB/s", "GB/s", "TB/s"];
    let mut v = bytes_per_sec;
    let mut i = 0;
    while v >= 1024.0 && i < UNITS.len() - 1 {
        v /= 1024.0;
        i += 1;
    }
    format!("{v:.2} {}", UNITS[i])
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Read;

    #[test]
    fn test_extract_filename_from_simple_url() {
        let url = "http://example.com/some/path/to/file.zip";
        assert_eq!(extract_filename(url), Some("file.zip".to_string()));
    }

    #[test]
    fn test_extract_filename_with_query_params() {
        let url = "http://example.com/archive.tar.gz?token=123&user=abc";
        assert_eq!(extract_filename(url), Some("archive.tar.gz".to_string()));
    }

    #[test]
    fn test_extract_filename_from_root() {
        let url = "http://example.com/";
        assert_eq!(extract_filename(url), None);
    }

    #[test]
    fn test_extract_filename_without_extension() {
        let url = "http://example.com/some/directory/resource";
        assert_eq!(extract_filename(url), Some("resource".to_string()));
    }

    #[test]
    fn test_extract_filename_from_invalid_url() {
        let url = "not-a-valid-url";
        assert_eq!(extract_filename(url), None);
    }

    #[test]
    fn test_parse_content_range_total() {
        assert_eq!(parse_content_range_total("bytes 0-0/12345"), Some(12345));
        assert_eq!(parse_content_range_total("bytes 100-199/200"), Some(200));
        assert_eq!(parse_content_range_total("bytes 0-0/*"), None);
        assert_eq!(parse_content_range_total("garbage"), None);
    }

    #[test]
    fn test_pwrite_all_writes_at_offset() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("pwrite.bin");

        let file = File::create(&path).unwrap();
        file.set_len(10).unwrap();

        pwrite_all(&file, 5, b"WORLD").unwrap();
        pwrite_all(&file, 0, b"HELLO").unwrap();
        drop(file);

        let mut contents = Vec::new();
        File::open(&path)
            .unwrap()
            .read_to_end(&mut contents)
            .unwrap();
        assert_eq!(&contents, b"HELLOWORLD");
    }

    #[test]
    fn test_format_speed() {
        assert_eq!(format_speed(512.0), "512.00 B/s");
        assert_eq!(format_speed(1024.0), "1.00 KB/s");
        assert_eq!(format_speed(1024.0 * 1024.0 * 2.5), "2.50 MB/s");
    }
}