Cross-Platform Files
Unifying File Descriptors, File Paths, and Browser Blobs into one API.
One of the biggest headaches when building a true cross-platform SDK in Rust is file handling.
- On a desktop, you read a file by providing a File Path.
- On the Web, there is no file system at all! We have to deal with JavaScript
FileorBlobobjects. - On iOS or Android, files are heavily sandboxed. Native languages (Swift/Kotlin) must open the file and pass a raw POSIX File Descriptor (FD) down to Rust.
If we let this complexity leak into the chunking or networking logic, our codebase would become an unmaintainable nightmare of #[cfg(target_arch)] blocks.
To solve this, we built UploadSource and XPlatFile.
Uploading via UploadSource
We don't want SDK consumers (or even our internal chunkers) worrying about platform-specific file APIs. We created an UploadSource object that exposes completely different constructors depending on the target platform!
Requires a standard Rust std::fs::File descriptor.
Whether you are on a desktop CLI using a local file path, or on iOS using a sandboxed file descriptor from NSFileProvider, the Rust std::fs::File struct handles the OS-level file handle transparently.
#[cfg(not(target_arch = "wasm32"))]
impl UploadSource {
pub fn new(file_name: String, file: std::fs::File) -> Self {
Self {
file_name,
xplat: XPlatFile::new(file),
}
}
}By doing this, a Swift developer simply passes an Int32 descriptor, a Web developer passes an HTML <input> file object, and the SDK instantly standardizes it into our internal XPlatFile structure!
Unifying the Engine: XPlatFile
Once UploadSource wraps the OS-specific file handle, it's passed into our XPlatFile enum. This is the magical boundary that completely isolates the rest of the SDK from platform differences.
// 1. The Core Enum
pub enum XPlatFile {
#[cfg(not(target_arch = "wasm32"))]
Native(std::fs::File),
#[cfg(target_arch = "wasm32")]
Wasm(web_sys::File),
}The rest of the SDK (like the Chunking Engine) only interacts with XPlatFile through a unified async trait interface.
The most important method is read_exact_at, which grabs a specific byte slice of the file for chunk uploading:
Uses std::os::unix::fs::FileExt for concurrent read access
impl XPlatFile {
pub async fn read_exact_at(&self, offset: u64, size: usize) -> Result<Vec<u8>, String> {
match self {
#[cfg(not(target_arch = "wasm32"))]
XPlatFile::Native(file) => {
use std::os::unix::fs::FileExt;
let mut buffer: Vec<u8> = vec![0; size];
file.read_exact_at(&mut buffer, offset)
.map_err(|e| e.to_string())?;
Ok(buffer)
}
// ... WASM branch omitted
#[cfg(target_arch = "wasm32")]
XPlatFile::Wasm(file) => unimplemented!(),
}
}
}The Magic of Blob Slicing on the Web
Notice how the WASM implementation works. We don't try to load the entire JavaScript file into Wasm memory (which would instantly crash the browser). Instead, we utilize the native browser Blob.slice() API to carve out just the 4MB chunk we need, request an ArrayBuffer, await the JS Promise natively in Rust via wasm_bindgen_futures, and copy only those specific bytes into our Rust vector!
This abstraction allows our core chunking loop to run identical logic on both an iPhone and a Chrome browser, completely unaware of how vastly different the underlying file I/O operations are!
Downloading via DownloadDestination
While uploading uses UploadSource, downloading writes to a DownloadDestination. However, writing files exposed a massive concurrency divergence between Native and WebAssembly:
- Native (Random Access): Desktop and mobile OSes support random-access disk writes (
std::os::unix::fs::FileExt::write_all_at). This means we can download chunks concurrently and write them to disk the exact millisecond they finish downloading, in any random order! This keeps RAM usage incredibly low and avoids serialization bottlenecks. - WebAssembly (Sequential Streams): The browser uses the Web Streams API combined with a Service Worker to stream the download directly to the user's disk. The resulting
WritableStreamstrictly enforces sequential writes. You cannot write chunk 3 before chunk 1 and 2.
To solve this without leaking cfg boilerplate into the download loop, we exposed two entirely separate write strategies on XPlatFile, gated by compile-time architecture checks:
Random Access via write_exact_at
impl XPlatFile {
#[cfg(not(target_arch = "wasm32"))]
pub async fn write_exact_at(&self, offset: u64, bytes: &[u8]) -> Result<(), String> {
match self {
XPlatFile::Native(file) => {
use std::os::unix::fs::FileExt;
file.write_all_at(bytes, offset).map_err(|e| e.to_string())
}
}
}
}In the download loop, Native uses FuturesUnordered to yield network chunks instantly and blindly calls write_exact_at, fully utilizing parallel I/O.
By strictly gating these methods via #[cfg] macros directly on the function signatures, the compiler physically prevents Native code from accidentally calling sequential streams, and prevents WASM from attempting random access!