3个版本 (破坏性更新)

0.3.0 2022年5月26日
0.2.0 2021年11月22日
0.1.0 2021年11月16日

HTTP客户端 中排名 263

MIT 许可证

495KB
10K SLoC

rust_filen — 构建状态 最新版本

这是一个用于从Rust调用Filen.io API的库。

Filen.io 是一个云存储提供商,拥有开源的 桌面客户端。我的目标是编写一个库,以一种有意义的方式调用Filen的API,并在过程中学习Rust。在写作时,Filen的API没有文档,我通过研究客户端的源代码来尽力做到正确,所以请带着一颗宽容的心接受。

这个库处于可使用但未完善的阶段。可以执行几乎所有的Filen查询:您可以登录,接收用户的RSA密钥,查看用户选项和事件,对文件和文件夹进行CRUD操作,列出Filen同步文件夹/回收站文件夹/最近文件的目录内容,下载和解密文件,加密和上传文件,以及使用递归辅助工具共享/链接文件/文件夹。

一些难以理解的特定于用户的API查询尚未实现,并且文档几乎不存在,对此表示歉意。如果您需要调用缺失的API查询,可以使用 rust_filen::queries::query_filen_api("/v1/some/uimplemented/api/path", any_serde_serializable_payload, filen_settings) 来实现。

可选的异步

默认情况下,所有查询都是同步的,并使用 ureq 执行。

如果您愿意,可以为每个API查询启用异步版本,并提供重试方式,使用RetrySettings::call_async。要这样做,请将此库的features = ["async"]设置到您的Cargo.toml文件中。结果,将使用reqwest而不是ureq

一些示例

所有Filen API请求都以原始URL命名,并在末尾附加_request。通常,请求与相关的*RequestPayload结构体相关联,该结构体对应于原始发送的JSON,以及*ResponsePayload结构体,该结构体对应于JSON响应。例如,/v1/user/baseFolders请求将由rust_filen::v1::user_base_folders_requestUserBaseFoldersRequestPayload执行,并将响应存储在UserBaseFoldersResponsePayload中。

如果您对此感兴趣,下面是一系列小演示,其中包含了您开始使用Filen所需的所有信息。首先,导入所有示例所需的内容

use rust_filen::{*, v1::*};
// All Filen API queries and related structs are in v1::*,
// while rust_filen::* provides FilenSettings, RetrySettings and their bundle
// for convenience, aptly called SettingsBundle.
// Also, for advanced usage, there are rust_filen::crypto
// with crypto-functions to encrypt/decrypt various Filen metadata and
// rust_filen::queries as a way to define your own Filen API queries.
use rust_filen::secstr::SecUtf8;
use rust_filen::uuid::Uuid;

在讨论导入时,rust_filen会重新导出在公共函数中使用的所有第三方crates。具体来说,包括rust_filen::ureqrust_filen::reqwestrust_filen::furerust_filen::retryrust_filen::secstrrust_filen::uuid

无论如何,让我们先登录。

获取认证信息

实际上,在登录之前,我们必须知道如何这样做。

// First let's calculate Filen password used for logging in.
// For that we need to know user's password, of course,
// and 'version' number which tells us which encryption algorithm to use.
// To get it all, call `/auth/info` endpoint:
let user_email = SecUtf8::from("[email protected]");
let user_password = SecUtf8::from("user.password.in.plaintext");
 // Filen actually uses XXXXXX when 2FA is absent.
let user_two_factor_key = SecUtf8::from("XXXXXX");
let settings = STANDARD_SETTINGS_BUNDLE.clone();
let filen_settings = &settings.filen;  // Provides Filen server URLs.

let auth_info_request_payload = AuthInfoRequestPayload {
    email: &user_email,
    two_factor_key: &user_two_factor_key,
};
let auth_info_response = auth_info_request(&auth_info_request_payload, filen_settings)?;
if !auth_info_response.status {
    panic!("Filen API failed to return auth info: {:?}", auth_info_response.message);
}
let auth_info_response_data = auth_info_response.data_ref_or_err()?;

// `filen_password_with_master_key` helper calculates Filen password for us,
// depending on returned auth_info_response_data.
let filen_password_and_m_key = auth_info_response_data
    .filen_password_with_master_key(&user_password)?;

登录

