use std::{
fs,
path::Path,
time::{SystemTime, UNIX_EPOCH},
};
use crate::{
crypto, queries, utils,
v1::{
response_payload, DirContentFile, LocationExistsRequestPayload, LocationExistsResponsePayload,
LocationNameMetadata, LocationTrashRequestPayload, PlainResponsePayload, METADATA_VERSION,
},
FilenSettings,
};
use secstr::{SecUtf8, SecVec};
use serde::{Deserialize, Serialize};
use serde_json::json;
use snafu::{ensure, Backtrace, ResultExt, Snafu};
use uuid::Uuid;
type Result<T, E = Error> = std::result::Result<T, E>;
const FILE_ARCHIVE_PATH: &str = "/v1/file/archive";
const FILE_EXISTS_PATH: &str = "/v1/file/exists";
const FILE_MOVE_PATH: &str = "/v1/file/move";
const FILE_RENAME_PATH: &str = "/v1/file/rename";
const FILE_RESTORE_PATH: &str = "/v1/file/restore";
const FILE_TRASH_PATH: &str = "/v1/file/trash";
const RM_PATH: &str = "/v1/rm";
const USER_DELETE_ALL_PATH: &str = "/v1/user/delete/all";
const USER_RECENT_PATH: &str = "/v1/user/recent";
#[derive(Snafu, Debug)]
pub enum Error {
#[snafu(display("Caller provided invalid argument: {}", message))]
BadArgument { message: String, backtrace: Backtrace },
#[snafu(display("Expected metadata to be base64-encoded, but cannot decode it as such"))]
CannotDecodeBase64Metadata {
metadata: String,
source: base64::DecodeError,
},
#[snafu(display("Failed to deserialize file metadata '{}': {}", metadata, source))]
DeserializeFileMetadataFailed {
metadata: String,
source: serde_json::Error,
},
#[snafu(display("Failed to decrypt file metadata '{}': {}", metadata, source))]
DecryptFileMetadataFailed { metadata: String, source: crypto::Error },
#[snafu(display("Failed to decrypt file metadata '{}' using RSA: {}", metadata, source))]
DecryptFileMetadataRsaFailed { metadata: String, source: crypto::Error },
#[snafu(display("Failed to encrypt file metadata '{}' using RSA: {}", metadata, source))]
EncryptFileMetadataRsaFailed { metadata: String, source: crypto::Error },
#[snafu(display("{} query failed: {}", FILE_ARCHIVE_PATH, source))]
FileArchieveQueryFailed { source: queries::Error },
#[snafu(display("{} query failed: {}", FILE_EXISTS_PATH, source))]
FileExistsQueryFailed { source: queries::Error },
#[snafu(display("{} query failed: {}", FILE_MOVE_PATH, source))]
FileMoveQueryFailed { source: queries::Error },
#[snafu(display("{} query failed: {}", FILE_RENAME_PATH, source))]
FileRenameQueryFailed { source: queries::Error },
#[snafu(display("{} query failed: {}", FILE_RESTORE_PATH, source))]
FileRestoreQueryFailed { source: queries::Error },
#[snafu(display("{} query failed: {}", FILE_TRASH_PATH, source))]
FileTrashQueryFailed { source: queries::Error },
#[snafu(display(
"File path does not contain valid filename.\
Check that given file path is a UTF-8 string with a file name at the end"
))]
FilePathDoesNotContainValidFilename { backtrace: Backtrace },
#[snafu(display("File system failed to get metadata for a file: {}", source))]
FileSystemMetadataError { source: std::io::Error },
#[snafu(display("{} query failed: {}", RM_PATH, source))]
RmQueryFailed { source: queries::Error },
#[snafu(display("Unknown system time error: {}", source))]
SystemTimeError { source: std::time::SystemTimeError },
#[snafu(display("{} query failed: {}", USER_DELETE_ALL_PATH, source))]
UserDeleteAllQueryFailed { source: queries::Error },
#[snafu(display("{} query failed: {}", USER_RECENT_PATH, source))]
UserRecentQueryFailed { source: queries::Error },
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct FileProperties {
pub name: String,
pub size: u64,
pub mime: String,
pub key: SecUtf8,
#[serde(rename = "lastModified")]
pub last_modified: u64,
}
utils::display_from_json!(FileProperties);
impl FileProperties {
pub fn from_name_size_modified_key(
name: &str,
size: u64,
last_modified: &SystemTime,
file_key: Option<SecUtf8>,
) -> Result<Self> {
ensure!(
size > 0,
BadArgumentSnafu {
message: "file size should be > 0"
}
);
let mime_guess = mime_guess::from_path(name).first_raw();
let mime = mime_guess.unwrap_or("");
let last_modified_secs = last_modified
.duration_since(UNIX_EPOCH)
.context(SystemTimeSnafu {})?
.as_secs();
Ok(Self {
name: name.to_owned(),
size,
mime: mime.to_owned(),
key: file_key.unwrap_or_else(|| SecUtf8::from(utils::random_alphanumeric_string(32))),
last_modified: last_modified_secs,
})
}
pub fn from_name_size_modified(name: &str, size: u64, last_modified: &SystemTime) -> Result<Self> {
Self::from_name_size_modified_key(name, size, last_modified, None)
}
pub fn from_local_path(local_file_path: &Path) -> Result<Self> {
match local_file_path.file_name().and_then(std::ffi::OsStr::to_str) {
Some(file_name) => Self::from_name_and_local_path(file_name, local_file_path),
None => FilePathDoesNotContainValidFilenameSnafu {}.fail(),
}
}
pub fn from_name_and_local_path(filen_filename: &str, local_file_path: &Path) -> Result<Self> {
let fs_metadata = fs::metadata(local_file_path).context(FileSystemMetadataSnafu {})?;
let last_modified_time = fs_metadata.modified().unwrap_or_else(|_| SystemTime::now());
Self::from_name_size_modified(filen_filename, fs_metadata.len(), &last_modified_time)
}
pub fn decrypt_file_metadata(metadata: &str, master_keys: &[SecUtf8]) -> Result<Self> {
crypto::decrypt_metadata_str_any_key(metadata, master_keys)
.context(DecryptFileMetadataFailedSnafu {
metadata: metadata.to_owned(),
})
.and_then(|file_properties_json| {
serde_json::from_str::<Self>(&file_properties_json).context(DeserializeFileMetadataFailedSnafu {
metadata: metadata.to_owned(),
})
})
}
#[allow(clippy::missing_panics_doc)]
#[must_use]
pub fn encrypt_file_metadata(file_properties: &Self, last_master_key: &SecUtf8) -> String {
let metadata_json = json!(file_properties).to_string();
crypto::encrypt_metadata_str(&metadata_json, last_master_key, METADATA_VERSION).unwrap()
}
pub fn decrypt_file_metadata_rsa(metadata: &str, rsa_private_key_bytes: &SecVec<u8>) -> Result<Self> {
let decoded = base64::decode(metadata).context(CannotDecodeBase64MetadataSnafu {
metadata: metadata.to_owned(),
})?;
let decrypted = crypto::decrypt_rsa(&decoded, rsa_private_key_bytes.unsecure()).context(
DecryptFileMetadataRsaFailedSnafu {
metadata: metadata.to_owned(),
},
)?;
serde_json::from_slice::<Self>(&decrypted).context(DeserializeFileMetadataFailedSnafu {
metadata: metadata.to_owned(),
})
}
pub fn encrypt_file_metadata_rsa(file_properties: &Self, rsa_public_key_bytes: &[u8]) -> Result<String> {
let metadata_json = json!(file_properties).to_string();
let encrypted = crypto::encrypt_rsa(metadata_json.as_bytes(), rsa_public_key_bytes).context(
EncryptFileMetadataRsaFailedSnafu {
metadata: metadata_json,
},
)?;
Ok(base64::encode(&encrypted))
}
#[must_use]
pub fn to_metadata_string(&self, last_master_key: &SecUtf8) -> String {
Self::encrypt_file_metadata(self, last_master_key)
}
pub fn to_metadata_rsa_string(&self, rsa_public_key_bytes: &[u8]) -> Result<String> {
Self::encrypt_file_metadata_rsa(self, rsa_public_key_bytes)
}
#[allow(clippy::missing_panics_doc)]
#[must_use]
pub fn name_encrypted(&self) -> String {
crypto::encrypt_metadata_str(&self.name, &self.key, METADATA_VERSION).unwrap()
}
#[allow(clippy::missing_panics_doc)]
#[must_use]
pub fn size_encrypted(&self) -> String {
crypto::encrypt_metadata_str(&self.size.to_string(), &self.key, METADATA_VERSION).unwrap()
}
#[allow(clippy::missing_panics_doc)]
#[must_use]
pub fn mime_encrypted(&self) -> String {
crypto::encrypt_metadata_str(&self.mime, &self.key, METADATA_VERSION).unwrap()
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct FileArchiveRequestPayload<'file_archive> {
#[serde(rename = "apiKey")]
pub api_key: &'file_archive SecUtf8,
pub uuid: Uuid,
#[serde(rename = "updateUuid")]
pub update_uuid: Uuid,
}
utils::display_from_json_with_lifetime!('file_archive, FileArchiveRequestPayload);
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct FileMoveRequestPayload<'file_move> {
#[serde(rename = "apiKey")]
pub api_key: &'file_move SecUtf8,
#[serde(rename = "folderUUID")]
pub folder_uuid: Uuid,
#[serde(rename = "fileUUID")]
pub file_uuid: Uuid,
}
utils::display_from_json_with_lifetime!('file_move, FileMoveRequestPayload);
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct FileRenameRequestPayload<'file_rename> {
#[serde(rename = "apiKey")]
pub api_key: &'file_rename SecUtf8,
pub uuid: Uuid,
#[serde(rename = "name")]
pub name_metadata: String,
#[serde(rename = "nameHashed")]
pub name_hashed: String,
#[serde(rename = "metaData")]
pub metadata: String,
}
utils::display_from_json_with_lifetime!('file_rename, FileRenameRequestPayload);
impl<'file_rename> FileRenameRequestPayload<'file_rename> {
#[must_use]
pub fn new(
api_key: &'file_rename SecUtf8,
uuid: Uuid,
new_file_name: &str,
file_metadata: &FileProperties,
last_master_key: &SecUtf8,
) -> Self {
let name_metadata = LocationNameMetadata::encrypt_name_to_metadata(new_file_name, last_master_key);
let name_hashed = LocationNameMetadata::name_hashed(new_file_name);
let metadata = file_metadata.to_metadata_string(last_master_key);
Self {
api_key,
uuid,
name_metadata,
name_hashed,
metadata,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct FileRestoreRequestPayload<'file_restore> {
#[serde(rename = "apiKey")]
pub api_key: &'file_restore SecUtf8,
pub uuid: Uuid,
}
utils::display_from_json_with_lifetime!('file_restore, FileRestoreRequestPayload);
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct RmRequestPayload<'rm> {
pub uuid: Uuid,
pub rm: &'rm str,
}
utils::display_from_json_with_lifetime!('rm, RmRequestPayload);
response_payload!(
UserRecentResponsePayload<Vec<DirContentFile>>
);
pub fn file_archive_request(
payload: &FileArchiveRequestPayload,
filen_settings: &FilenSettings,
) -> Result<PlainResponsePayload> {
queries::query_filen_api(FILE_ARCHIVE_PATH, payload, filen_settings).context(FileArchieveQueryFailedSnafu {})
}
#[cfg(feature = "async")]
pub async fn file_archive_request_async(
payload: &FileArchiveRequestPayload<'_>,
filen_settings: &FilenSettings,
) -> Result<PlainResponsePayload> {
queries::query_filen_api_async(FILE_ARCHIVE_PATH, payload, filen_settings)
.await
.context(FileArchieveQueryFailedSnafu {})
}
pub fn file_exists_request(
payload: &LocationExistsRequestPayload,
filen_settings: &FilenSettings,
) -> Result<LocationExistsResponsePayload> {
queries::query_filen_api(FILE_EXISTS_PATH, payload, filen_settings).context(FileExistsQueryFailedSnafu {})
}
#[cfg(feature = "async")]
pub async fn file_exists_request_async(
payload: &LocationExistsRequestPayload<'_>,
filen_settings: &FilenSettings,
) -> Result<LocationExistsResponsePayload> {
queries::query_filen_api_async(FILE_EXISTS_PATH, payload, filen_settings)
.await
.context(FileExistsQueryFailedSnafu {})
}
pub fn file_move_request(
payload: &FileMoveRequestPayload,
filen_settings: &FilenSettings,
) -> Result<PlainResponsePayload> {
queries::query_filen_api(FILE_MOVE_PATH, payload, filen_settings).context(FileMoveQueryFailedSnafu {})
}
#[cfg(feature = "async")]
pub async fn file_move_request_async(
payload: &FileMoveRequestPayload<'_>,
filen_settings: &FilenSettings,
) -> Result<PlainResponsePayload> {
queries::query_filen_api_async(FILE_MOVE_PATH, payload, filen_settings)
.await
.context(FileMoveQueryFailedSnafu {})
}
pub fn file_rename_request(
payload: &FileRenameRequestPayload,
filen_settings: &FilenSettings,
) -> Result<PlainResponsePayload> {
queries::query_filen_api(FILE_RENAME_PATH, payload, filen_settings).context(FileRenameQueryFailedSnafu {})
}
#[cfg(feature = "async")]
pub async fn file_rename_request_async(
payload: &FileRenameRequestPayload<'_>,
filen_settings: &FilenSettings,
) -> Result<PlainResponsePayload> {
queries::query_filen_api_async(FILE_RENAME_PATH, payload, filen_settings)
.await
.context(FileRenameQueryFailedSnafu {})
}
pub fn file_restore_request(
payload: &FileRestoreRequestPayload,
filen_settings: &FilenSettings,
) -> Result<PlainResponsePayload> {
queries::query_filen_api(FILE_RESTORE_PATH, payload, filen_settings).context(FileRestoreQueryFailedSnafu {})
}
#[cfg(feature = "async")]
pub async fn file_restore_request_async(
payload: &FileRestoreRequestPayload<'_>,
filen_settings: &FilenSettings,
) -> Result<PlainResponsePayload> {
queries::query_filen_api_async(FILE_RESTORE_PATH, payload, filen_settings)
.await
.context(FileRestoreQueryFailedSnafu {})
}
pub fn file_trash_request(
payload: &LocationTrashRequestPayload,
filen_settings: &FilenSettings,
) -> Result<PlainResponsePayload> {
queries::query_filen_api(FILE_TRASH_PATH, payload, filen_settings).context(FileTrashQueryFailedSnafu {})
}
#[cfg(feature = "async")]
pub async fn file_trash_request_async(
payload: &LocationTrashRequestPayload<'_>,
filen_settings: &FilenSettings,
) -> Result<PlainResponsePayload> {
queries::query_filen_api_async(FILE_TRASH_PATH, payload, filen_settings)
.await
.context(FileTrashQueryFailedSnafu {})
}
pub fn rm_request(payload: &RmRequestPayload, filen_settings: &FilenSettings) -> Result<PlainResponsePayload> {
queries::query_filen_api(RM_PATH, payload, filen_settings).context(RmQueryFailedSnafu {})
}
#[cfg(feature = "async")]
pub async fn rm_request_async(
payload: &RmRequestPayload<'_>,
filen_settings: &FilenSettings,
) -> Result<PlainResponsePayload> {
queries::query_filen_api_async(RM_PATH, payload, filen_settings)
.await
.context(RmQueryFailedSnafu {})
}
pub fn user_delete_all_request(api_key: &SecUtf8, filen_settings: &FilenSettings) -> Result<UserRecentResponsePayload> {
queries::query_filen_api(USER_DELETE_ALL_PATH, &utils::api_key_json(api_key), filen_settings)
.context(UserDeleteAllQueryFailedSnafu {})
}
#[cfg(feature = "async")]
pub async fn user_delete_all_request_async(
api_key: &SecUtf8,
filen_settings: &FilenSettings,
) -> Result<UserRecentResponsePayload> {
queries::query_filen_api_async(USER_DELETE_ALL_PATH, &utils::api_key_json(api_key), filen_settings)
.await
.context(UserDeleteAllQueryFailedSnafu {})
}
pub fn user_recent_request(api_key: &SecUtf8, filen_settings: &FilenSettings) -> Result<UserRecentResponsePayload> {
queries::query_filen_api(USER_RECENT_PATH, &utils::api_key_json(api_key), filen_settings)
.context(UserRecentQueryFailedSnafu {})
}
#[cfg(feature = "async")]
pub async fn user_recent_request_async(
api_key: &SecUtf8,
filen_settings: &FilenSettings,
) -> Result<UserRecentResponsePayload> {
queries::query_filen_api_async(USER_RECENT_PATH, &utils::api_key_json(api_key), filen_settings)
.await
.context(UserRecentQueryFailedSnafu {})
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "async")]
use crate::test_utils::validate_contract_async;
use crate::{test_utils::validate_contract, v1::ParentOrBase};
use once_cell::sync::Lazy;
use secstr::SecUtf8;
use std::str::FromStr;
static API_KEY: Lazy<SecUtf8> =
Lazy::new(|| SecUtf8::from("bYZmrwdVEbHJSqeA1RfnPtKiBcXzUpRdKGRkjw9m1o1eqSGP1s6DM11CDnklpFq6"));
const NAME_HASHED: &str = "19d24c63b1170a0b1b40520a636a25235735f39f";
#[test]
fn file_exists_request_should_be_correctly_typed() {
let request_payload = LocationExistsRequestPayload {
api_key: &API_KEY,
parent: ParentOrBase::from_str("b640414e-367e-4df6-b31a-030fd639bcff").unwrap(),
name_hashed: NAME_HASHED.to_owned(),
};
validate_contract(
FILE_EXISTS_PATH,
request_payload,
"tests/resources/responses/file_exists.json",
|request_payload, filen_settings| file_exists_request(&request_payload, &filen_settings),
);
}
#[cfg(feature = "async")]
#[tokio::test]
async fn file_exists_request_async_should_be_correctly_typed() {
let request_payload = LocationExistsRequestPayload {
api_key: &API_KEY,
parent: ParentOrBase::from_str("b640414e-367e-4df6-b31a-030fd639bcff").unwrap(),
name_hashed: NAME_HASHED.to_owned(),
};
validate_contract_async(
FILE_EXISTS_PATH,
request_payload,
"tests/resources/responses/file_exists.json",
|request_payload, filen_settings| async move {
file_exists_request_async(&request_payload, &filen_settings).await
},
)
.await;
}
#[test]
fn user_recent_request_should_be_correctly_typed() {
let request_payload = utils::api_key_json(&API_KEY);
validate_contract(
USER_RECENT_PATH,
request_payload,
"tests/resources/responses/user_recent.json",
|_, filen_settings| user_recent_request(&API_KEY, &filen_settings),
);
}
#[cfg(feature = "async")]
#[tokio::test]
async fn user_recent_request_async_should_be_correctly_typed() {
let request_payload = utils::api_key_json(&API_KEY);
validate_contract_async(
USER_RECENT_PATH,
request_payload,
"tests/resources/responses/user_recent.json",
|_, filen_settings| async move { user_recent_request_async(&API_KEY, &filen_settings).await },
)
.await;
}
}