This is a library to call Filen.io API from Rust.
Filen.io is a cloud storage provider with an open-source desktop client. My goal was to write a library which calls Filen's API in a meaningful way, and to learn Rust in process. Filen's API was undocumented at time of writing and I tried to get it right by studying the client's sources, so take it all with a grain of salt.
This library is in a usable yet unpolished state. It is possible to make almost every imaginable Filen query: you can login, receive users RSA keys, view user's options and events, perform CRUD on files and folders, list Filen Sync folder/trash folder/recent files contents, download and decrypt files, encrypt and upload files, and share/link files/folders with helpers for recursion.
Some obscure user-specific API queries are unimplemented and documentation is almost non-existent, sorry about that.
If you need to call missing API query, you can do so with rust_filen::queries::query_filen_api("/v1/some/uimplemented/api/path", any_serde_serializable_payload, filen_settings)
.
By default, all queries are synchronous and performed with ureq.
If you want, you can enable async versions of every API query and a way to retry them, RetrySettings::call_async
.
To do so, set features = ["async"]
for this library in your Cargo.toml
.
As a result, reqwest will be used instead of ureq.
If you're interested, that's how it all looks. Start by importing all we need for exemplary purposes:
rust
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 secstr::SecUtf8;
While we are on the topic of imports, rust_filen
re-exports all third-party crates used in public functions.
Namely rust_filen::ureq
, rust_filen::reqwest
, rust_filen::fure
, rust_filen::retry
, rust_filen::secstr
and rust_filen::uuid
.
Anyway, let's login first.
Actually, before logging in, we have to know how to do so.
``rust
// 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 useremail = SecUtf8::from("registered.user@email.com");
let userpassword = SecUtf8::from("user.password.in.plaintext");
// Filen actually uses XXXXXX when 2FA is absent.
let usertwofactorkey = SecUtf8::from("XXXXXX");
let settings = STANDARDSETTINGSBUNDLE.clone();
let filensettings = settings.filen; // Provides Filen server URLs.
let authinforequestpayload = auth::AuthInfoRequestPayload { email: useremail.clone(), twofactorkey: usertwofactorkey.clone(), }; let authinforesponse = auth::authinforequest(&authinforequestpayload, &filensettings)?; if !authinforesponse.status { panic!("Filen API failed to return auth info: {:?}", authinforesponse.message); } let authinforesponsedata = authinforesponse.dataorerr()?;
// filen_password_with_master_key
helper calculates Filen password for us,
// depending on returned authinforesponsedata.
let filenpasswordandmkey = authinforesponsedata
.filenpasswordwithmasterkey(&user_password)?;
```
```rust // 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 loginrequestpayload = auth::LoginRequestPayload { email: useremail.clone(), password: filenpasswordandmkey.sentpassword.clone(), twofactorkey: usertwofactorkey.clone(), authversion: authinforesponsedata.authversion, }; let loginresponse = auth::loginrequest(&loginrequestpayload, &filensettings)?; if !loginresponse.status { panic!("Filen API failed to login: {:?}", authinforesponse.message); }
// Login confirmed, now you can take API key and user's master key from the LoginResponseData // and go perform some API calls! let loginresponsedata = loginresponse.dataor_err()?;
// Api key is needed for almost every call to Filen API, so it's a must have. let apikey = loginresponsedata.apikey;
// Last master key is used for encryption of user's private data. let lastmasterkey = filenpasswordandmkey.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 masterkeys = loginresponsedata.decryptmasterkeysmetadata(&lastmasterkey)?; ```
```rust // Let's start by finding user's default folder: let userdirrequestpayload = UserDirsRequestPayload { apikey: apikey.clone(), }; let userdirsresponse = userdirsrequest(&userdirrequestpayload, &filensettings)?; if !userdirsresponse.status { panic!( "Filen API failed to provide user dirs: {:?}", userdirs_response.message ); }
// This is just a convenience helper, // you can iterate folders in userdirsresponse.data yourself: let defaultfolderdata = userdirsresponse.finddefaultfolder().unwrap(); ```
rust
// Alright, we have our default folder, let's check out its contents.
let download_dir_request_payload = DownloadDirRequestPayload {
api_key: api_key.clone(),
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_or_err()?;
let default_folder_files_and_properties =
download_dir_response_data.decrypt_all_file_properties(&master_keys)?;
```rust // Let's say user has a file 'some file.png' located in the default folder. // Let's find and download it: let (somefiledata, somefileproperties) = defaultfolderfilesandproperties .iter() .find(|(data, properties)| data.parent == defaultfolderdata.uuid && properties.name.eqignoreasciicase("some file.png")) .unwrap(); let filekey = somefileproperties.key.clone();
// Let's store file in-memory via writer over vec: let mut file_writer = std::io::BufWriter::new(Vec::new());
// STANDARDSETTINGSBUNDLE earlier contained retry
field with STANDARDRETRIES,
// 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 syncfiledownloadresult = downloadanddecryptfilefromdataandkey(
somefiledata,
&filekey,
&mut file_writer,
&settings,
);
// And now we have downloaded and decrypted bytes in memory. let filebytes = syncfiledownloadresult.map(|| filewriter.into_inner().unwrap())?; ```
```rust
// First let's define the file we will upload:
let filepath =
// 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 parentfolderid = "cf2af9a0-6f4e-485d-862c-0459f4662cf1";
// Prepare file properties like file size and mime type for Filen. // 'somefile.txt' is specified again here, because you can change uploaded file name if you want: let fileproperties = FileProperties::fromnameandlocalpath("somefile.txt", &filepath)?;
// 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 encryptanduploadfile() can read file bytes later: let mut filereader = std::io::BufReader::new(std::fs::File::open(filepath.tostr()?) .expect("Unable to open file"));
// We're all done: let uploadresult = encryptanduploadfile( &apikey, parentfolderid, &fileproperties, fileversion, &lastmasterkey, &mut filereader, &settings, ); ```
```rust
// 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 folderexists(
apikey: &SecUtf8,
// ParentOrBase defines whether to seek foldername among base folders or
// in the given parent folder.
parent: ParentOrBase,
// Plain-text folder name.
filensettings: &FilenSettings,
) -> Result
// Alright, now we have everything we need to create some folders. let newbasefolder_name = "New cloud drive";
// Check that base folder with name "New cloud drive" does not exist already. if folderexists(&apikey, ParentOrBase::Base, newbasefoldername, &filensettings)? { panic!("Folder {} already exists!", newbasefolder_name) }
// No "New cloud drive" base folder exists, so create one. Prepare request payload first: let createbasefolderpayload = DirCreateRequestPayload::new(apikey.clone(), newbasefoldername, &lastmaster_key);
// New folder ID is random, so get hold of it. let createdbasefolderuuid = createbasefolderpayload.uuid;
// Finally, create "New cloud drive" base folder, // dircreaterequest used for base folder creation. let createbasefolderresult = dircreaterequest(&createbasefolderpayload, &filensettings)?; if !createbasefolderresult.status { panic!( "Filen API failed to create base folder: {:?}", createbasefolder_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 newfoldername = "This is a new folder";
if folderexists(
&apikey,
ParentKind::Folder(createdbasefolderuuid.clone()),
newfoldername,
&filensettings,
)? {
panic!("Folder {} already exists!", newfoldername)
}
// Everything should make sense by now.
// Usual folders are created with dir_sub_create_request
:
let folderpayload = DirSubCreateRequestPayload::new(
apikey.clone(),
newfoldername,
createdbasefolderuuid,
&lastmasterkey,
);
let createfolderresult = dirsubcreaterequest(&folderpayload, &filensettings)?;
if !createfolderresult.status {
panic!("Filen API failed to create folder: {:?}", createfolderresult.message);
}
```
``rust
// To share a file, we need to know UUIDs of the file and its parent.
// Often these come from
downloaddirrequest`, but assume we have UUIDs already.
// There we have ID of the file we want to share.
let sharedfileuuid = 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 sharedfileparentuuid = Uuid::parsestr("cf2af9a0-6f4e-485d-862c-0459f4662cf1")?;
// Email of the user we want to share the file with. let receiver_email = "file.receiver@test.com";
// 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 getpublickeypayload = UserPublicKeyGetRequestPayload {
email: receiveremail.toowned(),
};
let userpublickeygetresponse = userpublickeygetrequest(&getpublickeypayload, &filensettings)?;
if !userpublickeygetresponse.status {
panic!(
"Filen API failed to get user's public key: {:?}",
userpublickeygetresponse.message
);
}
let receiverkeydata = userpublickeygetresponse.dataorerr()?;
// Now receiverkeydata.publickey contains base64-encoded public key,
// so let's use a helper method to get key bytes:
let receiverpublickey = receiverkeydata.decodepublickey()?;
// 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 downloaddirpayload = DownloadDirRequestPayload {
apikey: apikey.clone(),
uuid: sharedfileparentuuid,
};
let dircontentresponse = downloaddirrequest(&downloaddirpayload, &filensettings)?;
if !dircontentresponse.status {
panic!(
"Filen API failed to get folder contents: {:?}",
dircontentresponse.message
);
}
let contents = dircontentresponse.dataorerr()?;
// Find file description:
let sharedfile = contents
.filewithuuid(&sharedfileuuid)
.okorelse(|| "Parent folder does not contain shared file")?;
// Decrypt file metadata to the file properties.
let sharedfileproperties = sharedfile.decryptfilemetadata(&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 filesharestatuspayload = UserSharedItemStatusRequestPayload {
apikey: apikey.clone(),
uuid: sharedfileuuid.clone(),
};
let filesharestatusresponse =
usershareditemstatusrequest(&filesharestatuspayload, &filensettings)?;
// file_shared_with
below should be empty now, but if file was already shared,
// there would be several user records, often with duplicates.
let filesharedwith = filesharestatusresponse.dataor_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 sharerequest() 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 sharepayload = ShareRequestPayload::fromfileproperties(
apikey.clone(),
sharedfileuuid,
&sharedfileproperties,
ParentOrNone::None,
receiveremail.toowned(),
&receiverpublickey,
)?;
let shareresponse = sharerequest(&sharepayload, &filensettings)?;
if !shareresponse.status {
panic!("Filen API failed to share file: {:?}", share_response.message);
}
```
rust
// 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,
)
rust
// 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: api_key.clone(),
file_uuid: linked_file_uuid.clone(),
};
let link_status_response = link_status_request(&link_status_payload, &filen_settings)?;
let link_status_data = link_status_response.data_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.clone(), linked_file_uuid, link_status_data.uuid.unwrap())
} else {
LinkEditRequestPayload::enabled(
api_key.clone(),
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)
}
rust
// 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,
)?
Sooner or later you will encounter properties with "metadata" in their names and encrypted strings for their values. Quite often there are helper methods on structs with such properties which can be used to decrypt it easily. If not, you should know that "metadata" is a Filen way to encrypt any sensitive info, and there are 3 general ways of dealing with it:
rust_filen::crypto::decrypt_metadata_str
/ rust_filen::crypto::encrypt_metadata_str
using user's last master key.rust_filen::crypto::encrypt_rsa
using target user RSA public key, and decrypted by
rust_filen::crypto::decrypt_rsa
using target user RSA private key file. This can be pretty confusing when sharing files.
You will need to keep in mind who is currently the sharer and who is the receiver,
so receiver's metadata needs to be encrypted with receiver's public key, so it can decipher it later with its private key.Check FileProperties::encrypt_file_metadata(_rsa)
/FileProperties::decrypt_file_metadata(_rsa)
for a convenient way to encrypt&decrypt file properties. Note that folder properties consist of just folder name, so
LocationNameMetadata::encrypt_name_to_metadata(_rsa)
/LocationNameMetadata::decrypt_name_to_metadata(_rsa)
can be used to encrypt&decrypt folder/file names.
This is it for examples, at least for now. To dig deeper, you might want to check out https://filen.io/assets/js/fm.min.js and https://filen.io/assets/js/app.min.js for Filen API usage patterns.