// Now that we have Filen password, we can login. Master key is not needed for login,
// but is also very important, since it is used often throughout the API to encrypt/decrypt metadata.
let login_request_payload = LoginRequestPayload {
    email: &user_email,
    password: &filen_password_and_m_key.sent_password,
    two_factor_key: &user_two_factor_key,
    auth_version: auth_info_response_data.auth_version,
};
let login_response = login_request(&login_request_payload, filen_settings)?;
if !login_response.status {
    panic!("Filen API failed to login: {:?}", auth_info_response.message);
}

// Login confirmed, now you can take API key and user's master key from the LoginResponseData
// and go perform some API calls!
let login_response_data = login_response.data_ref_or_err()?;

// Api key is needed for almost every call to Filen API, so it's a must have.
let api_key = &login_response_data.api_key;

// Last master key is used for encryption of user's private data.
let last_master_key = filen_password_and_m_key.m_key;

// List of all user's master keys is used for decryption of user's private data.
// New master key is generated by Filen each time user changes password,
// so when decrypting previously encrypted user data we have to try not
// only the last master key, but all of the previous keys as well.
let master_keys = &login_response_data.decrypt_master_keys_metadata(&last_master_key)?;

获取用户的默认文件夹

// Let's start by finding user's default folder:
let user_dirs_response = user_dirs_request(api_key, filen_settings)?;
if !user_dirs_response.status {
    panic!(
        "Filen API failed to provide user dirs: {:?}",
        user_dirs_response.message
    );
}

// This is just a convenience helper, 
// you can iterate folders in user_dirs_response.data yourself:
let default_folder_data = user_dirs_response.find_default_folder().unwrap();

获取远程文件夹内容

// Alright, we have our default folder, let's check out its contents.
let download_dir_request_payload = DownloadDirRequestPayload {
    api_key,
    uuid: default_folder_data.uuid,
};
let download_dir_response = download_dir_request(&download_dir_request_payload, filen_settings)?;
if !download_dir_response.status {
    panic!(
        "Filen API failed to provide default folder contents: {:?}",
        download_dir_response.message
    );
}
// Again, this is just a helper method, feel free to decrypt metadata for every FileData yourself.
let download_dir_response_data = download_dir_response.data_ref_or_err()?;
let default_folder_files_and_properties =
    download_dir_response_data.decrypt_all_file_properties(&master_keys)?;

下载和解密文件

// Let's say user has a file 'some file.png' located in the default folder.
// Let's find and download it:
let (some_file_data, some_file_properties) = default_folder_files_and_properties
    .iter()
    .find(|(data, properties)|
        data.parent == default_folder_data.uuid &&
        properties.name.eq_ignore_ascii_case("some file.png"))
    .unwrap();
let file_key = some_file_properties.key.clone();

// Let's store file in-memory via writer over vec:
let mut file_writer = std::io::BufWriter::new(Vec::new());

// STANDARD_SETTINGS_BUNDLE earlier contained `retry` field with STANDARD_RETRIES,
// which retry 5 times with 1, 2, 4, 8 and 15 seconds pause
// between retries and some random jitter.
// Usually RetrySettings is opt-in, you call `RetrySettings::call` yourself
// when needed for every API query you want retried.
//
// File is downloaded or uploaded as a sequence of chunks,
// and a query to download or upload any one of them can fail.
// With external retries, if the last chunk fails, you'll have to redo the entire file.
// Internal retry logic avoid possible needless work.
//
// For this reason, file download/upload and other complex helper methods 
// with chains of Filen API queries inside, require reference to RetrySettings
// in addition to usual FilenSettings.
// So file download below uses settings bundle we defined earlier, which contains them: 
let sync_file_download_result = download_and_decrypt_file_from_data_and_key(
    some_file_data,
    &file_key,
    &mut file_writer,
    &settings,
);

// And now we have downloaded and decrypted bytes in memory.
let file_bytes = sync_file_download_result.map(|_| file_writer.into_inner().unwrap())?;

上传加密文件

// First let's define the file we will upload:
let file_path = <std::path::PathBuf as std::str::FromStr>::from_str(
    "D:\\file_path\\some_file.txt")?;

