use md5::Digest;
use num::{Integer, Signed};
use reqwest::{header, Client};
use serde::Deserialize;
use time::OffsetDateTime;
use serde_with::serde_as;
use uuid::Uuid;
use crate::api::read_xml;
use crate::auth::AccessToken;
use crate::path::AbsolutePath;
use crate::serde::OptTypoDateTime;
#[serde_as]
#[derive(Debug, Deserialize)]
pub struct Device {
pub name: String,
pub display_name: String,
#[serde(rename = "type")]
pub typ: String,
pub sid: Uuid,
pub size: u64,
#[serde_as(as = "OptTypoDateTime")]
pub modified: Option<OffsetDateTime>,
}
#[derive(Debug, Deserialize)]
pub struct Devices {
#[serde(rename = "$value")]
pub devices: Vec<Device>,
}
#[derive(Debug, Clone, Copy)]
pub enum MaybeUnlimited<T: Integer + Signed> {
Unlimited,
Limited(T),
}
impl<'de, T: Deserialize<'de> + Integer + Signed> Deserialize<'de> for MaybeUnlimited<T> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw = T::deserialize(deserializer)?;
if raw < T::zero() {
Ok(Self::Unlimited)
} else {
Ok(Self::Limited(raw))
}
}
}
impl<T: Integer + Signed + Copy> MaybeUnlimited<T> {
pub fn is_unlimited(&self) -> bool {
matches!(self, Self::Unlimited)
}
pub fn is_limited(&self) -> bool {
self.limit().is_some()
}
pub fn limit(&self) -> Option<T> {
match self {
MaybeUnlimited::Unlimited => None,
MaybeUnlimited::Limited(limit) => Some(*limit),
}
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all(deserialize = "kebab-case"))]
#[allow(clippy::struct_excessive_bools)]
pub struct AccountInfo {
pub username: String,
pub account_type: String,
pub locked: bool,
pub capacity: MaybeUnlimited<i64>,
pub max_devices: MaybeUnlimited<i32>,
pub max_mobile_devices: MaybeUnlimited<i32>,
pub usage: u64,
pub read_locked: bool,
pub write_locked: bool,
pub quota_write_locked: bool,
pub enable_sync: bool,
pub enable_foldershare: bool,
pub devices: Devices,
}
pub async fn get_account(
client: &Client,
username: &str,
token: &AccessToken,
) -> crate::Result<AccountInfo> {
let res = client
.get(format!("https://jfs.jottacloud.com/jfs/{}", username))
.header(header::AUTHORIZATION, format!("Bearer {}", token))
.send()
.await?;
let xml = res.text().await?;
let info = serde_xml_rs::from_str(&xml)?;
Ok(info)
}
#[serde_as]
#[derive(Debug, Deserialize)]
pub struct MountPoint {
pub name: String,
pub size: u64,
#[serde_as(as = "OptTypoDateTime")]
pub modified: Option<OffsetDateTime>,
}
pub async fn list_mountpoints(
client: &Client,
username: &str,
token: &AccessToken,
device_name: &str,
) -> crate::Result<Vec<MountPoint>> {
#[derive(Debug, Deserialize)]
struct MountPoints {
#[serde(rename = "$value")]
inner: Vec<MountPoint>,
}
#[derive(Debug, Deserialize)]
struct Res {
#[serde(rename(deserialize = "mountPoints"))]
mount_points: MountPoints,
}
let res = client
.get(format!(
"https://jfs.jottacloud.com/jfs/{}/{}",
username, device_name,
))
.header(header::AUTHORIZATION, format!("Bearer {}", token))
.send()
.await?;
let data: Res = read_xml(res).await?;
Ok(data.mount_points.inner)
}
#[derive(Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum RevisionState {
Completed,
Incomplete,
Corrupt,
}
#[serde_as]
#[derive(Debug, Deserialize)]
pub struct Revision {
pub number: u32,
pub state: RevisionState,
#[serde_as(as = "OptTypoDateTime")]
pub created: Option<OffsetDateTime>,
#[serde_as(as = "OptTypoDateTime")]
pub modified: Option<OffsetDateTime>,
pub mime: String,
pub size: Option<u64>,
#[serde(with = "crate::serde::md5_hex")]
pub md5: Digest,
#[serde_as(as = "OptTypoDateTime")]
pub updated: Option<OffsetDateTime>,
}
impl Revision {
#[must_use]
pub fn is_complete(&self) -> bool {
self.state == RevisionState::Completed
}
}
#[serde_as]
#[derive(Debug, Deserialize)]
#[serde(rename_all(deserialize = "camelCase"))]
pub struct ListedFile {
pub name: String,
pub uuid: Uuid,
#[serde_as(as = "OptTypoDateTime")]
#[serde(default)]
pub deleted: Option<OffsetDateTime>,
pub current_revision: Option<Revision>,
pub latest_revision: Option<Revision>,
}
#[derive(Debug, Deserialize, Default)]
pub struct Files {
#[serde(rename = "$value")]
pub inner: Vec<ListedFile>,
}
#[serde_as]
#[derive(Debug, Deserialize)]
pub struct Folder {
pub name: String,
#[serde_as(as = "OptTypoDateTime")]
#[serde(default)]
pub deleted: Option<OffsetDateTime>,
}
impl Folder {
#[must_use]
pub fn is_deleted(&self) -> bool {
self.deleted.is_some()
}
}
impl From<FolderDetail> for Folder {
fn from(f: FolderDetail) -> Self {
Self {
name: f.name,
deleted: None,
}
}
}
#[derive(Debug, Deserialize, Default)]
pub struct Folders {
#[serde(rename = "$value")]
pub inner: Vec<Folder>,
}
#[derive(Debug, Deserialize)]
pub struct IndexMeta {
pub total: u32,
pub num_folders: u32,
pub num_files: u32,
}
#[serde_as]
#[derive(Debug, Deserialize)]
pub struct FolderDetail {
pub name: String,
pub path: AbsolutePath,
#[serde(default)]
pub folders: Folders,
#[serde(default)]
pub files: Files,
pub metadata: Option<IndexMeta>,
}
#[derive(Debug, Deserialize, Default)]
pub struct Revisions {
#[serde(rename = "$value")]
pub inner: Vec<Revision>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all(deserialize = "camelCase"))]
pub struct FileDetail {
pub name: String,
pub uuid: Uuid,
pub path: AbsolutePath,
pub abspath: AbsolutePath,
pub latest_revision: Option<Revision>,
pub current_revision: Option<Revision>,
#[serde(default)]
pub revisions: Revisions,
}
impl FileDetail {
#[must_use]
pub fn last_upload_complete(&self) -> bool {
self.latest_revision.is_none()
&& self
.current_revision
.as_ref()
.map_or(false, Revision::is_complete)
}
}