use crate::{
queries, utils,
v1::{
crypto, response_payload, DownloadBtnState, DownloadBtnStateByte, Expire, PasswordState, PlainResponsePayload,
SEC_LINK_EMPTY_PASSWORD_VALUE,
},
FilenSettings,
};
use secstr::SecUtf8;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use snafu::{ResultExt, Snafu};
use strum::{Display, EnumString};
use uuid::Uuid;
type Result<T, E = Error> = std::result::Result<T, E>;
const LINK_EDIT_PATH: &str = "/v1/link/edit";
const LINK_STATUS_PATH: &str = "/v1/link/status";
#[derive(Snafu, Debug)]
pub enum Error {
#[snafu(display("{} query failed: {}", LINK_EDIT_PATH, source))]
LinkEditQueryFailed { source: queries::Error },
#[snafu(display("{} query failed: {}", LINK_STATUS_PATH, source))]
LinkStatusQueryFailed { source: queries::Error },
}
#[derive(Clone, Copy, Debug, Deserialize, Display, EnumString, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
#[strum(ascii_case_insensitive, serialize_all = "lowercase")]
pub enum LinkState {
Disable,
Enable,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct LinkEditRequestPayload<'link_edit> {
#[serde(rename = "apiKey")]
pub api_key: &'link_edit SecUtf8,
#[serde(rename = "downloadBtn")]
pub download_btn: DownloadBtnState,
pub expiration: Expire,
#[serde(rename = "fileUUID")]
pub file_uuid: Uuid,
pub password: PasswordState,
#[serde(rename = "passwordHashed")]
pub password_hashed: String,
pub salt: String,
#[serde(rename = "type")]
pub link_type: LinkState,
pub uuid: Uuid,
}
utils::display_from_json_with_lifetime!('link_edit, LinkEditRequestPayload);
impl<'link_edit> LinkEditRequestPayload<'link_edit> {
#[must_use]
pub fn new(
api_key: &'link_edit SecUtf8,
file_uuid: Uuid,
download_btn: DownloadBtnState,
expiration: Expire,
state: LinkState,
link_uuid: Option<Uuid>,
link_plain_password: Option<&SecUtf8>,
) -> Self {
let (password_hashed, salt) = link_plain_password.map_or_else(
|| crypto::encrypt_to_link_password_and_salt(&SEC_LINK_EMPTY_PASSWORD_VALUE),
crypto::encrypt_to_link_password_and_salt,
);
Self {
api_key,
download_btn,
expiration,
file_uuid,
password: link_plain_password.map_or(PasswordState::Empty, |_| PasswordState::NotEmpty),
password_hashed,
salt,
link_type: state,
uuid: link_uuid.unwrap_or_else(Uuid::new_v4),
}
}
#[must_use]
pub fn enabled(
api_key: &'link_edit SecUtf8,
file_uuid: Uuid,
download_btn: DownloadBtnState,
expiration: Expire,
link_uuid: Option<Uuid>,
link_plain_password: Option<&SecUtf8>,
) -> Self {
Self::new(
api_key,
file_uuid,
download_btn,
expiration,
LinkState::Enable,
link_uuid,
link_plain_password,
)
}
#[must_use]
pub fn disabled(api_key: &'link_edit SecUtf8, file_uuid: Uuid, link_uuid: Uuid) -> Self {
Self::new(
api_key,
file_uuid,
DownloadBtnState::Enable,
Expire::Never,
LinkState::Disable,
Some(link_uuid),
None,
)
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct LinkStatusRequestPayload<'link_status> {
#[serde(rename = "apiKey")]
pub api_key: &'link_status SecUtf8,
#[serde(rename = "fileUUID")]
pub file_uuid: Uuid,
}
utils::display_from_json_with_lifetime!('link_status, LinkStatusRequestPayload);
#[skip_serializing_none]
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct LinkStatusResponseData {
pub enabled: bool,
pub uuid: Option<Uuid>,
pub expiration: Option<u64>,
#[serde(rename = "expirationText")]
pub expiration_text: Option<Expire>,
#[serde(rename = "downloadBtn")]
pub download_btn: DownloadBtnStateByte,
pub password: Option<String>,
}
utils::display_from_json!(LinkStatusResponseData);
response_payload!(
LinkStatusResponsePayload<LinkStatusResponseData>
);
pub fn link_edit_request(
payload: &LinkEditRequestPayload,
filen_settings: &FilenSettings,
) -> Result<PlainResponsePayload> {
queries::query_filen_api(LINK_EDIT_PATH, payload, filen_settings).context(LinkEditQueryFailedSnafu {})
}
#[cfg(feature = "async")]
pub async fn link_edit_request_async(
payload: &LinkEditRequestPayload<'_>,
filen_settings: &FilenSettings,
) -> Result<PlainResponsePayload> {
queries::query_filen_api_async(LINK_EDIT_PATH, payload, filen_settings)
.await
.context(LinkEditQueryFailedSnafu {})
}
pub fn link_status_request(
payload: &LinkStatusRequestPayload,
filen_settings: &FilenSettings,
) -> Result<LinkStatusResponsePayload> {
queries::query_filen_api(LINK_STATUS_PATH, payload, filen_settings).context(LinkStatusQueryFailedSnafu {})
}
#[cfg(feature = "async")]
pub async fn link_status_request_async(
payload: &LinkStatusRequestPayload<'_>,
filen_settings: &FilenSettings,
) -> Result<LinkStatusResponsePayload> {
queries::query_filen_api_async(LINK_STATUS_PATH, payload, filen_settings)
.await
.context(LinkStatusQueryFailedSnafu {})
}
#[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_status_request_should_have_proper_contract_for_disabled_link() {
let request_payload = LinkStatusRequestPayload {
api_key: &API_KEY,
file_uuid: Uuid::nil(),
};
validate_contract(
LINK_STATUS_PATH,
request_payload,
"tests/resources/responses/link_status_disabled.json",
|request_payload, filen_settings| link_status_request(&request_payload, &filen_settings),
);
}
#[cfg(feature = "async")]
#[tokio::test]
async fn link_status_request_async_should_have_proper_contract_for_disabled_link() {
let request_payload = LinkStatusRequestPayload {
api_key: &API_KEY,
file_uuid: Uuid::nil(),
};
validate_contract_async(
LINK_STATUS_PATH,
request_payload,
"tests/resources/responses/link_status_disabled.json",
|request_payload, filen_settings| async move {
link_status_request_async(&request_payload, &filen_settings).await
},
)
.await;
}
#[test]
fn link_status_request_should_have_proper_contract_for_link_without_password() {
let request_payload = LinkStatusRequestPayload {
api_key: &API_KEY,
file_uuid: Uuid::nil(),
};
validate_contract(
LINK_STATUS_PATH,
request_payload,
"tests/resources/responses/link_status_enabled_no_password.json",
|request_payload, filen_settings| link_status_request(&request_payload, &filen_settings),
);
}
#[cfg(feature = "async")]
#[tokio::test]
async fn link_status_request_async_should_have_proper_contract_for_link_without_password() {
let request_payload = LinkStatusRequestPayload {
api_key: &API_KEY,
file_uuid: Uuid::nil(),
};
validate_contract_async(
LINK_STATUS_PATH,
request_payload,
"tests/resources/responses/link_status_enabled_no_password.json",
|request_payload, filen_settings| async move {
link_status_request_async(&request_payload, &filen_settings).await
},
)
.await;
}
#[test]
fn link_status_request_should_have_proper_contract_for_link_with_password() {
let request_payload = LinkStatusRequestPayload {
api_key: &API_KEY,
file_uuid: Uuid::nil(),
};
validate_contract(
LINK_STATUS_PATH,
request_payload,
"tests/resources/responses/link_status_enabled_with_password.json",
|request_payload, filen_settings| link_status_request(&request_payload, &filen_settings),
);
}
#[cfg(feature = "async")]
#[tokio::test]
async fn link_status_request_async_should_have_proper_contract_for_link_with_password() {
let request_payload = LinkStatusRequestPayload {
api_key: &API_KEY,
file_uuid: Uuid::nil(),
};
validate_contract_async(
LINK_STATUS_PATH,
request_payload,
"tests/resources/responses/link_status_enabled_with_password.json",
|request_payload, filen_settings| async move {
link_status_request_async(&request_payload, &filen_settings).await
},
)
.await;
}
}