// Then let's define where the file will be uploaded on Filen. 
// If you're wondering how you can check Filen folder IDs to choose folder to upload to,
// check previous section "Gettings user's default folder" or queries with 'dir' in their names,
// like `user_dirs_request` and `dir_content_request`.
let parent_folder_id = Uuid::parse_str("cf2af9a0-6f4e-485d-862c-0459f4662cf1").unwrap(); 

// Prepare file properties like file size and mime type for Filen.
// 'some_file.txt' is specified again here, because you can change uploaded file name if you want: 
let file_properties =
    FileProperties::from_name_and_local_path("some_file.txt", &file_path)?;

// `file_version` determines how file bytes should be encrypted/decrypted,
// for now Filen uses version = 1 everywhere.
let file_version = 1;

// Now open a file into a reader, so encrypt_and_upload_file() can read file bytes later: 
let mut file_reader = std::io::BufReader::new(
    std::fs::File::open(file_path.to_str().ok_or_else(|| "Path is not a valid unicode")?)
        .expect("Unable to open file"),
);

// We're all done:
let upload_result = encrypt_and_upload_file(
    api_key,
    parent_folder_id,
    &file_properties,
    file_version,
    &last_master_key,
    &mut file_reader,
    &settings,
);

创建新文件夹

// All folders in Filen can be divided into 'base' and 'non-base'.
// Base folders are called "cloud drives" in the web manager,
// non-base folders are your usual folders.
// 
// So let's create a new cloud drive, where we will put a new folder.
// But before creating something new, you should always check if it's name is free to use.
// 
// Since we will be creating 2 folders, it would be wise to put
// this check into a separate helper function:
fn folder_exists(
    api_key: &SecUtf8,
    // ParentOrBase defines whether to seek folder_name among base folders or
    // in the given parent folder.
    parent: ParentOrBase,
    // Folder name to check.
    folder_name: &str,
    // Plain-text folder name.
    filen_settings: &FilenSettings,
) -> std::result::Result<bool, Box<dyn std::error::Error>> {
    let folder_exists_payload = LocationExistsRequestPayload::new(api_key, parent, folder_name);
    let dir_exists_response = dir_exists_request(&folder_exists_payload, filen_settings)?;
    let dir_exists_data = dir_exists_response.data_ref_or_err()?;
    Ok(dir_exists_data.exists)
}

// Alright, now we have everything we need to create some folders.
let new_base_folder_name = "New cloud drive";

// Check that base folder with name "New cloud drive" does not exist already.
if folder_exists(api_key, ParentOrBase::Base, new_base_folder_name, filen_settings)? {
    panic!("Folder {} already exists!", new_base_folder_name)
}

// No "New cloud drive" base folder exists, so create one. Prepare request payload first:
let create_base_folder_payload =
    DirCreateRequestPayload::new(api_key, new_base_folder_name, &last_master_key);

// New folder ID is random, so get hold of it.
let created_base_folder_uuid = create_base_folder_payload.uuid;

// Finally, create "New cloud drive" base folder,
// dir_create_request used for base folder creation.
let create_base_folder_result = dir_create_request(&create_base_folder_payload, filen_settings)?;
if !create_base_folder_result.status {
    panic!(
        "Filen API failed to create base folder: {:?}",
        create_base_folder_result.message
    );
}

// Now lets create "This is a new folder" folder inside freshly
// created "New cloud drive" base folder.
// Good thing we stored base folder ID in `created_base_folder_uuid`, 
// we're going to pass it as a parent.
// 
// Again, check folder for existence first:
let new_folder_name = "This is a new folder";
if folder_exists(
    api_key,
    ParentOrBase::Folder(created_base_folder_uuid),
    new_folder_name,
    filen_settings,
)? {
    panic!("Folder {} already exists!", new_folder_name)
}

// Everything should make sense by now. 
// Usual folders are created with `dir_sub_create_request`:
let folder_payload = DirSubCreateRequestPayload::new(
    api_key,
    new_folder_name,
    created_base_folder_uuid,
    &last_master_key,
);
let create_folder_result = dir_sub_create_request(&folder_payload, filen_settings)?;
if !create_folder_result.status {
    panic!("Filen API failed to create folder: {:?}", create_folder_result.message);
}

共享文件

