#[cfg(feature = "async")]
use crate::v1::{dir_link_add_request_async, download_dir_request_async, link_edit_request_async};
use crate::{
queries, secstr, utils, uuid, v1,
v1::{
crypto, dir_link_add_request, dir_links, download_dir, download_dir_request, file_links, link_edit_request,
response_payload, Backtrace, DirLinkAddRequestPayload, DownloadBtnState, DownloadDirRequestPayload, Expire,
FileProperties, FilenResponse, HasFileMetadata, HasLinkKey, HasLocationName, HasUuid, LinkEditRequestPayload,
LocationNameMetadata, ParentOrBase, PlainResponsePayload, METADATA_VERSION,
},
FilenSettings, SettingsBundle,
};
use secstr::SecUtf8;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use snafu::{ResultExt, Snafu};
use uuid::Uuid;
type Result<T, E = Error> = std::result::Result<T, E>;
const LINK_DIR_ITEM_RENAME_PATH: &str = "/v1/link/dir/item/rename";
const LINK_DIR_ITEM_STATUS_PATH: &str = "/v1/link/dir/item/status";
const LINK_DIR_STATUS_PATH: &str = "/v1/link/dir/status";
#[derive(Snafu, Debug)]
pub enum Error {
#[snafu(display("Caller provided invalid argument: {}", message))]
BadArgument { message: String, backtrace: Backtrace },
#[snafu(display("{}", message))]
CannotDisableFileLink { message: String, backtrace: Backtrace },
#[snafu(display("{}", message))]
CannotEnableFileLink { message: String, backtrace: Backtrace },
#[snafu(display("{}", message))]
CannotEnableFolderLink { message: String, backtrace: Backtrace },
#[snafu(display("{}", source))]
CannotGetUserFolderContents { source: v1::Error },
#[snafu(display("{}", source))]
DirLinkAddRequestPayloadCreationFailed { source: dir_links::Error },
#[snafu(display("{}", source))]
DirLinkAddQueryFailed { source: dir_links::Error },
#[snafu(display("download_dir_request() failed: {}", source))]
DownloadDirRequestFailed { source: download_dir::Error },
#[snafu(display("{} query failed: {}", LINK_DIR_ITEM_RENAME_PATH, source))]
LinkDirItemRenameQueryFailed { source: queries::Error },
#[snafu(display("{} query failed: {}", LINK_DIR_ITEM_STATUS_PATH, source))]
LinkDirItemStatusQueryFailed { source: queries::Error },
#[snafu(display("{} query failed: {}", LINK_DIR_STATUS_PATH, source))]
LinkDirStatusQueryFailed { source: queries::Error },
#[snafu(display("{}", source))]
LinkEditQueryFailed { source: file_links::Error },
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct LinkDirItemRenameRequestPayload<'link_dir_item_rename> {
#[serde(rename = "apiKey")]
pub api_key: &'link_dir_item_rename SecUtf8,
pub uuid: Uuid,
#[serde(rename = "linkUUID")]
pub link_uuid: Uuid,
pub metadata: String,
}
utils::display_from_json_with_lifetime!('link_dir_item_rename, LinkDirItemRenameRequestPayload);
impl<'link_dir_item_rename> LinkDirItemRenameRequestPayload<'link_dir_item_rename> {
#[must_use]
pub fn from_file_properties(
api_key: &'link_dir_item_rename SecUtf8,
link_uuid: Uuid,
file_uuid: Uuid,
file_properties: &FileProperties,
link_key: &SecUtf8,
) -> Self {
let metadata = file_properties.to_metadata_string(link_key);
Self {
api_key,
metadata,
link_uuid,
uuid: file_uuid,
}
}
#[must_use]
pub fn from_folder_name(
api_key: &'link_dir_item_rename SecUtf8,
link_uuid: Uuid,
folder_uuid: Uuid,
folder_name: &str,
link_key: &SecUtf8,
) -> Self {
let metadata = LocationNameMetadata::encrypt_name_to_metadata(folder_name, link_key);
Self {
api_key,
metadata,
link_uuid,
uuid: folder_uuid,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct LinkDirItemStatusRequestPayload<'link_dir_item_status> {
#[serde(rename = "apiKey")]
pub api_key: &'link_dir_item_status SecUtf8,
pub uuid: Uuid,
}
utils::display_from_json_with_lifetime!('link_dir_item_status, LinkDirItemStatusRequestPayload);
#[skip_serializing_none]
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct LinkDirItemStatusResponseData {
pub link: bool,
#[serde(default)]
pub links: Vec<LinkIdWithKey>,
}
utils::display_from_json!(LinkDirItemStatusResponseData);
response_payload!(
LinkDirItemStatusResponsePayload<LinkDirItemStatusResponseData>
);
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct LinkDirStatusRequestPayload<'link_dir_status> {
#[serde(rename = "apiKey")]
pub api_key: &'link_dir_status SecUtf8,
pub uuid: Uuid,
}
utils::display_from_json_with_lifetime!('link_dir_status, LinkDirStatusRequestPayload);
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct LinkIdWithKey {
#[serde(rename = "linkKey")]
pub link_key_metadata: String,
#[serde(rename = "linkUUID")]
pub link_uuid: Uuid,
}
utils::display_from_json!(LinkIdWithKey);
impl HasLinkKey for LinkIdWithKey {
fn link_key_metadata_ref(&self) -> Option<&str> {
Some(&self.link_key_metadata)
}
}
impl LinkIdWithKey {
#[allow(clippy::missing_panics_doc)]
#[must_use]
pub fn generate(last_master_key: &SecUtf8) -> Self {
let (link_uuid, link_key_plain) = Self::generate_unencrypted();
let link_key_metadata =
crypto::encrypt_metadata_str(link_key_plain.unsecure(), last_master_key, METADATA_VERSION).unwrap();
Self {
link_key_metadata,
link_uuid,
}
}
#[must_use]
pub fn generate_unencrypted() -> (Uuid, SecUtf8) {
(Uuid::new_v4(), SecUtf8::from(utils::random_alphanumeric_string(32)))
}
}
#[skip_serializing_none]
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct LinkDirStatusResponseData {
pub link: bool,
#[serde(default)]
pub links: Vec<LinkIdWithKey>,
}
utils::display_from_json!(LinkDirStatusResponseData);
response_payload!(
LinkDirStatusResponsePayload<LinkDirStatusResponseData>
);
pub fn link_dir_item_rename_request(
payload: &LinkDirItemRenameRequestPayload,
filen_settings: &FilenSettings,
) -> Result<PlainResponsePayload> {
queries::query_filen_api(LINK_DIR_ITEM_RENAME_PATH, payload, filen_settings)
.context(LinkDirItemRenameQueryFailedSnafu {})
}
#[cfg(feature = "async")]
pub async fn link_dir_item_rename_request_async(
payload: &LinkDirItemRenameRequestPayload<'_>,
filen_settings: &FilenSettings,
) -> Result<PlainResponsePayload> {
queries::query_filen_api_async(LINK_DIR_ITEM_RENAME_PATH, payload, filen_settings)
.await
.context(LinkDirItemRenameQueryFailedSnafu {})
}
pub fn link_dir_item_status_request(
payload: &LinkDirItemStatusRequestPayload,
filen_settings: &FilenSettings,
) -> Result<LinkDirStatusResponsePayload> {
queries::query_filen_api(LINK_DIR_ITEM_STATUS_PATH, payload, filen_settings)
.context(LinkDirItemStatusQueryFailedSnafu {})
}
#[cfg(feature = "async")]
pub async fn link_dir_item_status_request_async(
payload: &LinkDirItemStatusRequestPayload<'_>,
filen_settings: &FilenSettings,
) -> Result<LinkDirStatusResponsePayload> {
queries::query_filen_api_async(LINK_DIR_ITEM_STATUS_PATH, payload, filen_settings)
.await
.context(LinkDirItemStatusQueryFailedSnafu {})
}
pub fn link_dir_status_request(
payload: &LinkDirStatusRequestPayload,
filen_settings: &FilenSettings,
) -> Result<LinkDirStatusResponsePayload> {
queries::query_filen_api(LINK_DIR_STATUS_PATH, payload, filen_settings).context(LinkDirStatusQueryFailedSnafu {})
}
#[cfg(feature = "async")]
pub async fn link_dir_status_request_async(
payload: &LinkDirStatusRequestPayload<'_>,
filen_settings: &FilenSettings,
) -> Result<LinkDirStatusResponsePayload> {
queries::query_filen_api_async(LINK_DIR_STATUS_PATH, payload, filen_settings)
.await
.context(LinkDirStatusQueryFailedSnafu {})
}
pub fn disable_file_link(
api_key: &SecUtf8,
file_uuid: Uuid,
link_uuid: Uuid,
filen_settings: &FilenSettings,
) -> Result<String> {
let link_disable_payload = LinkEditRequestPayload::disabled(api_key, file_uuid, link_uuid);
let link_disable_response =
link_edit_request(&link_disable_payload, filen_settings).context(LinkEditQueryFailedSnafu {})?;
let message = link_disable_response.message_ref().unwrap_or_default().to_owned();
if link_disable_response.status {
Ok(message)
} else {
CannotDisableFileLinkSnafu { message }.fail()
}
}
#[cfg(feature = "async")]
pub async fn disable_file_link_async(
api_key: &SecUtf8,
file_uuid: Uuid,
link_uuid: Uuid,
filen_settings: &FilenSettings,
) -> Result<String> {
let link_disable_payload = LinkEditRequestPayload::disabled(api_key, file_uuid, link_uuid);
let link_disable_response = link_edit_request_async(&link_disable_payload, filen_settings)
.await
.context(LinkEditQueryFailedSnafu {})?;
let message = link_disable_response.message_ref().unwrap_or_default().to_owned();
if link_disable_response.status {
Ok(message)
} else {
CannotDisableFileLinkSnafu { message }.fail()
}
}
pub fn enable_file_link(
api_key: &SecUtf8,
file_uuid: Uuid,
download_button_state: DownloadBtnState,
expiration: Expire,
link_plain_password: Option<&SecUtf8>,
filen_settings: &FilenSettings,
) -> Result<Uuid> {
let link_enable_payload = LinkEditRequestPayload::enabled(
api_key,
file_uuid,
download_button_state,
expiration,
None,
link_plain_password,
);
let link_enable_response =
link_edit_request(&link_enable_payload, filen_settings).context(LinkEditQueryFailedSnafu {})?;
let message = link_enable_response.message_ref().unwrap_or_default().to_owned();
if link_enable_response.status {
Ok(link_enable_payload.uuid)
} else {
CannotEnableFileLinkSnafu { message }.fail()
}
}
#[cfg(feature = "async")]
pub async fn enable_file_link_async(
api_key: &SecUtf8,
file_uuid: Uuid,
download_button_state: DownloadBtnState,
expiration: Expire,
link_plain_password: Option<&SecUtf8>,
filen_settings: &FilenSettings,
) -> Result<Uuid> {
let link_enable_payload = LinkEditRequestPayload::enabled(
api_key,
file_uuid,
download_button_state,
expiration,
None,
link_plain_password,
);
let link_enable_response = link_edit_request_async(&link_enable_payload, filen_settings)
.await
.context(LinkEditQueryFailedSnafu {})?;
let message = link_enable_response.message_ref().unwrap_or_default().to_owned();
if link_enable_response.status {
Ok(link_enable_payload.uuid)
} else {
CannotEnableFileLinkSnafu { message }.fail()
}
}
pub fn add_file_to_link<'add_file_to_link, T: HasFileMetadata + HasUuid>(
api_key: &'add_file_to_link SecUtf8,
file_data: &T,
parent: ParentOrBase,
link_uuid: Uuid,
link_key_metadata: &'add_file_to_link str,
master_keys: &[SecUtf8],
filen_settings: &FilenSettings,
) -> Result<String> {
let dir_link_add_payload =
DirLinkAddRequestPayload::from_file_data(api_key, file_data, parent, link_uuid, link_key_metadata, master_keys)
.context(DirLinkAddRequestPayloadCreationFailedSnafu {})?;
let dir_link_add_response =
dir_link_add_request(&dir_link_add_payload, filen_settings).context(DirLinkAddQueryFailedSnafu {})?;
let message = dir_link_add_response.message_ref().unwrap_or_default().to_owned();
if dir_link_add_response.status {
Ok(message)
} else {
CannotEnableFileLinkSnafu { message }.fail()
}
}
#[cfg(feature = "async")]
pub async fn add_file_to_link_async<'add_file_to_link, T: HasFileMetadata + HasUuid + Sync>(
api_key: &'add_file_to_link SecUtf8,
file_data: &T,
parent: ParentOrBase,
link_uuid: Uuid,
link_key_metadata: &'add_file_to_link str,
master_keys: &[SecUtf8],
filen_settings: &FilenSettings,
) -> Result<String> {
let dir_link_add_payload =
DirLinkAddRequestPayload::from_file_data(api_key, file_data, parent, link_uuid, link_key_metadata, master_keys)
.context(DirLinkAddRequestPayloadCreationFailedSnafu {})?;
let dir_link_add_response = dir_link_add_request_async(&dir_link_add_payload, filen_settings)
.await
.context(DirLinkAddQueryFailedSnafu {})?;
let message = dir_link_add_response.message_ref().unwrap_or_default().to_owned();
if dir_link_add_response.status {
Ok(message)
} else {
CannotEnableFileLinkSnafu { message }.fail()
}
}
pub fn add_folder_to_link<'add_folder_to_link, T: HasLocationName + HasUuid>(
api_key: &'add_folder_to_link SecUtf8,
folder_data: &T,
parent: ParentOrBase,
link_uuid: Uuid,
link_key_metadata: &'add_folder_to_link str,
master_keys: &[SecUtf8],
filen_settings: &FilenSettings,
) -> Result<String> {
let dir_link_add_payload = DirLinkAddRequestPayload::from_folder_data(
api_key,
folder_data,
parent,
link_uuid,
link_key_metadata,
master_keys,
)
.context(DirLinkAddRequestPayloadCreationFailedSnafu {})?;
let dir_link_add_response =
dir_link_add_request(&dir_link_add_payload, filen_settings).context(DirLinkAddQueryFailedSnafu {})?;
let message = dir_link_add_response.message_ref().unwrap_or_default().to_owned();
if dir_link_add_response.status {
Ok(message)
} else {
CannotEnableFolderLinkSnafu { message }.fail()
}
}
#[cfg(feature = "async")]
pub async fn add_folder_to_link_async<'add_folder_to_link, T: HasLocationName + HasUuid + Sync>(
api_key: &'add_folder_to_link SecUtf8,
folder_data: &T,
parent: ParentOrBase,
link_uuid: Uuid,
link_key_metadata: &'add_folder_to_link str,
master_keys: &[SecUtf8],
filen_settings: &FilenSettings,
) -> Result<String> {
let dir_link_add_payload = DirLinkAddRequestPayload::from_folder_data(
api_key,
folder_data,
parent,
link_uuid,
link_key_metadata,
master_keys,
)
.context(DirLinkAddRequestPayloadCreationFailedSnafu {})?;
let dir_link_add_response = dir_link_add_request_async(&dir_link_add_payload, filen_settings)
.await
.context(DirLinkAddQueryFailedSnafu {})?;
let message = dir_link_add_response.message_ref().unwrap_or_default().to_owned();
if dir_link_add_response.status {
Ok(message)
} else {
CannotEnableFolderLinkSnafu { message }.fail()
}
}
pub fn link_folder_recursively(
api_key: &SecUtf8,
folder_uuid: Uuid,
master_keys: &[SecUtf8],
settings: &SettingsBundle,
) -> Result<LinkIdWithKey> {
let last_master_key = match master_keys.last() {
Some(key) => key,
None => BadArgumentSnafu {
message: "master keys cannot be empty",
}
.fail()?,
};
let content_payload = DownloadDirRequestPayload {
api_key,
uuid: folder_uuid,
};
let contents_response = settings
.retry
.call(|| download_dir_request(&content_payload, &settings.filen))
.context(DownloadDirRequestFailedSnafu {})?;
let contents = contents_response
.data_ref_or_err()
.context(CannotGetUserFolderContentsSnafu {})?;
let link_id_with_key = LinkIdWithKey::generate(last_master_key);
let link_metadata = &link_id_with_key.link_key_metadata;
contents
.folders
.iter()
.map(|folder| {
settings.retry.call(|| {
let parent = if folder.uuid == folder_uuid {
ParentOrBase::Base
} else {
folder.parent
};
add_folder_to_link(
api_key,
folder,
parent,
link_id_with_key.link_uuid,
link_metadata,
master_keys,
&settings.filen,
)
.map(|_| ())
})
})
.collect::<Result<Vec<()>>>()?;
contents
.files
.iter()
.map(|file| {
settings.retry.call(|| {
add_file_to_link(
api_key,
file,
ParentOrBase::Folder(file.parent),
link_id_with_key.link_uuid,
link_metadata,
master_keys,
&settings.filen,
)
.map(|_| ())
})
})
.collect::<Result<Vec<()>>>()?;
Ok(link_id_with_key)
}
#[cfg(feature = "async")]
pub async fn link_folder_recursively_async(
api_key: &SecUtf8,
folder_uuid: Uuid,
master_keys: &[SecUtf8],
settings: &SettingsBundle,
) -> Result<LinkIdWithKey> {
let last_master_key = match master_keys.last() {
Some(key) => key,
None => BadArgumentSnafu {
message: "master keys cannot be empty",
}
.fail()?,
};
let content_payload = DownloadDirRequestPayload {
api_key,
uuid: folder_uuid,
};
let contents_response = settings
.retry
.call_async(|| download_dir_request_async(&content_payload, &settings.filen))
.await
.context(DownloadDirRequestFailedSnafu {})?;
let contents = contents_response
.data_ref_or_err()
.context(CannotGetUserFolderContentsSnafu {})?;
let link_id_with_key = LinkIdWithKey::generate(last_master_key);
let link_uuid = link_id_with_key.link_uuid;
let link_metadata = &link_id_with_key.link_key_metadata;
let folder_futures = contents.folders.iter().map(|folder| {
settings.retry.call_async(|| async {
let parent = if folder.uuid == folder_uuid {
ParentOrBase::Base
} else {
folder.parent
};
add_folder_to_link_async(
api_key,
folder,
parent,
link_uuid,
link_metadata,
master_keys,
&settings.filen,
)
.await
.map(|_| ())
})
});
futures::future::try_join_all(folder_futures).await?;
let file_futures = contents.files.iter().map(|file| {
settings.retry.call_async(|| async {
add_file_to_link_async(
api_key,
file,
ParentOrBase::Folder(file.parent),
link_uuid,
link_metadata,
master_keys,
&settings.filen,
)
.await
.map(|_| ())
})
});
futures::future::try_join_all(file_futures).await?;
Ok(link_id_with_key)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::validate_contract;
#[cfg(feature = "async")]
use crate::test_utils::validate_contract_async;
use once_cell::sync::Lazy;
use secstr::SecUtf8;
static API_KEY: Lazy<SecUtf8> =
Lazy::new(|| SecUtf8::from("bYZmrwdVEbHJSqeA1RfnPtKiBcXzUpRdKGRkjw9m1o1eqSGP1s6DM11CDnklpFq6"));
#[test]
fn link_dir_status_request_should_have_proper_contract_for_no_link() {
let request_payload = LinkDirStatusRequestPayload {
api_key: &API_KEY,
uuid: Uuid::nil(),
};
validate_contract(
LINK_DIR_STATUS_PATH,
request_payload,
"tests/resources/responses/link_dir_status_no_link.json",
|request_payload, filen_settings| link_dir_status_request(&request_payload, &filen_settings),
);
}
#[cfg(feature = "async")]
#[tokio::test]
async fn link_dir_status_request_async_should_have_proper_contract_for_no_link() {
let request_payload = LinkDirStatusRequestPayload {
api_key: &API_KEY,
uuid: Uuid::nil(),
};
validate_contract_async(
LINK_DIR_STATUS_PATH,
request_payload,
"tests/resources/responses/link_dir_status_no_link.json",
|request_payload, filen_settings| async move {
link_dir_status_request_async(&request_payload, &filen_settings).await
},
)
.await;
}
#[test]
fn link_dir_status_request_should_have_proper_contract_for_a_link() {
let request_payload = LinkDirStatusRequestPayload {
api_key: &API_KEY,
uuid: Uuid::nil(),
};
validate_contract(
LINK_DIR_STATUS_PATH,
request_payload,
"tests/resources/responses/link_dir_status.json",
|request_payload, filen_settings| link_dir_status_request(&request_payload, &filen_settings),
);
}
#[cfg(feature = "async")]
#[tokio::test]
async fn link_dir_status_request_async_should_have_proper_contract_for_a_link() {
let request_payload = LinkDirStatusRequestPayload {
api_key: &API_KEY,
uuid: Uuid::nil(),
};
validate_contract_async(
LINK_DIR_STATUS_PATH,
request_payload,
"tests/resources/responses/link_dir_status.json",
|request_payload, filen_settings| async move {
link_dir_status_request_async(&request_payload, &filen_settings).await
},
)
.await;
}
}