From c96ee7fe2b9041828f5c03e0f1059c3b0580ef13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Kr=C3=BCger?= Date: Tue, 1 Aug 2023 16:51:56 +0200 Subject: [PATCH 1/3] Implement `cp` for public directories --- wnfs/src/private/directory.rs | 2 +- wnfs/src/public/directory.rs | 75 +++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/wnfs/src/private/directory.rs b/wnfs/src/private/directory.rs index 8f4f4a6f..3a64790d 100644 --- a/wnfs/src/private/directory.rs +++ b/wnfs/src/private/directory.rs @@ -1204,7 +1204,7 @@ impl PrivateDirectory { /// .await /// .unwrap(); /// - /// let result = root_dir + /// root_dir /// .cp( /// &["code".into(), "python".into(), "hello.py".into()], /// &["code".into(), "hello.py".into()], diff --git a/wnfs/src/public/directory.rs b/wnfs/src/public/directory.rs index 217fc825..0cfc106e 100644 --- a/wnfs/src/public/directory.rs +++ b/wnfs/src/public/directory.rs @@ -656,6 +656,81 @@ impl PublicDirectory { Ok(()) } + /// Copies a file or directory from one path to another. + /// + /// # Examples + /// + /// ``` + /// use anyhow::Result; + /// use std::rc::Rc; + /// use libipld_core::cid::Cid; + /// use chrono::Utc; + /// use rand::thread_rng; + /// use wnfs::{ + /// public::PublicDirectory, + /// common::{BlockStore, MemoryBlockStore}, + /// }; + /// + /// #[async_std::main] + /// async fn main() -> Result<()> { + /// let dir = &mut Rc::new(PublicDirectory::new(Utc::now())); + /// let store = &MemoryBlockStore::new(); + /// + /// dir + /// .write( + /// &["code".into(), "python".into(), "hello.py".into()], + /// Cid::default(), + /// Utc::now(), + /// store + /// ) + /// .await?; + /// + /// dir + /// .cp( + /// &["code".into(), "python".into(), "hello.py".into()], + /// &["code".into(), "hello.py".into()], + /// Utc::now(), + /// store + /// ) + /// .await?; + /// + /// let result = dir + /// .ls(&["code".into()], store) + /// .await?; + /// + /// assert_eq!(result.len(), 2); + /// + /// Ok(()) + /// } + /// ``` + pub async fn cp( + self: &mut Rc, + path_segments_from: &[String], + path_segments_to: &[String], + time: DateTime, + store: &impl BlockStore, + ) -> Result<()> { + let (path, filename) = utils::split_last(path_segments_to)?; + let Some(mut node) = self.get_node(path_segments_from, store).await?.cloned() else { + bail!(FsError::NotFound); + }; + + let SearchResult::Found(dir) = self.get_leaf_dir_mut(path, store).await? else { + bail!(FsError::NotFound); + }; + + ensure!( + !dir.userland.contains_key(filename), + FsError::FileAlreadyExists + ); + + node.upsert_mtime(time); + + dir.userland.insert(filename.clone(), PublicLink::new(node)); + + Ok(()) + } + #[async_recursion(?Send)] /// Stores directory in provided block store. /// From da91b1a0c412d8f3fa912163eb988fb649407d25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Kr=C3=BCger?= Date: Tue, 1 Aug 2023 17:19:54 +0200 Subject: [PATCH 2/3] Implement leaner copy algorithms --- wnfs/src/private/directory.rs | 8 ++--- wnfs/src/private/file.rs | 58 ++++++++++++++++++++++++++++++----- wnfs/src/private/node/node.rs | 4 +-- 3 files changed, 54 insertions(+), 16 deletions(-) diff --git a/wnfs/src/private/directory.rs b/wnfs/src/private/directory.rs index 3a64790d..2d38227e 100644 --- a/wnfs/src/private/directory.rs +++ b/wnfs/src/private/directory.rs @@ -602,11 +602,9 @@ impl PrivateDirectory { /// .await?; /// // Clone the forest that was used to write the file /// // Open the file mutably - /// let file = { - /// root_dir - /// .open_file_mut(hello_py, true, Utc::now(), forest, store, rng) - /// .await? - /// }; + /// let file = root_dir + /// .open_file_mut(hello_py, true, Utc::now(), forest, store, rng) + /// .await?; /// // Define the content that will replace what is already in the file /// let new_file_content = b"print('hello world 2')"; /// // Set the contents of the file, waiting for result and expecting no errors diff --git a/wnfs/src/private/file.rs b/wnfs/src/private/file.rs index 267f4f64..ce20c332 100644 --- a/wnfs/src/private/file.rs +++ b/wnfs/src/private/file.rs @@ -260,6 +260,56 @@ impl PrivateFile { }) } + /// Create a copy of this file without re-encrypting the actual content + /// (if the ciphertext is external ciphertext), so this is really fast + /// even if the file contains gigabytes of data. + /// + /// # Example + /// + /// ``` + /// use anyhow::Result; + /// use std::rc::Rc; + /// use chrono::Utc; + /// use rand::thread_rng; + /// use wnfs::{ + /// private::{PrivateDirectory, PrivateFile, forest::{hamt::HamtForest, traits::PrivateForest}}, + /// common::{MemoryBlockStore, utils::get_random_bytes}, + /// }; + /// + /// #[async_std::main] + /// async fn main() -> Result<()> { + /// let store = &MemoryBlockStore::new(); + /// let rng = &mut thread_rng(); + /// let forest = &mut Rc::new(HamtForest::new_rsa_2048(rng)); + /// + /// let file = PrivateFile::with_content( + /// &forest.empty_name(), + /// Utc::now(), + /// get_random_bytes::<100>(rng).to_vec(), + /// forest, + /// store, + /// rng, + /// ) + /// .await?; + /// + /// let root_dir = &mut Rc::new(PrivateDirectory::new(&forest.empty_name(), Utc::now(), rng)); + /// + /// let copy = root_dir + /// .open_file_mut(&["some".into(), "copy.txt".into()], true, Utc::now(), forest, store, rng) + /// .await?; + /// + /// copy.copy_content_from(&file, Utc::now()); + /// + /// assert_eq!(file.get_content(forest, store).await?, copy.get_content(forest, store).await?); + /// + /// Ok(()) + /// } + /// ``` + pub fn copy_content_from(&mut self, other: &Self, time: DateTime) { + self.content.metadata.upsert_mtime(time); + self.content.content = other.content.content.clone(); + } + /// Streams the content of a file as chunk of blocks. /// /// # Examples @@ -663,24 +713,16 @@ impl PrivateFile { /// will update the name to be the sub-name of given parent name, /// so it inherits the write access rules from the new parent and /// resets the `persisted_as` pointer. - /// Will copy and re-encrypt all external content. pub(crate) async fn prepare_key_rotation( &mut self, parent_name: &Name, - forest: &mut impl PrivateForest, - store: &impl BlockStore, rng: &mut impl CryptoRngCore, ) -> Result<()> { - let content = self.get_content(forest, store).await?; - self.header.inumber = NameSegment::new(rng); self.header.update_name(parent_name); self.header.reset_ratchet(rng); self.content.persisted_as = OnceCell::new(); - let content = Self::prepare_content(&self.header.name, content, forest, store, rng).await?; - self.content.content = content; - Ok(()) } diff --git a/wnfs/src/private/node/node.rs b/wnfs/src/private/node/node.rs index d86722be..2b119c1b 100644 --- a/wnfs/src/private/node/node.rs +++ b/wnfs/src/private/node/node.rs @@ -123,9 +123,7 @@ impl PrivateNode { match self { Self::File(file_rc) => { let file = Rc::make_mut(file_rc); - - file.prepare_key_rotation(parent_name, forest, store, rng) - .await?; + file.prepare_key_rotation(parent_name, rng).await?; } Self::Dir(dir_rc) => { let dir = Rc::make_mut(dir_rc); From 7f15ee6eec64d717ef631b4ebf269cc948f93764 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Kr=C3=BCger?= Date: Tue, 1 Aug 2023 17:26:09 +0200 Subject: [PATCH 3/3] Write wasm bindings for public cp & tests --- wnfs-wasm/src/fs/public/directory.rs | 24 +++++++++++++++ wnfs-wasm/tests/public.spec.ts | 46 ++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/wnfs-wasm/src/fs/public/directory.rs b/wnfs-wasm/src/fs/public/directory.rs index 1d952646..f55f35b6 100644 --- a/wnfs-wasm/src/fs/public/directory.rs +++ b/wnfs-wasm/src/fs/public/directory.rs @@ -211,6 +211,30 @@ impl PublicDirectory { })) } + /// Copies a specific node to another location. + pub fn cp( + &self, + path_segments_from: &Array, + path_segments_to: &Array, + time: &Date, + store: BlockStore, + ) -> JsResult { + let mut directory = Rc::clone(&self.0); + 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 { + (&mut directory) + .cp(&path_segments_from, &path_segments_to, time, &store) + .await + .map_err(error("Cannot copy content between directories"))?; + + Ok(utils::create_public_op_result(directory, JsValue::NULL)?) + })) + } + /// Creates a new directory at the specified path. /// /// This method acts like `mkdir -p` in Unix because it creates intermediate directories if they do not exist. diff --git a/wnfs-wasm/tests/public.spec.ts b/wnfs-wasm/tests/public.spec.ts index 77215c88..7a01cf5f 100644 --- a/wnfs-wasm/tests/public.spec.ts +++ b/wnfs-wasm/tests/public.spec.ts @@ -225,6 +225,52 @@ test.describe("PublicDirectory", () => { expect(imagesContent[0].name).toEqual("cats"); }); + test("cp can copy content between directories", async ({ page }) => { + const [imagesContent, picturesContent] = await page.evaluate(async () => { + const { + wnfs: { PublicDirectory }, + mock: { MemoryBlockStore, sampleCID }, + } = await window.setup(); + + const time = new Date(); + const store = new MemoryBlockStore(); + const root = new PublicDirectory(time); + + var { rootDir } = await root.write( + ["pictures", "cats", "luna.jpeg"], + sampleCID, + time, + store + ); + + var { rootDir } = await rootDir.write( + ["pictures", "cats", "tabby.png"], + sampleCID, + time, + store + ); + + var { rootDir } = await rootDir.mkdir(["images"], time, store); + + var { rootDir } = await rootDir.cp( + ["pictures", "cats"], + ["images", "cats"], + time, + store + ); + + const imagesContent = await rootDir.ls(["images"], store); + + const picturesContent = await rootDir.ls(["pictures"], store); + + return [imagesContent, picturesContent]; + }); + + expect(imagesContent.length).toEqual(1); + expect(picturesContent.length).toEqual(1); + expect(imagesContent[0].name).toEqual("cats"); + }); + test("A PublicDirectory has the correct metadata", async ({ page }) => { const result = await page.evaluate(async () => { const {