diff --git a/Cargo.lock b/Cargo.lock index 9bc6da68..2573ae96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1540,7 +1540,7 @@ dependencies = [ [[package]] name = "wasm-wnfs" -version = "0.1.1" +version = "0.1.6" dependencies = [ "anyhow", "async-trait", @@ -1665,7 +1665,7 @@ checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9" [[package]] name = "wnfs" -version = "0.1.1" +version = "0.1.6" dependencies = [ "anyhow", "async-recursion", @@ -1675,6 +1675,7 @@ dependencies = [ "chrono", "field_names", "futures", + "futures-util", "hashbrown 0.12.0", "libipld", "multihash", diff --git a/README.md b/README.md index 231f4de6..f7ecca84 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,103 @@ ## -This project will implement a pure rust crate for creating and manipulating IPLD graphs that encode WNFS. -Its goal is to be as dependency-less as possible in order to be easily compiled to WebAssembly to be used in the browsers or other environments. +This crate is a Rust implementation of the primitives for creating and manipulating IPLD graphs that encode WNFS. + +A goal of the project is to be easily compiled to WebAssembly to be used in the browsers or other environments. + +## Outline + +- [Usage](#usage) +- [Building the Project](#building-the-project) +- [Testing the Project](#testing-the-project) + +## Usage + +Creating a new public directory. + +```rust +use wnfs::{PublicDirectory, Id}; + +use async_std::main; +use chrono::Utc; + +#[async_std::main] +async fn main() { + let dir = PublicDirectory::new(Utc::now()); + println!("id = {}", dir.get_id()); +} +``` + +The in-memory files and directories you create with `wnfs` will need to be sealed and stored somewhere. For that, a type that implements the BlockStore trait like [this one](https://github.com/WebNativeFileSystem/rs-wnfs/blob/8bb0fbb457051295f1ed4a4707dc230c04612658/crates/fs/common/blockstore.rs#L42-L62) can be used. + +```rust +use wnfs::{MemoryBlockStore, PublicDirectory, OpResult, ipld::Cid}; + +use async_std::main; +use chrono::Utc; + +use std::rc::Rc; +// ... +``` + +The WNFS API is immutable, therefore, we need to keep track of the updated root directory after every change. + +Each fs operation returns a possibly updated root directory that subsequent changes can be applied on. + +```rust +// ... +#[async_std::main] +async fn main() { + let time = Utc::now(); + let dir = Rc::new(PublicDirectory::new(time)); + let store = MemoryBlockStore::default(); + + // Create a /pictures/cats directory. + let OpResult { root_dir, .. } = dir + .mkdir(&["pictures".into(), "cats".into()], time, &store) + .await + .unwrap(); + + // Get a sample CIDv1. + let cid = Cid::default(); + + // Add a file to /pictures/cats. + let OpResult { root_dir, .. } = root_dir + .write( + &["pictures".into(), "cats".into(), "tabby.png".into()], + cid, + time, + &store, + ) + .await + .unwrap(); + + // Create and add a file to /pictures/dogs directory. + let OpResult { root_dir, .. } = root_dir + .write( + &["pictures".into(), "dogs".into(), "billie.jpeg".into()], + cid, + time, + &store, + ) + .await + .unwrap(); + + // Delete /pictures/cats directory. + let OpResult { root_dir, .. } = root_dir + .rm(&["pictures".into(), "cats".into()], &store) + .await + .unwrap(); + + // List all files in /pictures directory. + let OpResult { result, .. } = root_dir + .ls(&["pictures".into()], &store) + .await + .unwrap(); + + println!("Files in /pictures: {:#?}", result); +} +``` ## Building the Project @@ -44,7 +139,7 @@ Its goal is to be as dependency-less as possible in order to be easily compiled - **The WebAssembly Toolchain** - If yous are interested in compiling the project for WebAssembly, you can follow the instructions below. + If you are interested in compiling the project for WebAssembly, you can follow the instructions below.
Read more @@ -85,9 +180,9 @@ Its goal is to be as dependency-less as possible in order to be easily compiled
-- **The _wnfs_ Helper Script** +- **The _rs-wnfs_ Command** - If you are on a Unix platform, you can optionally install the `wnfs` script. + You can optionally set up the `rs-wnfs` script.
Read more @@ -95,13 +190,13 @@ Its goal is to be as dependency-less as possible in order to be easily compiled - Install it using the following command: ```bash - sh script/wnfs.sh setup + sh script/rs-wnfs.sh setup ``` - - This lets you run the `wnfs.sh` script with just the `wnfs` command. + - This lets you run the `rs-wnfs.sh` script as a command. ```bash - wnfs help + rs-wnfs help ```
@@ -122,8 +217,16 @@ Its goal is to be as dependency-less as possible in order to be easily compiled - Build the project + Check [REQUIREMENTS](#requirements) on how to set up the `rs-wnfs` command. + + ```bash + rs-wnfs build --all + ``` + +- You can also build for specific crates + ```bash - sh scripts/wnfs.sh build + rs-wnfs build --wasm ``` ## Testing the Project @@ -131,11 +234,11 @@ Its goal is to be as dependency-less as possible in order to be easily compiled - Run all tests ```bash - sh scripts/wnfs.sh test + rs-wnfs test --all ``` - Show code coverage ```bash - sh scripts/wnfs.sh coverage + rs-wnfs coverage ``` diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml index 8d08e03e..7a529647 100644 --- a/crates/fs/Cargo.toml +++ b/crates/fs/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "wnfs" -version = "0.1.1" +version = "0.1.6" description = "WebNative filesystem core implementation" keywords = ["wnfs", "webnative", "ipfs", "decentralisation"] categories = [ @@ -9,10 +9,10 @@ categories = [ "web-programming", "wasm", ] -license-file = "../../LICENSE" +license = "Apache-2.0" readme = "README.md" edition = "2021" -repository = "https://github.com/fission-suite/rs-wnfs" +repository = "https://github.com/WebNativeFileSystem/rs-wnfs/tree/main/crates/fs" homepage = "https://fission.codes" authors = ["The Fission Authors"] @@ -29,7 +29,7 @@ async-recursion = "1.0.0" field_names = "0.2.0" futures = "0.3.21" async-stream = "0.3.3" - +futures-util = "0.3.21" [lib] path = "lib.rs" diff --git a/crates/fs/LICENSE b/crates/fs/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/crates/fs/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/crates/fs/README.md b/crates/fs/README.md index 8b145749..a5f0c2e6 100644 --- a/crates/fs/README.md +++ b/crates/fs/README.md @@ -1,4 +1,131 @@ -## The FileSystem +
+ + Fission Logo + + +

WebNative FileSystem (WNFS)

+ +

+ + Concurrency Docs + + + Code Coverage + + + Build Status + + + License + + + Concurrency Docs + + + Discord + +

+
+ +## + +This crate is a Rust implementation of the primitives for creating and manipulating IPLD graphs that encode WNFS. + +A goal of the project is to be easily compiled to WebAssembly to be used in the browsers or other environments. + +## Outline + +- [Usage](#usage) +- [Building the Project](#building-the-project) +- [Testing the Project](#testing-the-project) + +## Usage + +Creating a new public directory. + +```rust +use wnfs::{PublicDirectory, Id}; + +use async_std::main; +use chrono::Utc; + +#[async_std::main] +async fn main() { + let dir = PublicDirectory::new(Utc::now()); + println!("id = {}", dir.get_id()); +} +``` + +The in-memory files and directories you create with `wnfs` will need to be sealed and stored somewhere. For that, a type that implements the BlockStore trait like [this one](https://github.com/WebNativeFileSystem/rs-wnfs/blob/8bb0fbb457051295f1ed4a4707dc230c04612658/crates/fs/common/blockstore.rs#L42-L62) can be used. + +```rust +use wnfs::{MemoryBlockStore, PublicDirectory, OpResult, ipld::Cid}; + +use async_std::main; +use chrono::Utc; + +use std::rc::Rc; +// ... +``` + +The WNFS API is immutable, therefore, we need to keep track of the updated root directory after every change. + +Each fs operation returns a possibly updated root directory that subsequent changes can be applied on. + +```rust +// ... +#[async_std::main] +async fn main() { + let time = Utc::now(); + let dir = Rc::new(PublicDirectory::new(time)); + let store = MemoryBlockStore::default(); + + // Create a /pictures/cats directory. + let OpResult { root_dir, .. } = dir + .mkdir(&["pictures".into(), "cats".into()], time, &store) + .await + .unwrap(); + + // Get a sample CIDv1. + let cid = Cid::default(); + + // Add a file to /pictures/cats. + let OpResult { root_dir, .. } = root_dir + .write( + &["pictures".into(), "cats".into(), "tabby.png".into()], + cid, + time, + &store, + ) + .await + .unwrap(); + + // Create and add a file to /pictures/dogs directory. + let OpResult { root_dir, .. } = root_dir + .write( + &["pictures".into(), "dogs".into(), "billie.jpeg".into()], + cid, + time, + &store, + ) + .await + .unwrap(); + + // Delete /pictures/cats directory. + let OpResult { root_dir, .. } = root_dir + .rm(&["pictures".into(), "cats".into()], &store) + .await + .unwrap(); + + // List all files in /pictures directory. + let OpResult { result, .. } = root_dir + .ls(&["pictures".into()], &store) + .await + .unwrap(); + + println!("Files in /pictures: {:#?}", result); +} +``` ## Building the Project diff --git a/crates/fs/lib.rs b/crates/fs/lib.rs index 718caec8..879b04a5 100644 --- a/crates/fs/lib.rs +++ b/crates/fs/lib.rs @@ -2,6 +2,7 @@ mod common; pub mod public; pub use common::*; +pub use public::*; //-------------------------------------------------------------------------------------------------- // Re-exports diff --git a/crates/fs/public/directory.rs b/crates/fs/public/directory.rs index 03c1cc60..7df59b4c 100644 --- a/crates/fs/public/directory.rs +++ b/crates/fs/public/directory.rs @@ -26,7 +26,18 @@ use super::{Id, Link, PublicFile, PublicNode}; // Type Definitions //-------------------------------------------------------------------------------------------------- -/// A directory in a WNFS public file system.`` +/// A directory in a WNFS public file system. +/// +/// # Examples +/// +/// ``` +/// use wnfs::{PublicDirectory, Id}; +/// use chrono::Utc; +/// +/// let dir = PublicDirectory::new(Utc::now()); +/// +/// println!("id = {}", dir.get_id()); +/// ``` #[derive(Debug, Clone, PartialEq, Eq, FieldNames)] pub struct PublicDirectory { pub(crate) metadata: Metadata, @@ -34,7 +45,7 @@ pub struct PublicDirectory { pub(crate) previous: Option, } -/// Represents a directory that has possibly diverged. It is the result of operating on a directory. +/// The result of an operation applied to a directory. #[derive(Debug, Clone, PartialEq, Eq)] pub struct OpResult { // The root directory. @@ -44,22 +55,81 @@ pub struct OpResult { } /// Represents the directory nodes along a path. +/// +/// # Examples +/// +/// ``` +/// use wnfs::{PublicDirectory, PathNodes}; +/// use std::rc::Rc; +/// use chrono::Utc; +/// +/// let nodes = PathNodes::new( +/// Utc::now(), +/// &["movies".into(), "anime".into()], +/// Rc::new(PublicDirectory::new(Utc::now())), +/// ); +/// +/// println!("path nodes = {:?}", nodes); +/// ``` #[derive(Debug, Clone, PartialEq, Eq)] pub struct PathNodes { - path: Vec<(Rc, String)>, - tail: Rc, + pub path: Vec<(Rc, String)>, + pub tail: Rc, } /// The kinds of outcome from getting a `PathNodes`. -pub enum GetNodePathResult { +/// +/// # Examples +/// +/// ``` +/// use wnfs::{PublicDirectory, MemoryBlockStore, OpResult}; +/// use std::rc::Rc; +/// use chrono::Utc; +/// +/// #[async_std::main] +/// async fn main() { +/// let time = Utc::now(); +/// let dir = Rc::new(PublicDirectory::new(time)); +/// let store = MemoryBlockStore::default(); +/// +/// let OpResult { root_dir, result } = dir +/// .ls(&[], &store) +/// .await +/// .unwrap(); +/// +/// println!("ls = {:?}", result); +/// } +/// ``` +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PathNodesResult { Complete(PathNodes), MissingLink(PathNodes, String), NotADirectory(PathNodes, String), } +//-------------------------------------------------------------------------------------------------- +// Implementations +//-------------------------------------------------------------------------------------------------- + impl PathNodes { /// Creates a new `PathNodes` that is not based on an existing file tree. - fn new(time: DateTime, path_segments: &[String], tail: Rc) -> Self { + /// + /// # Examples + /// + /// ``` + /// use wnfs::{PublicDirectory, PathNodes}; + /// use std::rc::Rc; + /// use chrono::Utc; + /// + /// let nodes = PathNodes::new( + /// Utc::now(), + /// &["movies".into(), "anime".into()], + /// Rc::new(PublicDirectory::new(Utc::now())), + /// ); + /// + /// println!("path nodes = {:?}", nodes); + /// ``` + pub fn new(time: DateTime, path_segments: &[String], tail: Rc) -> Self { let path: Vec<(Rc, String)> = path_segments .iter() .map(|segment| (Rc::new(PublicDirectory::new(time)), segment.clone())) @@ -69,29 +139,96 @@ impl PathNodes { } /// Constructs a diverged path nodes by fixing up links in a `PathNodes` and returning the resulting root node. - fn reconstruct(self) -> Rc { + /// + /// # Examples + /// + /// ``` + /// use wnfs::{PublicDirectory, PathNodes}; + /// use std::rc::Rc; + /// use chrono::Utc; + /// + /// let nodes = PathNodes::new( + /// Utc::now(), + /// &["movies".into(), "anime".into()], + /// Rc::new(PublicDirectory::new(Utc::now())), + /// ); + /// + /// let new_root = nodes.reconstruct(); + /// + /// println!("new_root = {:?}", new_root); + /// ``` + pub fn reconstruct(self) -> Rc { if self.path.is_empty() { return self.tail; } - let mut working_node = self.tail; + let mut working_dir = self.tail; for (dir, segment) in self.path.iter().rev() { let mut dir = (**dir).clone(); - let link = Link::Node(PublicNode::Dir(working_node)); + let link = Link::with_dir(working_dir); dir.userland.insert(segment.clone(), link); - working_node = Rc::new(dir); + working_dir = Rc::new(dir); } - working_node + working_dir } -} -//-------------------------------------------------------------------------------------------------- -// Implementations -//-------------------------------------------------------------------------------------------------- + /// Returns the length of the path nodes. + /// + /// # Examples + /// + /// ``` + /// use wnfs::{PublicDirectory, PathNodes}; + /// use std::rc::Rc; + /// use chrono::Utc; + /// + /// let nodes = PathNodes::new( + /// Utc::now(), + /// &["movies".into(), "anime".into()], + /// Rc::new(PublicDirectory::new(Utc::now())), + /// ); + /// + /// assert_eq!(nodes.len(), 2); + /// ``` + pub fn len(&self) -> usize { + self.path.len() + } + + /// Checks if the path nodes are empty. + /// + /// # Examples + /// + /// ``` + /// use wnfs::{PublicDirectory, PathNodes}; + /// use std::rc::Rc; + /// use chrono::Utc; + /// + /// let nodes = PathNodes::new( + /// Utc::now(), + /// &["movies".into(), "anime".into()], + /// Rc::new(PublicDirectory::new(Utc::now())), + /// ); + /// + /// assert!(!nodes.is_empty()); + /// ``` + pub fn is_empty(&self) -> bool { + self.path.is_empty() + } +} impl PublicDirectory { - /// Creates a new directory using the given metadata. + /// Creates a new directory with provided time. + /// + /// # Examples + /// + /// ``` + /// use wnfs::{PublicDirectory, Id}; + /// use chrono::Utc; + /// + /// let dir = PublicDirectory::new(Utc::now()); + /// + /// println!("id = {}", dir.get_id()); + /// ``` pub fn new(time: DateTime) -> Self { Self { metadata: Metadata::new(time, UnixFsNodeKind::Dir), @@ -105,14 +242,15 @@ impl PublicDirectory { self.previous } - /// Gets the directory nodes along a path and allows for cases where the path is nopt fully constructed. - pub async fn get_node_path( + /// Gets the directory nodes along specified path. + /// + /// Supports cases where the entire path does not exist. + pub(crate) async fn get_path_nodes( self: Rc, path_segments: &[String], store: &B, - ) -> Result { - use GetNodePathResult::*; - + ) -> Result { + use PathNodesResult::*; let mut working_node = self; let mut path_nodes = Vec::with_capacity(path_segments.len()); @@ -148,16 +286,17 @@ impl PublicDirectory { } /// Gets the directory nodes along a path and also supports creating missing intermediate directories. - pub async fn get_node_path_with_mkdir( + pub(crate) async fn get_path_nodes_or_create( self: Rc, path_segments: &[String], time: DateTime, store: &B, ) -> Result { - match self.get_node_path(path_segments, store).await? { - GetNodePathResult::Complete(path_nodes) => Ok(path_nodes), - GetNodePathResult::NotADirectory(_, _) => error(FsError::InvalidPath), - GetNodePathResult::MissingLink(path_so_far, missing_link) => { + use PathNodesResult::*; + match self.get_path_nodes(path_segments, store).await? { + Complete(path_nodes) => Ok(path_nodes), + NotADirectory(_, _) => error(FsError::InvalidPath), + MissingLink(path_so_far, missing_link) => { let missing_path = path_segments.split_at(path_so_far.path.len() + 1).1; let missing_path_nodes = PathNodes::new(time, missing_path, Rc::new(PublicDirectory::new(time))); @@ -176,25 +315,53 @@ impl PublicDirectory { } /// Follows a path and fetches the node at the end of the path. + /// + /// # Examples + /// + /// ``` + /// use wnfs::{PublicDirectory, MemoryBlockStore, OpResult}; + /// use std::rc::Rc; + /// use chrono::Utc; + /// + /// #[async_std::main] + /// async fn main() { + /// let time = Utc::now(); + /// let dir = Rc::new(PublicDirectory::new(time)); + /// let store = MemoryBlockStore::default(); + /// + /// let OpResult { root_dir, .. } = Rc::new(PublicDirectory::new(Utc::now())) + /// .mkdir(&["pictures".into(), "cats".into()], Utc::now(), &store) + /// .await + /// .unwrap(); + /// + /// let OpResult { root_dir, result } = root_dir + /// .get_node(&["pictures".into()], &store) + /// .await + /// .unwrap(); + /// + /// assert!(result.is_some()); + /// } + /// ``` pub async fn get_node( self: Rc, path_segments: &[String], store: &B, ) -> Result>> { + use PathNodesResult::*; let root_dir = Rc::clone(&self); Ok(match path_segments.split_last() { Some((path_segment, parent_path)) => { - match self.get_node_path(parent_path, store).await? { - GetNodePathResult::Complete(parent_path_nodes) => OpResult { + match self.get_path_nodes(parent_path, store).await? { + Complete(parent_path_nodes) => OpResult { root_dir, result: parent_path_nodes .tail .lookup_node(path_segment, store) .await?, }, - GetNodePathResult::MissingLink(_, _) => bail!(FsError::NotFound), - GetNodePathResult::NotADirectory(_, _) => bail!(FsError::NotFound), + MissingLink(_, _) => bail!(FsError::NotFound), + NotADirectory(_, _) => bail!(FsError::NotFound), } } None => OpResult { @@ -205,6 +372,28 @@ impl PublicDirectory { } /// Looks up a node by its path name in the current directory. + /// + /// # Examples + /// + /// ``` + /// use wnfs::{PublicDirectory, Id, MemoryBlockStore, OpResult}; + /// use std::rc::Rc; + /// use chrono::Utc; + /// + /// #[async_std::main] + /// async fn main() { + /// let mut store = MemoryBlockStore::default(); + /// + /// let OpResult { root_dir, .. } = Rc::new(PublicDirectory::new(Utc::now())) + /// .mkdir(&["pictures".into(), "cats".into()], Utc::now(), &store) + /// .await + /// .unwrap(); + /// + /// let node = root_dir.lookup_node("pictures", &store).await.unwrap(); + /// + /// assert!(node.is_some()); + /// } + /// ``` pub async fn lookup_node( &self, path_segment: &str, @@ -216,16 +405,65 @@ impl PublicDirectory { }) } + #[async_recursion(?Send)] /// Stores directory in provided block store. /// /// This function can be recursive if the directory contains other directories. - #[async_recursion(?Send)] + /// + /// # Examples + /// + /// ``` + /// use wnfs::{PublicDirectory, Id, MemoryBlockStore}; + /// use chrono::Utc; + /// + /// #[async_std::main] + /// async fn main() { + /// let mut store = MemoryBlockStore::default(); + /// let dir = PublicDirectory::new(Utc::now()); + /// + /// dir.store(&mut store).await.unwrap(); + /// } + /// ``` pub async fn store(&self, store: &mut B) -> Result { let bytes = self.encode(store).await?; store.put_block(bytes, IpldCodec::DagCbor).await } /// Reads specified file content from the directory. + /// + /// # Examples + /// + /// ``` + /// use wnfs::{PublicDirectory, MemoryBlockStore, OpResult}; + /// use libipld::cid::Cid; + /// use std::rc::Rc; + /// use chrono::Utc; + /// + /// #[async_std::main] + /// async fn main() { + /// let time = Utc::now(); + /// let dir = Rc::new(PublicDirectory::new(time)); + /// let mut store = MemoryBlockStore::default(); + /// let cid = Cid::default(); + /// + /// let OpResult { root_dir, .. } = Rc::new(PublicDirectory::new(Utc::now())) + /// .write( + /// &["pictures".into(), "cats".into(), "tabby.png".into()], + /// cid, + /// Utc::now(), + /// &store + /// ) + /// .await + /// .unwrap(); + /// + /// let OpResult { root_dir, result } = root_dir + /// .read(&["pictures".into(), "cats".into(), "tabby.png".into()], &mut store) + /// .await + /// .unwrap(); + /// + /// assert_eq!(result, cid); + /// } + /// ``` pub async fn read( self: Rc, path_segments: &[String], @@ -234,8 +472,8 @@ impl PublicDirectory { let root_dir = Rc::clone(&self); let (path, filename) = utils::split_last(path_segments)?; - match self.get_node_path(path, store).await? { - GetNodePathResult::Complete(node_path) => { + match self.get_path_nodes(path, store).await? { + PathNodesResult::Complete(node_path) => { match node_path.tail.lookup_node(filename, store).await? { Some(PublicNode::File(file)) => Ok(OpResult { root_dir, @@ -250,6 +488,32 @@ impl PublicDirectory { } /// Writes a file to the directory. + /// + /// # Examples + /// + /// ``` + /// use wnfs::{PublicDirectory, MemoryBlockStore, OpResult}; + /// use libipld::cid::Cid; + /// use std::rc::Rc; + /// use chrono::Utc; + /// + /// #[async_std::main] + /// async fn main() { + /// let time = Utc::now(); + /// let dir = Rc::new(PublicDirectory::new(time)); + /// let store = MemoryBlockStore::default(); + /// + /// let OpResult { root_dir, .. } = Rc::new(PublicDirectory::new(Utc::now())) + /// .write( + /// &["pictures".into(), "cats".into(), "tabby.png".into()], + /// Cid::default(), + /// Utc::now(), + /// &store + /// ) + /// .await + /// .unwrap(); + /// } + /// ``` pub async fn write( self: Rc, path_segments: &[String], @@ -261,7 +525,7 @@ impl PublicDirectory { // This will create directories if they don't exist yet let mut directory_path_nodes = self - .get_node_path_with_mkdir(directory_path, time, store) + .get_path_nodes_or_create(directory_path, time, store) .await?; let mut directory = (*directory_path_nodes.tail).clone(); @@ -279,10 +543,9 @@ impl PublicDirectory { }; // insert the file into its parent directory - directory.userland.insert( - filename.to_string(), - Link::Node(PublicNode::File(Rc::new(file))), - ); + directory + .userland + .insert(filename.to_string(), Link::with_file(Rc::new(file))); directory_path_nodes.tail = Rc::new(directory); // reconstruct the file path @@ -294,6 +557,24 @@ impl PublicDirectory { /// Creates a new directory at the specified path. /// + /// # Examples + /// + /// ``` + /// use wnfs::{PublicDirectory, Id, MemoryBlockStore, OpResult}; + /// use std::rc::Rc; + /// use chrono::Utc; + /// + /// #[async_std::main] + /// async fn main() { + /// let mut store = MemoryBlockStore::default(); + /// + /// let OpResult { root_dir, .. } = Rc::new(PublicDirectory::new(Utc::now())) + /// .mkdir(&["pictures".into(), "cats".into()], Utc::now(), &store) + /// .await + /// .unwrap(); + /// } + /// ``` + /// /// This method acts like `mkdir -p` in Unix because it creates intermediate directories if they do not exist. pub async fn mkdir( self: Rc, @@ -301,27 +582,61 @@ impl PublicDirectory { time: DateTime, store: &B, ) -> Result> { - let node_path_with_dirs = self - .get_node_path_with_mkdir(path_segments, time, store) + let path_nodes = self + .get_path_nodes_or_create(path_segments, time, store) .await?; Ok(OpResult { - root_dir: node_path_with_dirs.reconstruct(), + root_dir: path_nodes.reconstruct(), result: (), }) } /// Returns the name and metadata of the direct children of a directory. + /// + /// # Examples + /// + /// ``` + /// use wnfs::{PublicDirectory, MemoryBlockStore, OpResult}; + /// use libipld::cid::Cid; + /// use std::rc::Rc; + /// use chrono::Utc; + /// + /// #[async_std::main] + /// async fn main() { + /// let time = Utc::now(); + /// let dir = Rc::new(PublicDirectory::new(time)); + /// let store = MemoryBlockStore::default(); + /// + /// let OpResult { root_dir, .. } = Rc::new(PublicDirectory::new(Utc::now())) + /// .write( + /// &["pictures".into(), "cats".into(), "tabby.png".into()], + /// Cid::default(), + /// Utc::now(), + /// &store + /// ) + /// .await + /// .unwrap(); + /// + /// let OpResult { root_dir, result } = root_dir + /// .ls(&["pictures".into(), "cats".into()], &store) + /// .await + /// .unwrap(); + /// + /// assert_eq!(result.len(), 1); + /// assert_eq!(result[0].0, "tabby.png"); + /// } + /// ``` pub async fn ls( self: Rc, path_segments: &[String], store: &B, ) -> Result>> { let root_dir = Rc::clone(&self); - match self.get_node_path(path_segments, store).await? { - GetNodePathResult::Complete(node_path) => { + match self.get_path_nodes(path_segments, store).await? { + PathNodesResult::Complete(path_nodes) => { let mut result = vec![]; - for (name, link) in node_path.tail.userland.iter() { + for (name, link) in path_nodes.tail.userland.iter() { match link.resolve(store).await? { PublicNode::File(file) => { result.push((name.clone(), file.metadata.clone())); @@ -338,6 +653,44 @@ impl PublicDirectory { } /// Removes a file or directory from the directory. + /// + /// # Examples + /// + /// ``` + /// use wnfs::{PublicDirectory, MemoryBlockStore, OpResult}; + /// use libipld::cid::Cid; + /// use std::rc::Rc; + /// use chrono::Utc; + /// + /// #[async_std::main] + /// async fn main() { + /// let time = Utc::now(); + /// let dir = Rc::new(PublicDirectory::new(time)); + /// let store = MemoryBlockStore::default(); + /// + /// let OpResult { root_dir, .. } = Rc::new(PublicDirectory::new(Utc::now())) + /// .write( + /// &["pictures".into(), "cats".into(), "tabby.png".into()], + /// Cid::default(), + /// Utc::now(), + /// &store + /// ) + /// .await + /// .unwrap(); + /// + /// let OpResult { root_dir, .. } = root_dir + /// .rm(&["pictures".into(), "cats".into()], &store) + /// .await + /// .unwrap(); + /// + /// let OpResult { root_dir, result } = root_dir + /// .ls(&["pictures".into()], &store) + /// .await + /// .unwrap(); + /// + /// assert_eq!(result.len(), 0); + /// } + /// ``` pub async fn rm( self: Rc, path_segments: &[String], @@ -345,8 +698,8 @@ impl PublicDirectory { ) -> Result> { let (directory_path, node_name) = utils::split_last(path_segments)?; - let mut directory_node_path = match self.get_node_path(directory_path, store).await? { - GetNodePathResult::Complete(node_path) => node_path, + let mut directory_node_path = match self.get_path_nodes(directory_path, store).await? { + PathNodesResult::Complete(node_path) => node_path, _ => bail!(FsError::NotFound), }; @@ -369,36 +722,81 @@ impl PublicDirectory { /// Moves a file or directory from one path to another. /// /// This function requires stating the destination name explicitly. + /// + /// # Examples + /// + /// ``` + /// use wnfs::{PublicDirectory, MemoryBlockStore, OpResult}; + /// use libipld::cid::Cid; + /// use std::rc::Rc; + /// use chrono::Utc; + /// + /// #[async_std::main] + /// async fn main() { + /// let time = Utc::now(); + /// let dir = Rc::new(PublicDirectory::new(time)); + /// let store = MemoryBlockStore::default(); + /// + /// let OpResult { root_dir, .. } = Rc::new(PublicDirectory::new(Utc::now())) + /// .write( + /// &["pictures".into(), "cats".into(), "tabby.png".into()], + /// Cid::default(), + /// Utc::now(), + /// &store + /// ) + /// .await + /// .unwrap(); + /// + /// let OpResult { root_dir, .. } = root_dir + /// .basic_mv( + /// &["pictures".into(), "cats".into()], + /// &["cats".into()], + /// Utc::now(), + /// &store + /// ) + /// .await + /// .unwrap(); + /// + /// let OpResult { root_dir, result } = root_dir + /// .ls(&[], &store) + /// .await + /// .unwrap(); + /// + /// assert_eq!(result.len(), 2); + /// } + /// ``` pub async fn basic_mv( self: Rc, path_segments_from: &[String], path_segments_to: &[String], + time: DateTime, store: &B, ) -> Result> { let root_dir = Rc::clone(&self); - let (directory_path_nodes, tail) = utils::split_last(path_segments_to)?; + let (directory_path_nodes, filename) = utils::split_last(path_segments_to)?; let OpResult { root_dir, result: removed_node, } = root_dir.rm(path_segments_from, store).await?; - let mut path_nodes = match root_dir.get_node_path(directory_path_nodes, store).await? { - GetNodePathResult::Complete(node_path) => node_path, + let mut path_nodes = match root_dir.get_path_nodes(directory_path_nodes, store).await? { + PathNodesResult::Complete(node_path) => node_path, _ => bail!(FsError::NotFound), }; let mut directory = (*path_nodes.tail).clone(); ensure!( - !directory.userland.contains_key(tail), + !directory.userland.contains_key(filename), FsError::FileAlreadyExists ); - // TODO(appcypher): We need to update the mtime of the moved node. + let removed_node = removed_node.update_mtime(time); + directory .userland - .insert(tail.clone(), Link::Node(removed_node)); + .insert(filename.clone(), Link::Node(removed_node)); path_nodes.tail = Rc::new(directory); @@ -408,58 +806,49 @@ impl PublicDirectory { }) } - /// Encode the directory as a CBOR object. - pub async fn encode(&self, store: &mut B) -> Result> { - let mut bytes = Vec::new(); - - // Write the major of the section being written. - encode::write_u64( - &mut bytes, - MajorKind::Map, - PublicDirectory::FIELDS.len() as u64, - )?; - - // Ordering the fields by name based on RFC-7049 which is also what libipld uses. - let mut cbor_order: Vec<&'static str> = Vec::from_iter(PublicDirectory::FIELDS); - cbor_order.sort_unstable_by(|&a, &b| match a.len().cmp(&b.len()) { - Ordering::Greater => Ordering::Greater, - Ordering::Less => Ordering::Less, - Ordering::Equal => a.cmp(b), - }); - - // Iterate over the fields. - for field in cbor_order.iter() { - // Encode field name. - field.encode(DagCborCodec, &mut bytes)?; - // Encode field value. - match *field { - "metadata" => { - self.metadata.encode(DagCborCodec, &mut bytes)?; - } - "userland" => { - let new_userland = { - let mut tmp = BTreeMap::new(); - for (k, link) in self.userland.iter() { - let cid = link.seal(store).await?; - tmp.insert(k.clone(), cid); - } - tmp - }; - - new_userland.encode(DagCborCodec, &mut bytes)?; - } - "previous" => { - self.previous.encode(DagCborCodec, &mut bytes)?; - } - _ => unreachable!(), - } - } - - Ok(bytes) - } - // TODO(appcypher): Make non recursive. /// Constructs a tree from directory with `base` as the historical ancestor. + /// + /// # Examples + /// + /// ``` + /// use wnfs::{PublicDirectory, MemoryBlockStore, OpResult}; + /// use libipld::cid::Cid; + /// use std::rc::Rc; + /// use chrono::Utc; + /// + /// #[async_std::main] + /// async fn main() { + /// let time = Utc::now(); + /// let dir = Rc::new(PublicDirectory::new(time)); + /// let mut store = MemoryBlockStore::default(); + /// + /// let OpResult { root_dir: base_root, .. } = Rc::new(PublicDirectory::new(Utc::now())) + /// .write( + /// &["pictures".into(), "cats".into(), "tabby.png".into()], + /// Cid::default(), + /// Utc::now(), + /// &store + /// ) + /// .await + /// .unwrap(); + /// + /// let OpResult { root_dir: recent_root, .. } = Rc::clone(&base_root) + /// .write( + /// &["pictures".into(), "cats".into(), "katherine.png".into()], + /// Cid::default(), + /// Utc::now(), + /// &store + /// ) + /// .await + /// .unwrap(); + /// + /// let OpResult { root_dir: derived_root, .. } = recent_root + /// .base_history_on(base_root, &mut store) + /// .await + /// .unwrap(); + /// } + /// ``` pub async fn base_history_on( self: Rc, base: Rc, @@ -493,7 +882,7 @@ impl PublicDirectory { /// Constructs a tree from directory with `base` as the historical ancestor. #[async_recursion(?Send)] - pub async fn base_history_on_helper( + pub(crate) async fn base_history_on_helper( link: &Link, base_link: &Link, store: &mut B, @@ -514,7 +903,7 @@ impl PublicDirectory { (PublicNode::File(file_rc), PublicNode::File(_)) => { let mut file = (*file_rc).clone(); file.previous = Some(base_link.seal(store).await?); - return Ok(Some(Link::Node(PublicNode::File(Rc::new(file))))); + return Ok(Some(Link::with_file(Rc::new(file)))); } _ => { // One is a file and the other is a directory @@ -533,10 +922,62 @@ impl PublicDirectory { } } - Ok(Some(Link::Node(PublicNode::Dir(Rc::new(dir))))) + Ok(Some(Link::with_dir(Rc::new(dir)))) } - /// Gets the iterator for walking the history of a directory node. + /// Gets a stream for walking the history of a directory node. + /// + /// # Examples + /// + /// ``` + /// use std::{rc::Rc, pin::Pin}; + /// + /// use wnfs::{PublicDirectory, MemoryBlockStore, OpResult}; + /// use libipld::cid::Cid; + /// use chrono::Utc; + /// use futures_util::pin_mut; + /// use async_std::stream::StreamExt; + /// + /// #[async_std::main] + /// async fn main() { + /// let time = Utc::now(); + /// let dir = Rc::new(PublicDirectory::new(time)); + /// let mut store = MemoryBlockStore::default(); + /// + /// let OpResult { root_dir: base_root, .. } = Rc::new(PublicDirectory::new(Utc::now())) + /// .write( + /// &["pictures".into(), "cats".into(), "tabby.png".into()], + /// Cid::default(), + /// Utc::now(), + /// &store + /// ) + /// .await + /// .unwrap(); + /// + /// let OpResult { root_dir: recent_root, .. } = Rc::clone(&base_root) + /// .write( + /// &["pictures".into(), "cats".into(), "katherine.png".into()], + /// Cid::default(), + /// Utc::now(), + /// &store + /// ) + /// .await + /// .unwrap(); + /// + /// let OpResult { root_dir: derived_root, .. } = recent_root + /// .base_history_on(base_root, &mut store) + /// .await + /// .unwrap(); + /// + /// let history = derived_root.get_history(&store); + /// + /// pin_mut!(history); + /// + /// while let Some(cid) = history.next().await { + /// println!("previous = {:?}", cid); + /// } + /// } + /// ``` pub fn get_history( self: Rc, store: &B, @@ -549,6 +990,56 @@ impl PublicDirectory { } } } + + /// Encode the directory as a CBOR object. + pub(crate) async fn encode(&self, store: &mut B) -> Result> { + let mut bytes = Vec::new(); + + // Write the major of the section being written. + encode::write_u64( + &mut bytes, + MajorKind::Map, + PublicDirectory::FIELDS.len() as u64, + )?; + + // Ordering the fields by name based on RFC-7049 which is also what libipld uses. + let mut cbor_order: Vec<&'static str> = Vec::from_iter(PublicDirectory::FIELDS); + cbor_order.sort_unstable_by(|&a, &b| match a.len().cmp(&b.len()) { + Ordering::Greater => Ordering::Greater, + Ordering::Less => Ordering::Less, + Ordering::Equal => a.cmp(b), + }); + + // Iterate over the fields. + for field in cbor_order.iter() { + // Encode field name. + field.encode(DagCborCodec, &mut bytes)?; + // Encode field value. + match *field { + "metadata" => { + self.metadata.encode(DagCborCodec, &mut bytes)?; + } + "userland" => { + let new_userland = { + let mut tmp = BTreeMap::new(); + for (k, link) in self.userland.iter() { + let cid = link.seal(store).await?; + tmp.insert(k.clone(), cid); + } + tmp + }; + + new_userland.encode(DagCborCodec, &mut bytes)?; + } + "previous" => { + self.previous.encode(DagCborCodec, &mut bytes)?; + } + _ => unreachable!(), + } + } + + Ok(bytes) + } } impl Id for PublicDirectory { @@ -716,14 +1207,11 @@ mod public_directory_tests { let time = Utc::now(); let store = MemoryBlockStore::default(); - // on a fresh directory let OpResult { root_dir, .. } = Rc::new(PublicDirectory::new(time)) - // create a new dir .mkdir(&["tamedun".into(), "pictures".into()], time, &store) .await .unwrap(); - // get the node let OpResult { result, .. } = root_dir .get_node(&["tamedun".into(), "pictures".into()], &store) .await @@ -856,14 +1344,14 @@ mod public_directory_tests { let reconstructed = path_nodes.clone().reconstruct(); let result = reconstructed - .get_node_path(&["Documents".into(), "Apps".into()], &store) + .get_path_nodes(&["Documents".into(), "Apps".into()], &store) .await .unwrap(); match result { - GetNodePathResult::MissingLink(_, segment) => panic!("MissingLink {segment}"), - GetNodePathResult::NotADirectory(_, segment) => panic!("NotADirectory {segment}"), - GetNodePathResult::Complete(path_nodes_2) => { + PathNodesResult::MissingLink(_, segment) => panic!("MissingLink {segment}"), + PathNodesResult::NotADirectory(_, segment) => panic!("NotADirectory {segment}"), + PathNodesResult::Complete(path_nodes_2) => { assert_eq!(path_nodes.path.len(), path_nodes_2.path.len()); assert_eq!(path_nodes.path[0].1, path_nodes_2.path[0].1); assert_eq!(path_nodes.path[1].1, path_nodes_2.path[1].1); @@ -994,6 +1482,7 @@ mod public_directory_tests { .basic_mv( &["pictures".into(), "cats".into()], &["images".into(), "cats".into()], + Utc::now(), &store, ) .await @@ -1033,6 +1522,7 @@ mod public_directory_tests { .basic_mv( &["videos".into(), "movies".into()], &["videos".into(), "movies".into(), "anime".into()], + Utc::now(), &store, ) .await; @@ -1052,7 +1542,12 @@ mod public_directory_tests { .unwrap(); let OpResult { root_dir, .. } = root_dir - .basic_mv(&["file.txt".into()], &["renamed.txt".into()], &store) + .basic_mv( + &["file.txt".into()], + &["renamed.txt".into()], + Utc::now(), + &store, + ) .await .unwrap(); @@ -1084,6 +1579,7 @@ mod public_directory_tests { .basic_mv( &["movies".into(), "ghibli".into()], &["file.txt".into()], + Utc::now(), &store, ) .await; diff --git a/crates/fs/public/file.rs b/crates/fs/public/file.rs index d508c8e2..20276234 100644 --- a/crates/fs/public/file.rs +++ b/crates/fs/public/file.rs @@ -11,6 +11,18 @@ use crate::{BlockStore, Metadata, UnixFsNodeKind}; use super::Id; /// A file in a WNFS public file system. +/// +/// # Examples +/// +/// ``` +/// use wnfs::{PublicFile, Id}; +/// use chrono::Utc; +/// use libipld::Cid; +/// +/// let file = PublicFile::new(Utc::now(), Cid::default()); +/// +/// println!("id = {}", file.get_id()); +/// ``` #[derive(Debug, Clone, PartialEq, Eq, DagCbor)] pub struct PublicFile { pub(crate) metadata: Metadata, @@ -24,6 +36,18 @@ pub struct PublicFile { impl PublicFile { /// Creates a new file using the given metadata and CID. + /// + /// # Examples + /// + /// ``` + /// use wnfs::{PublicFile, Id}; + /// use chrono::Utc; + /// use libipld::Cid; + /// + /// let file = PublicFile::new(Utc::now(), Cid::default()); + /// + /// println!("id = {}", file.get_id()); + /// ``` pub fn new(time: DateTime, userland: Cid) -> Self { Self { metadata: Metadata::new(time, UnixFsNodeKind::File), @@ -38,6 +62,22 @@ impl PublicFile { } /// Stores file in provided block store. + /// + /// # Examples + /// + /// ``` + /// use wnfs::{PublicFile, Id, MemoryBlockStore}; + /// use chrono::Utc; + /// use libipld::Cid; + /// + /// #[async_std::main] + /// async fn main() { + /// let mut store = MemoryBlockStore::default(); + /// let file = PublicFile::new(Utc::now(), Cid::default()); + /// + /// file.store(&mut store).await.unwrap(); + /// } + /// ``` pub async fn store(&self, store: &mut B) -> Result { let bytes = { let mut tmp = vec![]; diff --git a/crates/fs/public/link.rs b/crates/fs/public/link.rs index ae91bba9..8ae6f675 100644 --- a/crates/fs/public/link.rs +++ b/crates/fs/public/link.rs @@ -18,16 +18,14 @@ pub enum Link { } impl Link { - // TODO(appcypher): Remove. /// Creates a new directory node link. - pub fn with_dir(dir: PublicDirectory) -> Self { - Link::Node(PublicNode::Dir(Rc::new(dir))) + pub fn with_dir(dir: Rc) -> Self { + Link::Node(PublicNode::Dir(dir)) } - // TODO(appcypher): Remove. /// Creates a new file node link. - pub fn with_file(file: PublicFile) -> Self { - Link::Node(PublicNode::File(Rc::new(file))) + pub fn with_file(file: Rc) -> Self { + Link::Node(PublicNode::File(file)) } /// Resolves a CID linkin the file system to a node. @@ -46,6 +44,7 @@ impl Link { }) } + /// Checks if the link matches another link. pub async fn partial_equal(&self, other: &Self, store: &mut B) -> Result { match (self, other) { (Self::Cid(cid), Self::Cid(base_cid)) => { @@ -102,7 +101,7 @@ mod public_link_tests { let userland = Cid::default(); - let file = PublicFile::new(time, userland); + let file = Rc::new(PublicFile::new(time, userland)); let mut store = MemoryBlockStore::default(); diff --git a/crates/fs/public/node.rs b/crates/fs/public/node.rs index 45fedd49..7b3e35d8 100644 --- a/crates/fs/public/node.rs +++ b/crates/fs/public/node.rs @@ -7,6 +7,7 @@ use std::{ }; use anyhow::{bail, Result}; +use chrono::{DateTime, Utc}; use libipld::{cbor::DagCborCodec, codec::Decode, Cid}; use super::{Id, PublicDirectory, PublicFile}; @@ -20,6 +21,7 @@ pub enum PublicNode { } impl PublicNode { + /// Checks if the reference of one node is the same as the reference of another node. pub(crate) fn ptr_eq(&self, other: &PublicNode) -> bool { match (self, other) { (Self::File(self_file), Self::File(other_file)) => Rc::ptr_eq(self_file, other_file), @@ -28,6 +30,22 @@ impl PublicNode { } } + /// Create node with updated modified time. + pub fn update_mtime(&self, time: DateTime) -> Self { + match self { + Self::File(file) => { + let mut file = (**file).clone(); + file.metadata.unix_fs.modified = time.timestamp(); + Self::File(Rc::new(file)) + } + Self::Dir(dir) => { + let mut dir = (**dir).clone(); + dir.metadata.unix_fs.modified = time.timestamp(); + Self::Dir(Rc::new(dir)) + } + } + } + /// Create node with updated previous pointer value. pub fn update_previous(&self, cid: Option) -> Self { match self { diff --git a/crates/wasm/Cargo.toml b/crates/wasm/Cargo.toml index a6d53158..9b13451a 100644 --- a/crates/wasm/Cargo.toml +++ b/crates/wasm/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "wasm-wnfs" -version = "0.1.1" -description = "WebNative filesystem WebAssembly API" +version = "0.1.6" +description = "WebNative Filesystem API (WebAssembly)" keywords = ["wnfs", "webnative", "ipfs", "decentralisation"] categories = [ "filesystem", @@ -9,17 +9,15 @@ categories = [ "web-programming", "wasm", ] -license-file = "LICENSE" +license = "Apache-2.0" readme = "README.md" edition = "2021" -repository = "https://github.com/fission-suite/rs-wnfs" +repository = "https://github.com/WebNativeFileSystem/rs-wnfs/tree/main/crates/wasm" homepage = "https://fission.codes" authors = ["The Fission Authors"] [dependencies] wnfs = { path = "../fs", version = "0.1.0" } -# serde_json = "1.0" -# serde = { version = "1.0", features = ["derive"] } wasm-bindgen = { version = "0.2", optional = true, features = ["serde-serialize"] } wasm-bindgen-futures = { version = "0.4", optional = true } js-sys = { version = "0.3", optional = true } diff --git a/crates/wasm/LICENSE b/crates/wasm/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/crates/wasm/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/crates/wasm/README.md b/crates/wasm/README.md index e3111448..22239bbd 100644 --- a/crates/wasm/README.md +++ b/crates/wasm/README.md @@ -1,6 +1,83 @@ -## The WebAssembly API +## Wasm WNFS -## Building the Project +This package implements the primitives for creating and manipulating IPLD graphs that encode WNFS. + +The core of this project is a WebAssembly binary compiled from the [Rust source code](https://github.com/WebNativeFileSystem/rs-wnfs/tree/main/crates/fs). + +## Outline + +- [Usage](#usage) +- [Setting up the project](#setting-up-the-project) +- [Testing the Project](#testing-the-project) +- [Publishing Package](#publishing-package) + +## Usage + +Creating a new public directory. + +```js +import { PublicDirectory } from "wnfs"; + +const time = new Date(); +const dir = new PublicDirectory(time); +``` + +The in-memory files and directories you create with `wnfs` will need to be sealed and stored somewhere. For that, an type that implements the BlockStore interface like [this one](https://github.com/WebNativeFileSystem/rs-wnfs/blob/8bb0fbb457051295f1ed4a4707dc230c04612658/crates/wasm/examples/graph/src/blockstore.ts#L9-L29) can be used. + +```js +import { MemoryBlockStore } from "./store"; +import { PublicDirectory } from "wnfs"; + +const time = new Date(); +const dir = new PublicDirectory(time); +const store = new MemoryBlockStore(); + +// ... +``` + +The WNFS API is immutable, therefore, we need to keep track of the updated root directory after every change. + +Each fs operation returns a possibly updated root directory that subsequent changes can be applied on. + +```js +// ... + +// Create a /pictures/cats directory. +var { rootDir } = await dir.mkdir(["pictures", "cats"], time, store); + +// Get a sample CIDv1. +const cid = Uint8Array.from([ + 1, 112, 18, 32, 195, 196, 115, 62, 200, 175, 253, 6, 207, 158, 159, 245, 15, + 252, 107, 205, 46, 200, 90, 97, 112, 0, 75, 183, 9, 102, 156, 49, 222, 148, + 57, 26, +]); + +// Add a file to /pictures/cats. +var { rootDir } = await rootDir.write( + ["pictures", "cats", "tabby.png"], + cid, + time, + store +); + +// Create and add a file to /pictures/dogs directory. +var { rootDir } = await rootDir.write( + ["pictures", "dogs", "billie.jpeg"], + cid, + time, + store +); + +// Delete /pictures/cats directory. +var { rootDir } = await rootDir.rm(["pictures", "cats"], store); + +// List all files in /pictures directory. +var { result } = await rootDir.ls(["pictures"], store); + +console.log("Files in /pictures directory:", result); +``` + +## Setting up the Project - Install `wasm-pack` @@ -8,7 +85,13 @@ cargo install wasm-pack ``` -- Install playwrigth binaries +- Install dependencies + + ```bash + yarn + ``` + +- Install playwright binaries ```bash npx playwright install @@ -17,11 +100,31 @@ - Build project ```bash - wasm-pack build --target web + wasm-pack build ``` +## Testing the Project + - Run tests ```bash yarn playwright test ``` + +## Publishing Package + +- Build the project + + ```bash + rs-wnfs build --wasm + ``` + +- Publish from the `pkg` directory + + ```bash + cd pkg + ``` + + ```bash + npm publish + ``` diff --git a/crates/wasm/fs/public/directory.rs b/crates/wasm/fs/public/directory.rs index 895f8f76..48275343 100644 --- a/crates/wasm/fs/public/directory.rs +++ b/crates/wasm/fs/public/directory.rs @@ -35,10 +35,6 @@ impl PublicDirectory { } /// Follows a path and fetches the node at the end of the path. - /// - /// If path is empty, this returns a cloned directory based on `self`. - /// - /// If `diverge` is true, this will clone the spine of the path. #[wasm_bindgen(js_name = "getNode")] pub fn get_node(&self, path_segments: &Array, store: BlockStore) -> JsResult { let directory = Rc::clone(&self.0); @@ -127,8 +123,6 @@ impl PublicDirectory { } /// Removes a file or directory from the directory. - /// - /// Rather than mutate the directory directly, we create a new directory and return it. pub fn rm(&self, path_segments: &Array, store: BlockStore) -> JsResult { let directory = Rc::clone(&self.0); let store = ForeignBlockStore(store); @@ -148,8 +142,6 @@ impl PublicDirectory { } /// Writes a file to the directory. - /// - /// Rather than mutate the directory directly, we create a new directory and return it. pub fn write( &self, path_segments: &Array, @@ -180,16 +172,18 @@ impl PublicDirectory { &self, path_segments_from: &Array, path_segments_to: &Array, + time: &Date, store: BlockStore, ) -> JsResult { let directory = self.0.clone(); let store = ForeignBlockStore(store); + let time = DateTime::::from(time); let path_segments_from = utils::convert_path_segments(path_segments_from)?; let path_segments_to = utils::convert_path_segments(path_segments_to)?; Ok(future_to_promise(async move { let WnfsOpResult { root_dir, .. } = directory - .basic_mv(&path_segments_from, &path_segments_to, &store) + .basic_mv(&path_segments_from, &path_segments_to, time, &store) .await .map_err(|e| Error::new(&format!("Cannot create directory: {e}")))?; @@ -199,8 +193,6 @@ impl PublicDirectory { /// Creates a new directory at the specified path. /// - /// If path is empty, this returns a cloned directory based on `self`. - /// /// This method acts like `mkdir -p` in Unix because it creates intermediate directories if they do not exist. pub fn mkdir( &self, @@ -223,7 +215,7 @@ impl PublicDirectory { })) } - /// Converts a directory to a node. + /// Converts directory to a node. #[wasm_bindgen(js_name = "asNode")] pub fn as_node(&self) -> PublicNode { PublicNode(WnfsPublicNode::Dir(self.0.clone())) diff --git a/crates/wasm/package.json b/crates/wasm/package.json index a27c3435..fbf6035e 100644 --- a/crates/wasm/package.json +++ b/crates/wasm/package.json @@ -1,7 +1,4 @@ { - "scripts": { - "build": "webpack" - }, "devDependencies": { "@playwright/test": "^1.21.1", "@wasm-tool/wasm-pack-plugin": "^1.6.0", diff --git a/scripts/rs-wnfs.sh b/scripts/rs-wnfs.sh new file mode 100755 index 00000000..403c9744 --- /dev/null +++ b/scripts/rs-wnfs.sh @@ -0,0 +1,273 @@ +#!/bin/bash +set -e + +# PATHS +# Get current working directory +current_dir=`pwd` + +# Get the absolute path where current script is running from. +script_path=$(readlink -f $(which $0)) + +# Get the canonical directory of script. +if [[ -L $script_path ]]; then + script_dir=$(dirname $(readlink -f $script_path)) +else + script_dir=$(dirname $script_path) +fi + +# RETURN VARIABLE +ret="" + +# ARGUMENTS +args="${@:2}" # All arguments except the first + +# COLORS +red='\033[0;31m' +green='\033[0;32m' +purple='\033[0;35m' +none='\033[0m' + +# DESCRIPTION: +# Where execution starts +# +main() { + case $1 in + build ) + build + ;; + test ) + test + ;; + coverage ) + coverage + ;; + publish ) + publish + ;; + setup ) + setup + ;; + *) + help + ;; + esac + + exit 0 +} + +# DESCRIPTION: +# Prints the help info. +# +# USAGE: +# rs-wnfs build +# +help() { + echo "" + echo "Rust WNFS Utility Script" + echo "" + echo "USAGE:" + echo " rs-wnfs [COMMAND] [...args]" + echo "" + echo "COMMAND:" + echo " * build [--fs|--wasm|--all] - build projects" + echo " * test [--fs|--wasm|--all] - run tests" + echo " * publish [--fs|--wasm|--all] - publish packages" + echo " * coverage [--fs|--wasm|--all] - show code coverage" + echo " * setup - install rs-wnfs script" + echo " * help - print this help message" + echo "" + echo "" +} + +#------------------------------------------------------------------------------- +# Commands +#------------------------------------------------------------------------------- + +# DESCRIPTION: +# Builds the project. +# +# USAGE: +# rs-wnfs build [--fs|--wasm|--all] +# +build() { + if check_flag --fs; then + build_fs + elif check_flag --wasm; then + build_wasm + else + build_fs + build_wasm + fi +} + +build_fs() { + display_header "💿 | BUILDING WNFS PROJECT | 💿" + cargo build --release +} + +build_wasm() { + display_header "💿 | BUILDING WASM-WNFS PROJECT | 💿" + echo "script_dir = $script_dir" + cd $script_dir/../crates/wasm + wasm-pack build --target nodejs + sed -i ".bak" -e 's/"name": "wasm-wnfs"/"name": "wnfs"/g' pkg/package.json + rm pkg/package.json.bak +} + +# DESCRIPTION: +# Runs tests. +# +# USAGE: +# rs-wnfs test [--fs|--wasm|--all] +# +test() { + if check_flag --fs; then + test_fs + elif check_flag --wasm; then + test_wasm + else + test_fs + test_wasm + fi +} + +test_fs() { + display_header "🧪 | RUNNING WNFS TESTS | 🧪" + cargo test -p wnfs --release -- --nocapture +} + +test_wasm() { + display_header "🧪 | RUNNING WASM-WNFS TESTS | 🧪" + echo "script_dir = $script_dir" + cd $script_dir/../crates/wasm + yarn playwright test +} + +# DESCRIPTION: +# Shows the code coverage of the project +# +# USAGE: +# rs-wnfs coverage [--fs|--wasm|--all] +# +coverage() { + errorln "coverage command not implemented yet" + exit 1 +} + +# DESCRIPTION: +# Publishes the project. +# +# USAGE: +# rs-wnfs publish [--fs|--wasm|--all] +# +publish() { + errorln "publish command not implemented yet" + exit 1 +} + +#------------------------------------------------------------------------------ +# Helper functions +#------------------------------------------------------------------------------ + +# DESCRIPTION: +# Gets the value following a flag +# +get_flag_value() { + local found=false + local key=$1 + local count=0 + + # For every argument in the list of arguments + for arg in $args; do + count=$((count + 1)) + # Check if any of the argument matches the key provided + if [[ $arg = $key ]]; then + found=true + break + fi + done + + local args=($args) + local value=${args[count]} + + # Check if argument specified was found + if [[ $found = true ]]; then + + # Check if there exists a word after the key + # And that such word doesn't start with hyphen + if [[ ! -z $value ]] && [[ $value != "-"* ]]; then + ret=$value + else + ret="" + fi + + else + ret="" + fi +} + +# DESCRIPTION: +# Checks if the flag is present in the list of arguments +# +check_flag() { + local found=1 + local key=$1 + + # For every argument in the list of arguments + for arg in $args; do + # Check if any of the argument matches the key provided + if [[ $arg = $key ]]; then + found=0 + break + fi + done + + return $found +} + +# DESCRIPTION: +# Sets up the cript by making it excutable and available system wide +# +setup() { + displayln "Make script executable" + chmod u+x $script_path + + displayln "Drop a link to it in /usr/local/bin" + if ln -s $script_path /usr/local/bin/rs-wnfs; then + successln "Successfully installed" + else + local result=$? + errorln "Failed to install" + exit $result + fi +} + +# DESCRIPTION: +# Prints a message. +# +displayln() { + printf "\n::: $1 :::\n" +} + +# DESCRIPTION: +# Prints an error message. +# +errorln() { + printf "\n${red}::: $1 :::${none}\n\n" +} + +# DESCRIPTION: +# Prints an success message. +# +successln() { + printf "\n${green}::: $1 :::${none}\n\n" +} + +# DESCRIPTION: +# Prints a header message. +# +display_header() { + printf "\n${purple}$1${none}\n\n" +} + +main $@ diff --git a/scripts/wnfs.sh b/scripts/wnfs.sh deleted file mode 100755 index 5076be02..00000000 --- a/scripts/wnfs.sh +++ /dev/null @@ -1,185 +0,0 @@ -#!/bin/bash - -# PATHS -# Get current working directory -current_dir=`pwd` - -# Get the absolute path of where script is running from -script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd)" -script_path="$script_dir/wnfs.sh" - -# RETURN VARIABLE -ret="" - -# ARGUMENTS -args="${@:2}" # All arguments except the first - -# COLORS -red='\033[0;31m' -green='\033[0;32m' -none='\033[0m' - -# DESCRIPTION: -# Where execution starts -# -main() { - case $1 in - build ) - build - ;; - test ) - test - ;; - coverage ) - coverage - ;; - setup ) - setup - ;; - *) - help - ;; - esac - - exit 0 -} - -# DESCRIPTION: -# Prints the help info. -# -# USAGE: -# wnfs build -# -help() { - echo "" - echo "Rust WNFS Utility Script" - echo "" - echo "USAGE:" - echo " wnfs [COMMAND] [...args]" - echo "" - echo "COMMAND:" - echo " * build - build project" - echo " * test - run tests" - echo " * coverage - show code coverage" - echo " * setup - install wnfs script" - echo " * -h, help - print this help message" - echo "" - echo "" -} - -#------------------------------------------------------------------------------- -# Commands -#------------------------------------------------------------------------------- - -# DESCRIPTION: -# Builds the project. -# -# USAGE: -# wnfs build -# -build() { - display_header "💿 BUILDING WNFS PROJECT" - cargo build --release - - display_header "💿 BUILDING WASM-WNFS PROJECT" - cd crates/wasm && wasm-pack build --target web --release -} - -# DESCRIPTION: -# Runs tests. -# -# USAGE: -# wnfs test -# -test() { - display_header "🧪 RUNNING WNFS TESTS" - cargo test -p wnfs --release -- --nocapture - - display_header "🧪 RUNNING WASM-WNFS TESTS" - cd crates/wasm && yarn playwright test -} - -# DESCRIPTION: -# Shows the code coverage of the project -# -# USAGE: -# wnfs coverage -# -coverage() { - displayln "coverage command not implemented yet" -} - -#------------------------------------------------------------------------------ -# Helper functions -#------------------------------------------------------------------------------ - -# DESCRIPTION: -# Gets the value following a flag -# -get_flag_value() { - local found=false - local key=$1 - local count=0 - - # For every argument in the list of arguments - for arg in $args; do - count=$((count + 1)) - # Check if any of the argument matches the key provided - if [[ $arg = $key ]]; then - found=true - break - fi - done - - local args=($args) - local value=${args[count]} - - # Check if argument specified was found - if [[ $found = true ]]; then - - # Check if there exists a word after the key - # And that such word doesn't start with hyphen - if [[ ! -z $value ]] && [[ $value != "-"* ]]; then - ret=$value - else - ret="" - fi - - else - ret="" - fi -} - -# DESCRIPTION: -# Sets up the cript by making it excutable and available system wide -# -setup() { - display "Make script executable" - chmod u+x $script_path - - display "Drop a link to it in /usr/local/bin" - ln -s $script_path /usr/local/bin/wnfs -} - -# DESCRIPTION: -# A custom print function -# -display() { - printf "::: $1 :::\n" -} - -# DESCRIPTION: -# A custom print function that starts its output with a newline -# -displayln() { - printf "\n::: $1 :::\n" -} - -# DESCRIPTION: -# A custom print function for headers. -# -display_header() { - printf "\n${green}$1${none}\n\n" -} - -main $@