// To share a file, we need to know UUIDs of the file and its parent.
// Often these come from `download_dir_request`, but assume we have UUIDs already.
// There we have ID of the file we want to share.
let shared_file_uuid = Uuid::parse_str("e132fbc4-22c9-4ee6-af91-f53f8855a65b")?;

// And this is ID of its parent folder. Our shared file is located in a root,
// so its parent is a base folder. So in the context of this tutorial 'cf2a...2cf1' uuid below
// refers to a 'Default' Filen cloud drive.
let shared_file_parent_uuid = Uuid::parse_str("cf2af9a0-6f4e-485d-862c-0459f4662cf1")?;

// Email of the user we want to share the file with.
let receiver_email = "[email protected]";

// Before sharing the file with the user, we need to know said user's RSA public key,
// so we can use it to encode shared file's metadata.
// Public key can be ferched with `user_public_key_get_request`, it's straightforward:
let get_public_key_payload = UserPublicKeyGetRequestPayload {
    email: receiver_email,
};
let user_public_key_get_response = user_public_key_get_request(&get_public_key_payload, filen_settings)?;
if !user_public_key_get_response.status {
    panic!(
        "Filen API failed to get user's public key: {:?}",
        user_public_key_get_response.message
    );
}
let receiver_key_data = user_public_key_get_response.data_ref_or_err()?;
// Now receiver_key_data.public_key contains base64-encoded public key,
// so let's use a helper method to get key bytes:
let receiver_public_key = receiver_key_data.decode_public_key()?;
// Having user's public key, we can start sharing the file.
// However, having just a file UUID won't be sufficient, we need to have file metadata as well.
// So let's fetch files from parent folder and find our target file metadata:
let download_dir_payload = DownloadDirRequestPayload {
    api_key,
    uuid: shared_file_parent_uuid,
};
let dir_content_response = download_dir_request(&download_dir_payload, filen_settings)?;
if !dir_content_response.status {
    panic!(
        "Filen API failed to get folder contents: {:?}",
        dir_content_response.message
    );
}
let contents = dir_content_response.data_ref_or_err()?;
// Find file description:
let shared_file = contents
    .file_with_uuid(&shared_file_uuid)
    .ok_or_else(|| "Parent folder does not contain shared file")?;
// Decrypt file metadata to the file properties.
let shared_file_properties = shared_file.decrypt_file_metadata(&master_keys)?;

// Finally, we are all set to share the file.
// Properly executed share queries are idempotent-ish, so there is no need
// to check if file is shared or not. But if you want, you can see
// with whom the file is shared by calling `user_shared_item_status_request`:
let file_share_status_payload = UserSharedItemStatusRequestPayload {
    api_key,
    uuid: shared_file_uuid,
};
let file_share_status_response =
    user_shared_item_status_request(&file_share_status_payload, filen_settings)?;
// `file_shared_with` below should be empty now, but if file was already shared,
// there would be several user records, often with duplicates.
let file_shared_with = &file_share_status_response.data_ref_or_err()?.users;

// Alright, back to sharing the file.
// When sharing items, Filen expects special parent notation.
// If an item's parent is a base folder, no UUID is needed, pass "none" instead.
// 
// As you can recall, in this tutorial shared file is rooted,
// and `shared_file_parent_uuid` refers to a base folder. So instead of passing
// `ParentOrNone::Folder(shared_file_parent_uuid)`to share_request() call,
// we should pass `ParentOrNone::None`.
// 
// See `user_base_folders_request` query for a way to fetch base folders
// and check if file parent is a base folder or not.
let share_payload = ShareRequestPayload::from_file_properties(
    api_key,
    shared_file_uuid,
    &shared_file_properties,
    ParentOrNone::None,
    receiver_email,
    &receiver_public_key,
)?;
let share_response = share_request(&share_payload, filen_settings)?;
if !share_response.status {
    panic!("Filen API failed to share file: {:?}", share_response.message);
}

共享文件夹

// If you share a folder which contains other folders or files,
// you need to share all those sub-items manually. However, rust_filen has a helper
// method that does it for you. Just take some folder to share and feed it to
// `share_folder_recursively(_async)`:
let folder_to_share = Uuid::parse_str("3a15c71c-762b-43d3-99d6-c484093b9db5")?;
let share_folder_recursively_result = share_folder_recursively(
    api_key,
    folder_to_share,
    receiver_email,
    &receiver_public_key,
    &master_keys,
    &settings,
);

