use std::{
collections::BTreeMap,
path::Path,
sync::Arc,
time::{SystemTime, UNIX_EPOCH},
};
use tokio::io::AsyncWrite;
use uuid::Uuid;
use crate::{
Error, actions,
channel::{self, Channel, TlsMode},
error::Result,
frame,
pool::{self, Pool},
pstream::PObject,
};
#[derive(Clone)]
pub struct Client {
pool: Arc<Pool>,
agent: PObject,
session: String,
pub(crate) server_build: u64,
}
#[derive(macon::Builder)]
pub struct Config {
#[builder(Default=!)]
pub host: String,
#[builder(Default)]
pub port: Port,
#[builder(Default)]
pub tls: TlsMode,
pub credentials: Credentials,
pub device_uuid: Option<String>,
#[builder(Default)]
pub max_channels: MaxChannels,
}
pub enum Credentials {
Session { session: String, restore_id: String },
Password {
username: String,
password: String,
otp: Option<String>,
},
}
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Port(pub u16);
impl Default for Port {
fn default() -> Self {
Self(6690)
}
}
impl From<u16> for Port {
fn from(v: u16) -> Self {
Self(v)
}
}
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MaxChannels(pub usize);
impl Default for MaxChannels {
fn default() -> Self {
Self(5)
}
}
impl From<usize> for MaxChannels {
fn from(v: usize) -> Self {
Self(v)
}
}
impl Client {
#[tracing::instrument(skip_all, fields(host = %config.host, port = config.port.0))]
pub async fn connect(config: Config) -> Result<Self> {
let device_uuid = config
.device_uuid
.unwrap_or_else(|| Uuid::new_v4().to_string());
let tls_config = match config.tls {
TlsMode::None => None,
_ => Some(Arc::new(channel::build_tls_config(config.tls)?)),
};
tracing::debug!("connecting auth channel");
let mut ch =
Channel::connect(&config.host, config.port.0, config.tls, tls_config.as_ref()).await?;
let (session, supplied_restore_id) = match &config.credentials {
Credentials::Password {
username,
password,
otp,
} => {
let req = pmap! {
"_action" => "auth",
"dry_run" => false,
"renew_session" => "",
"client_type" => "drive",
"username" => username.as_str(),
"password" => password.as_str(),
"client" => "SynologyDriveClient",
"otp" => otp.as_deref().unwrap_or(""),
"client_version" => crate::frame::CLIENT_VERSION,
};
tracing::debug!("authenticating");
let resp = ch.request(frame::SCMD_AUTH, &req).await?;
tracing::debug!("authenticated");
let session = resp
.get("session")
.and_then(|v| v.as_str())
.ok_or_else(|| Error::Decode("missing session in auth response".into()))?
.to_string();
(session, None)
},
Credentials::Session {
session,
restore_id,
} => (session.clone(), Some(restore_id.as_str())),
};
let info_req = pmap! {
"_action" => "query_server_info",
"get_all" => true,
"session" => session.as_str(),
};
tracing::debug!("querying server info");
let info_resp = ch.request(frame::SCMD_SERVER_INFO, &info_req).await?;
tracing::debug!("server info received");
let restore_id = info_resp
.get("database_restore_id")
.and_then(|v| v.as_str())
.or(supplied_restore_id)
.unwrap_or("")
.to_string();
let server_build = info_resp
.get("package_version")
.and_then(|v| v.get("build"))
.and_then(PObject::as_int)
.unwrap_or(0);
let agent = crate::pool::agent_map(&device_uuid, &restore_id);
let pool = Arc::new(Pool::new(pool::Config {
tls_config,
restore_id,
device_uuid,
tls: config.tls,
host: config.host,
port: config.port.0,
session: session.clone(),
max_channels: config.max_channels.0,
})?);
tracing::debug!("donating auth channel to pool");
pool.donate(ch).await?;
tracing::debug!("client ready");
Ok(Self {
pool,
agent,
session,
server_build,
})
}
pub async fn warm_pool(&self, count: usize) -> Result<()> {
self.pool.warm(count).await
}
#[must_use]
pub(crate) fn build_request(&self, action: &str, fields: PObject) -> PObject {
let mut map = BTreeMap::new();
map.insert(
"@proto".into(),
pmap! {
"type" => "header",
"body-continue" => false,
"date" => unix_timestamp(),
"version" => pmap! {
"major" => 7u64,
"minor" => 0u64,
},
},
);
map.insert("_agent".into(), self.agent.clone());
map.insert("_action".into(), PObject::Str(action.to_string()));
map.insert("session".into(), PObject::Str(self.session.clone()));
if let PObject::Map(mut extra) = fields {
map.append(&mut extra);
}
PObject::Map(map)
}
pub async fn list_shares(&self) -> Result<Vec<actions::list::ShareInfo>> {
let mut guard = self.pool.acquire().await?;
let result = actions::list::list_team_folder(guard.channel(), self).await;
guard.poison_on_err(result)
}
pub async fn list_dir(&self, view_id: u64, path: &str) -> Result<Vec<actions::list::NodeInfo>> {
let mut guard = self.pool.acquire().await?;
let result = actions::list::list(guard.channel(), self, view_id, path).await;
guard.poison_on_err(result)
}
pub async fn list_sync(
&self,
view_id: u64,
path: &str,
cursor: Option<&str>,
) -> Result<(Vec<actions::list::NodeInfo>, Option<String>)> {
let mut guard = self.pool.acquire().await?;
let result =
actions::list::list_sync_to_device(guard.channel(), self, view_id, path, cursor).await;
guard.poison_on_err(result)
}
pub async fn download(&self, file_id: &str, dest: &Path) -> Result<()> {
let mut guard = self.pool.acquire().await?;
let result = actions::download::download(guard.channel(), self, file_id, dest).await;
guard.poison_on_err(result)
}
pub async fn download_to<W: AsyncWrite + Unpin + Send>(
&self,
file_id: &str,
dest: &mut W,
) -> Result<()> {
let mut guard = self.pool.acquire().await?;
let result = actions::download::download_to(guard.channel(), self, file_id, dest).await;
guard.poison_on_err(result.map(|_| ()))
}
}
fn unix_timestamp() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
}