sproto 0.1.0

Rust client for the Synology Drive sync protocol
Documentation
use std::{
	fs::{self, File},
	path::Path,
};
use tokio::{fs as tokio_fs, io::AsyncWrite};

use crate::{
	channel::{Channel, LazyWriter},
	client::Client,
	error::Result,
	frame,
	pstream::PObject,
};

/// Download a file by its `file_id` to a local path.
///
/// The `file_id` comes from a prior `list_dir` call's `NodeInfo`.
/// The destination file is only created if the server sends binary data;
/// metadata-only responses (e.g. dedup/refer) leave the filesystem untouched.
pub async fn download(ch: &mut Channel, client: &Client, file_id: &str, dest: &Path) -> Result<()> {
	let dest = dest.to_path_buf();
	let mut writer = LazyWriter::new(move || {
		if let Some(parent) = dest.parent() {
			fs::create_dir_all(parent)?;
		}
		let file = File::create(&dest)?;
		Ok(tokio_fs::File::from_std(file))
	});

	download_to(ch, client, file_id, &mut writer).await?;

	Ok(())
}

/// Download a file by its `file_id` to any `AsyncWrite` sink.
///
/// Returns the full response `PObject` containing file metadata
/// (`sync_id`, `path`, `file.hash`, `file.size`, etc.).
pub async fn download_to<W: AsyncWrite + Unpin + Send>(
	ch: &mut Channel,
	client: &Client,
	file_id: &str,
	dest: &mut W,
) -> Result<PObject> {
	let request = build_download_request(client, file_id)?;

	tracing::debug!(file_id, "sending download request");
	ch.send(frame::SCMD_REQUEST, &request).await?;

	tracing::debug!(file_id, "receiving download response");
	let result = ch.recv_download(dest).await;
	tracing::debug!(file_id, "download complete");
	result
}

/// Minimum server build that supports the `"id:<file_id>"` path format.
const MIN_FILE_ID_BUILD: u64 = 12001;

fn build_download_request(client: &Client, file_id: &str) -> Result<PObject> {
	if client.server_build < MIN_FILE_ID_BUILD {
		return Err(crate::Error::UnsupportedServer {
			build: client.server_build,
			min_build: MIN_FILE_ID_BUILD,
			reason: "downloads by file ID require id:-style paths, \
			         which are not supported on this server build"
				.into(),
		});
	}

	Ok(client.build_request(
		"download",
		pmap! {
			"sync_id" => 0u64,
			"file" => pmap! { "offset" => 0u64 },
			"path" => PObject::Str(format!("id:{file_id}")),
		},
	))
}