链接文件

// First of all, creating a public link for a file and
// adding a file to existing folder link so it can be seen inside linked folder
// are two different concepts.
// 
// File links are "global": they are always present and not attached to any linked folder,
// but can be disabled or enabled. 
// At any given time only one file link can be enabled, so it is not possible
// to link the same file two times with different settings.
// 
// For this example, let's toggle file's global public link:
// disable file's link if it is already enabled, and vice-versa.
//
// Start by checking current file link status:
let linked_file_uuid = Uuid::parse_str("e132fbc4-22c9-4ee6-af91-f53f8855a65b")?;
let link_status_payload = LinkStatusRequestPayload {
    api_key,
    file_uuid: linked_file_uuid,
};
let link_status_response = link_status_request(&link_status_payload, filen_settings)?;
let link_status_data = link_status_response.data_ref_or_err()?;
// LinkEditRequestPayload::(disabled|enabled) are helper methods
// to create a payload used disable or enable file link.
let toggle_payload = if link_status_data.enabled && link_status_data.uuid.is_some() {
    LinkEditRequestPayload::disabled(api_key, linked_file_uuid, link_status_data.uuid.unwrap())
} else {
    LinkEditRequestPayload::enabled(
        api_key,
        linked_file_uuid,
        DownloadBtnState::Enable,
        Expire::Never,
        None,
        None,
    )
};
// This is it, file link will be enabled or disabled depending on its current status.
let response = link_edit_request(&toggle_payload, filen_settings)?;
if !response.status {
    panic!("Filen API failed to edit file link: {:?}", response.message)
}

链接文件夹

// Again, creating a public link for a folder and
// adding a folder to existing folder link so it can be seen inside linked folder
// are two different concepts.
//
// Unlike file links, folder links are not global and multiple links with different settings
// can be created to the same folder.
//
// If you want to create a folder link for a folder which contains other folders or files,
// you need to link all those sub-items manually. However, rust_filen has a helper
// method that does it for you. Just take some folder to share and feed it to
// `link_folder_recursively(_async)`:
let linked_folder_uuid = Uuid::parse_str("3a15c71c-762b-43d3-99d6-c484093b9db5")?;
link_folder_recursively(
    api_key,
    linked_folder_uuid,
    master_keys,
    &settings,
)?;

到处都是加密的元数据,该怎么办?

迟早您会遇到名为“metadata”的属性和其值为加密字符串的情况。通常,在具有此类属性的struct上存在帮助方法,可以轻松解密它。如果没有,您应该知道“metadata”是Filen加密任何敏感信息的途径,处理它通常有3种方法

  1. 仅供用户使用的用户数据,例如文件属性和文件夹名称。它可以使用rust_filen::crypto::decrypt_metadata_str_any_key与用户主密钥解密,并使用rust_filen::crypto::encrypt_metadata_str与用户的最后一个主密钥加密。

  2. 用户希望公开的数据,如共享或公开链接的文件属性。共享和链接项的元数据处理方式不同。共享项的元数据可以使用目标用户的RSA公钥通过 rust_filen::crypto::encrypt_rsa 进行加密,并使用目标用户的RSA私钥文件通过 rust_filen::crypto::decrypt_rsa 进行解密。这在共享文件时可能会很困惑。您需要记住当前谁是分享者谁是接收者,因此接收者的元数据需要使用接收者的公钥进行加密,以便接收者可以使用其私钥稍后解密元数据。

  3. 链接项的元数据被当作列表项#1中的用户私有数据处理,只是使用链接密钥而不是用户的最后一个主密钥。

检查 FileProperties::encrypt_file_metadata(_rsa)/FileProperties::decrypt_file_metadata(_rsa) 以获取加密和解密文件属性的便捷方法。请注意,文件夹属性仅包含文件夹名称,因此可以使用 LocationNameMetadata::encrypt_name_to_metadata(_rsa)/LocationNameMetadata::decrypt_name_to_metadata(_rsa) 加密和解密文件夹/文件名称。

就是这样,各位!

这是目前示例的结束。要深入了解,您可以查看 https://filen.io/assets/js/fm.min.jshttps://filen.io/assets/js/app.min.js 以获取Filen API的使用模式。

依赖项

~10–23MB
~355K SLoC