Skip to content

Commit

Permalink
feat: Implement public directory cp & more efficient copy for `Privat…
Browse files Browse the repository at this point in the history
…eFile` (#319)

* Implement `cp` for public directories

* Implement leaner copy algorithms

* Write wasm bindings for public cp & tests
  • Loading branch information
matheus23 committed Aug 2, 2023
1 parent 55cf2e0 commit cebb956
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 17 deletions.
24 changes: 24 additions & 0 deletions wnfs-wasm/src/fs/public/directory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Promise> {
let mut directory = Rc::clone(&self.0);
let store = ForeignBlockStore(store);
let time = DateTime::<Utc>::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.
Expand Down
46 changes: 46 additions & 0 deletions wnfs-wasm/tests/public.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
10 changes: 4 additions & 6 deletions wnfs/src/private/directory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1204,7 +1202,7 @@ impl PrivateDirectory {
/// .await
/// .unwrap();
///
/// let result = root_dir
/// root_dir
/// .cp(
/// &["code".into(), "python".into(), "hello.py".into()],
/// &["code".into(), "hello.py".into()],
Expand Down
58 changes: 50 additions & 8 deletions wnfs/src/private/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Utc>) {
self.content.metadata.upsert_mtime(time);
self.content.content = other.content.content.clone();
}

/// Streams the content of a file as chunk of blocks.
///
/// # Examples
Expand Down Expand Up @@ -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(())
}

Expand Down
4 changes: 1 addition & 3 deletions wnfs/src/private/node/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
75 changes: 75 additions & 0 deletions wnfs/src/public/directory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self>,
path_segments_from: &[String],
path_segments_to: &[String],
time: DateTime<Utc>,
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.
///
Expand Down

0 comments on commit cebb956

Please sign in to comment.