diff --git a/wnfs-wasm/src/fs/private/file.rs b/wnfs-wasm/src/fs/private/file.rs index 1f6926dd..019cf448 100644 --- a/wnfs-wasm/src/fs/private/file.rs +++ b/wnfs-wasm/src/fs/private/file.rs @@ -89,6 +89,23 @@ impl PrivateFile { self.read_at(value!(0).into(), None, forest, store) } + /// Gets the exact content size without fetching all content blocks. + #[wasm_bindgen(js_name = "getSize")] + pub fn get_size(&self, forest: &PrivateForest, store: BlockStore) -> JsResult { + let file = Rc::clone(&self.0); + let store = ForeignBlockStore(store); + let forest = Rc::clone(&forest.0); + + Ok(future_to_promise(async move { + let size = file + .get_size(&forest, &store) + .await + .map_err(error("Cannot determine file size"))?; + + Ok(value!(size)) + })) + } + /// Gets the metadata of this file. pub fn metadata(&self) -> JsResult { JsMetadata(self.0.get_metadata()).try_into() diff --git a/wnfs-wasm/src/fs/public/file.rs b/wnfs-wasm/src/fs/public/file.rs index e675d9fd..df435b2e 100644 --- a/wnfs-wasm/src/fs/public/file.rs +++ b/wnfs-wasm/src/fs/public/file.rs @@ -95,6 +95,22 @@ impl PublicFile { self.read_at(value!(0).into(), None, store) } + /// Gets the exact content size without fetching all content blocks. + #[wasm_bindgen(js_name = "getSize")] + pub fn get_size(&self, store: BlockStore) -> JsResult { + let file = Rc::clone(&self.0); + let store = ForeignBlockStore(store); + + Ok(future_to_promise(async move { + let size = file + .get_size(&store) + .await + .map_err(error("Cannot determine file size"))?; + + Ok(value!(size)) + })) + } + /// Gets the metadata of this file. pub fn metadata(&self) -> JsResult { JsMetadata(self.0.get_metadata()).try_into() diff --git a/wnfs-wasm/tests/private.spec.ts b/wnfs-wasm/tests/private.spec.ts index 5261e478..4fcf0fa1 100644 --- a/wnfs-wasm/tests/private.spec.ts +++ b/wnfs-wasm/tests/private.spec.ts @@ -453,6 +453,31 @@ test.describe("PrivateFile", () => { expect(new Uint8Array(Object.values(content))).toEqual(new Uint8Array([2, 3, 4])); }); + test("getSize returns the exact content size", async ({ page }) => { + const size = await page.evaluate(async () => { + const { + wnfs: { PrivateFile, PrivateForest }, + mock: { MemoryBlockStore, Rng }, + } = await window.setup(); + + const rng = new Rng(); + const initialForest = new PrivateForest(rng); + const store = new MemoryBlockStore(); + var [file, forest] = await PrivateFile.withContent( + initialForest.emptyName(), + new Date(), + new Uint8Array(2 * 1024 * 1024), + initialForest, + store, + rng, + ); + + return await file.getSize(forest, store); + }); + + expect(size).toEqual(2 * 1024 * 1024); + }); + test("A PrivateDirectory has the correct metadata", async ({ page }) => { const result = await page.evaluate(async () => { const { diff --git a/wnfs-wasm/tests/public.spec.ts b/wnfs-wasm/tests/public.spec.ts index 02e4df67..77941019 100644 --- a/wnfs-wasm/tests/public.spec.ts +++ b/wnfs-wasm/tests/public.spec.ts @@ -29,9 +29,7 @@ test.describe("PublicDirectory", () => { expect(result).toBeDefined(); }); - test("lookupNode cannot fetch file not added to directory", async ({ - page, - }) => { + test("lookupNode cannot fetch file not added to directory", async ({ page }) => { const result = await page.evaluate(async () => { const { wnfs: { PublicDirectory }, @@ -65,18 +63,12 @@ test.describe("PublicDirectory", () => { ["pictures", "cats", "tabby.png"], sampleCID, time, - store + store, ); - let result0 = await rootDir.getNode( - ["pictures", "cats", "tabby.png"], - store - ); + let result0 = await rootDir.getNode(["pictures", "cats", "tabby.png"], store); - let result1 = await rootDir.getNode( - ["pictures", "dogs", "bingo.png"], - store - ); + let result1 = await rootDir.getNode(["pictures", "dogs", "bingo.png"], store); return [result0, result1]; }); @@ -102,7 +94,7 @@ test.describe("PublicDirectory", () => { ["pictures", "cats", "tabby.png"], sampleCID, time, - store + store, ); await rootDir.getNode(["pictures", "cats", "tabby.png"], store); @@ -130,7 +122,7 @@ test.describe("PublicDirectory", () => { ["pictures", "cats", "tabby.png"], sampleCID, time, - store + store, ); const result = await rootDir.ls(["pictures"], store); @@ -158,14 +150,14 @@ test.describe("PublicDirectory", () => { ["pictures", "dogs", "billie.jpeg"], sampleCID, time, - store + store, ); var { rootDir } = await rootDir.write( ["pictures", "cats", "tabby.png"], sampleCID, time, - store + store, ); var { rootDir } = await rootDir.rm(["pictures", "cats"], store); @@ -190,18 +182,13 @@ test.describe("PublicDirectory", () => { const store = new MemoryBlockStore(); const root = new PublicDirectory(time); - var { rootDir } = await root.write( - ["pictures", "cats", "luna.jpeg"], - sampleCID, - time, - store - ); + var { rootDir } = await root.write(["pictures", "cats", "luna.jpeg"], sampleCID, time, store); var { rootDir } = await rootDir.write( ["pictures", "cats", "tabby.png"], sampleCID, time, - store + store, ); var { rootDir } = await rootDir.mkdir(["images"], time, store); @@ -210,7 +197,7 @@ test.describe("PublicDirectory", () => { ["pictures", "cats"], ["images", "cats"], time, - store + store, ); const imagesContent = await rootDir.ls(["images"], store); @@ -236,28 +223,18 @@ test.describe("PublicDirectory", () => { const store = new MemoryBlockStore(); const root = new PublicDirectory(time); - var { rootDir } = await root.write( - ["pictures", "cats", "luna.jpeg"], - sampleCID, - time, - store - ); + var { rootDir } = await root.write(["pictures", "cats", "luna.jpeg"], sampleCID, time, store); var { rootDir } = await rootDir.write( ["pictures", "cats", "tabby.png"], sampleCID, time, - store + store, ); var { rootDir } = await rootDir.mkdir(["images"], time, store); - var { rootDir } = await rootDir.cp( - ["pictures", "cats"], - ["images", "cats"], - time, - store - ); + var { rootDir } = await rootDir.cp(["pictures", "cats"], ["images", "cats"], time, store); const imagesContent = await rootDir.ls(["images"], store); @@ -314,10 +291,7 @@ test.describe("PublicDirectory", () => { const readBack = await file2.getContent(store); const partialRead = await file2.readAt(7, 5, store); - return [ - new TextDecoder().decode(readBack), - new TextDecoder().decode(partialRead) - ]; + return [new TextDecoder().decode(readBack), new TextDecoder().decode(partialRead)]; }); expect(result[0]).toEqual("Hello, World!"); @@ -370,4 +344,25 @@ test.describe("PublicDirectory", () => { expect(result).not.toBeUndefined(); expect(result).toEqual("bafkr4ihkr4ld3m4gqkjf4reryxsy2s5tkbxprqkow6fin2iiyvreuzzab4"); }); + + test("A PublicFile has a content size", async ({ page }) => { + const result = await page.evaluate(async () => { + const { + wnfs: { PublicFile }, + mock: { MemoryBlockStore }, + } = await window.setup(); + + const store = new MemoryBlockStore(); + const time = new Date(); + const file = new PublicFile(time); + + const longString = "x".repeat(5 * 1024 * 1024); + const content = new TextEncoder().encode(longString); + const file2 = await file.setContent(time, content, store); + + return await file2.getSize(store); + }); + + expect(result).toEqual(5 * 1024 * 1024); + }); }); diff --git a/wnfs/src/private/file.rs b/wnfs/src/private/file.rs index 6920e5c8..4aa67f94 100644 --- a/wnfs/src/private/file.rs +++ b/wnfs/src/private/file.rs @@ -586,6 +586,55 @@ impl PrivateFile { Ok(self.prepare_next_revision()?.get_metadata_mut()) } + /// Gets the exact content size without fetching all content blocks. + /// + /// # Examples + /// + /// ``` + /// use anyhow::Result; + /// use chrono::Utc; + /// use rand_chacha::ChaCha12Rng; + /// use rand_core::SeedableRng; + /// use wnfs::{ + /// private::{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 ChaCha12Rng::from_entropy(); + /// let forest = &mut HamtForest::new_rsa_2048_rc(rng); + /// + /// let content = get_random_bytes::<324_568>(rng).to_vec(); + /// let file = PrivateFile::with_content( + /// &forest.empty_name(), + /// Utc::now(), + /// content.clone(), + /// forest, + /// store, + /// rng, + /// ) + /// .await?; + /// + /// let mut size = file.get_size(forest, store).await?; + /// + /// assert_eq!(content.len(), size); + /// + /// Ok(()) + /// } + /// ``` + pub async fn get_size( + &self, + forest: &impl PrivateForest, + store: &impl BlockStore, + ) -> Result { + match &self.content.content { + FileContent::Inline { data } => Ok(data.len()), + FileContent::External(forest_content) => forest_content.get_size(forest, store).await, + } + } + /// Gets the entire content of a file. /// /// # Examples @@ -1039,6 +1088,23 @@ impl PrivateForestContent { self.block_count * self.block_content_size } + /// Gets the exact size of the content. + pub async fn get_size( + &self, + forest: &impl PrivateForest, + store: &impl BlockStore, + ) -> Result { + let size_without_last_block = + std::cmp::max(0, self.block_count - 1) * self.block_content_size; + + let size_last_block = self + .read_at(size_without_last_block as u64, None, forest, store) + .await? + .len(); + + Ok(size_without_last_block + size_last_block) + } + /// Generates the labels for all of the content shard blocks. pub(crate) fn generate_shard_labels<'a>( key: &'a SnapshotKey, diff --git a/wnfs/src/public/file.rs b/wnfs/src/public/file.rs index b5f4108d..76b98fdb 100644 --- a/wnfs/src/public/file.rs +++ b/wnfs/src/public/file.rs @@ -309,6 +309,44 @@ impl PublicFile { } } + /// Gets the exact content size without fetching all content blocks. + /// + /// # Examples + /// + /// ``` + /// use anyhow::Result; + /// use rand_chacha::ChaCha12Rng; + /// use rand_core::SeedableRng; + /// use chrono::Utc; + /// use wnfs::{ + /// public::PublicFile, + /// common::{MemoryBlockStore, utils::get_random_bytes}, + /// }; + /// + /// #[async_std::main] + /// async fn main() -> Result<()> { + /// let store = &MemoryBlockStore::new(); + /// let rng = &mut ChaCha12Rng::from_entropy(); + /// let content = get_random_bytes::<324_568>(rng).to_vec(); + /// let file = PublicFile::with_content( + /// Utc::now(), + /// content.clone(), + /// store, + /// ) + /// .await?; + /// + /// let mut size = file.get_size(store).await?; + /// + /// assert_eq!(content.len(), size); + /// + /// Ok(()) + /// } + /// ``` + pub async fn get_size(&self, store: &impl BlockStore) -> Result { + let value = self.userland.resolve_value(store).await?; + Ok(value.filesize().unwrap_or(0) as usize) + } + /// Gets the entire content of a file. /// /// # Examples