Rust implementation of NextGraph, a Decentralized and local-first web 3.0 ecosystem https://nextgraph.org
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
nextgraph-rs/ng-repo/src/file.rs

1635 lines
54 KiB

// Copyright (c) 2022-2024 Niko Bonnieure, Par le Peuple, NextGraph.org developers
// All rights reserved.
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.
//! File and RandomAccessFile objects
use core::fmt;
use std::cmp::min;
use std::collections::HashMap;
use chacha20::cipher::{KeyIvInit, StreamCipher};
use chacha20::ChaCha20;
use zeroize::Zeroize;
use crate::errors::*;
use crate::log::*;
use crate::object::*;
use crate::store::*;
use crate::types::*;
/// File errors
#[derive(Debug, PartialEq)]
pub enum FileError {
/// Missing blocks
MissingBlocks(Vec<BlockId>),
/// Missing root key
MissingRootKey,
/// Invalid BlockId encountered in the tree
InvalidBlockId,
/// Too many or too few children of a block
InvalidChildren,
/// Number of keys does not match number of children of a block
InvalidKeys,
/// Invalid CommitHeader object content
InvalidHeader,
/// Error deserializing content of a block
BlockDeserializeError,
/// Error deserializing content of the RandomAccessFileMeta
MetaDeserializeError,
/// Files are immutable, you cannot modify them and this one was already saved once. Create a new File for your new data (and delete the old one if needed)
AlreadySaved,
/// File is too big
TooBig,
NotFound,
StorageError,
EndOfFile,
InvalidArgument,
NotAFile,
}
impl From<StorageError> for FileError {
fn from(e: StorageError) -> Self {
match e {
StorageError::NotFound => FileError::NotFound,
_ => FileError::StorageError,
}
}
}
impl From<ObjectParseError> for FileError {
fn from(e: ObjectParseError) -> Self {
match e {
_ => FileError::BlockDeserializeError,
}
}
}
trait ReadFile {
fn read(&self, pos: usize, size: usize) -> Result<Vec<u8>, FileError>;
}
/// A File in memory (read access only)
pub struct File<'a> {
internal: Box<dyn ReadFile + 'a>,
}
impl<'a> File<'a> {
pub fn open(
id: ObjectId,
key: SymKey,
storage: &'a Box<dyn RepoStore + Send + Sync + 'a>,
) -> Result<File<'a>, FileError> {
let root_block = storage.get(&id)?;
if root_block.children().len() == 2
&& *root_block.content().commit_header_obj() == CommitHeaderObject::RandomAccess
{
Ok(File {
internal: Box::new(RandomAccessFile::open(id, key, storage)?),
})
} else {
let obj = Object::load(id, Some(key), storage)?;
match obj.content_v0()? {
ObjectContentV0::SmallFile(small_file) => Ok(File {
internal: Box::new(small_file),
}),
_ => Err(FileError::NotAFile),
}
}
}
}
impl<'a> ReadFile for File<'a> {
fn read(&self, pos: usize, size: usize) -> Result<Vec<u8>, FileError> {
self.internal.read(pos, size)
}
}
impl ReadFile for SmallFile {
fn read(&self, pos: usize, size: usize) -> Result<Vec<u8>, FileError> {
match self {
Self::V0(v0) => v0.read(pos, size),
}
}
}
impl ReadFile for SmallFileV0 {
fn read(&self, pos: usize, size: usize) -> Result<Vec<u8>, FileError> {
if size == 0 {
return Err(FileError::InvalidArgument);
}
if pos + size > self.content.len() {
return Err(FileError::EndOfFile);
}
Ok(self.content[pos..pos + size].to_vec())
}
}
/// A RandomAccessFile in memory. This is not used to serialize data
pub struct RandomAccessFile<'a> {
//storage: Arc<&'a dyn RepoStore>,
storage: &'a Box<dyn RepoStore + Send + Sync + 'a>,
/// accurate once saved or opened
meta: RandomAccessFileMeta,
//meta_object_id: Option<BlockId>,
//content_block_id: Option<BlockId>,
/// keeps the deduplicated blocks' IDs, used for async writes
block_contents: HashMap<BlockKey, BlockId>,
/// Blocks of the Object (nodes of the tree). Only used when writing asynchronously, before saving.
blocks: Vec<(BlockId, BlockKey)>,
/// When an id is present, the File is opened in Read mode, and cannot be saved.
id: Option<ObjectId>,
key: Option<ObjectKey>,
content_block: Option<(BlockId, BlockKey)>,
// used for writes
conv_key: Option<[u8; 32]>,
remainder: Vec<u8>,
size: usize,
}
impl<'a> ReadFile for RandomAccessFile<'a> {
/// reads at most one block from the file. the returned vector should be tested for size. it might be smaller than what you asked for.
/// `pos`ition can be anywhere in the file.
//TODO: parallelize decryption on multi threads (cores)
fn read(&self, pos: usize, size: usize) -> Result<Vec<u8>, FileError> {
if size == 0 {
return Err(FileError::InvalidArgument);
}
if self.id.is_some() {
if pos + size > self.meta.total_size() as usize {
return Err(FileError::EndOfFile);
}
let mut current_block_id_key = self.content_block.as_ref().unwrap().clone();
let depth = self.meta.depth();
let arity = self.meta.arity();
let mut level_pos = pos;
for level in 0..depth {
let tree_block = self.storage.get(&current_block_id_key.0)?;
let (children, content) = tree_block.read(&current_block_id_key.1)?;
if children.len() == 0 || content.len() > 0 {
return Err(FileError::BlockDeserializeError);
}
let factor = (arity as usize).pow(depth as u32 - level as u32 - 1)
* self.meta.chunk_size() as usize;
let level_index = pos / factor;
if level_index >= children.len() {
return Err(FileError::EndOfFile);
}
current_block_id_key = (children[level_index]).clone();
level_pos = pos as usize % factor;
}
let content_block = self.storage.get(&current_block_id_key.0)?;
//log_debug!("CONTENT BLOCK SIZE {}", content_block.size());
let (children, content) = content_block.read(&current_block_id_key.1)?;
if children.len() == 0 && content.len() > 0 {
//log_debug!("CONTENT SIZE {}", content.len());
if level_pos >= content.len() {
return Err(FileError::EndOfFile);
}
let end = min(content.len(), level_pos + size);
return Ok(content[level_pos..end].to_vec());
} else {
return Err(FileError::BlockDeserializeError);
}
} else {
// hasn't been saved yet, we can use the self.blocks as a flat array and the remainder too
let factor = self.meta.chunk_size() as usize;
let index = pos / factor as usize;
let level_pos = pos % factor as usize;
let remainder_pos = self.blocks.len() * factor;
if pos >= remainder_pos {
let pos_in_remainder = pos - remainder_pos;
if self.remainder.len() > 0 && pos_in_remainder < self.remainder.len() {
let end = min(self.remainder.len(), pos_in_remainder + size);
return Ok(self.remainder[pos_in_remainder..end].to_vec());
} else {
return Err(FileError::EndOfFile);
}
}
//log_debug!("{} {} {} {}", index, self.blocks.len(), factor, level_pos);
if index >= self.blocks.len() {
return Err(FileError::EndOfFile);
}
let block = &self.blocks[index];
let content_block = self.storage.get(&block.0)?;
let (children, content) = content_block.read(&block.1)?;
if children.len() == 0 && content.len() > 0 {
//log_debug!("CONTENT SIZE {}", content.len());
if level_pos >= content.len() {
return Err(FileError::EndOfFile);
}
let end = min(content.len(), level_pos + size);
return Ok(content[level_pos..end].to_vec());
} else {
return Err(FileError::BlockDeserializeError);
}
}
}
}
impl<'a> RandomAccessFile<'a> {
pub fn meta(&self) -> &RandomAccessFileMeta {
&self.meta
}
pub fn id(&self) -> &Option<ObjectId> {
&self.id
}
pub fn key(&self) -> &Option<ObjectKey> {
&self.key
}
fn make_block(
mut content: Vec<u8>,
conv_key: &[u8; blake3::OUT_LEN],
children: Vec<ObjectId>,
already_existing: &mut HashMap<BlockKey, BlockId>,
storage: &Box<dyn RepoStore + Send + Sync + 'a>,
) -> Result<(BlockId, BlockKey), StorageError> {
let key_hash = blake3::keyed_hash(conv_key, &content);
let key_slice = key_hash.as_bytes();
let key = SymKey::ChaCha20Key(key_slice.clone());
let it = already_existing.get(&key);
if it.is_some() {
return Ok((*it.unwrap(), key));
}
let nonce = [0u8; 12];
let mut cipher = ChaCha20::new(key_slice.into(), &nonce.into());
//let mut content_enc = Vec::from(content);
let mut content_enc_slice = &mut content.as_mut_slice();
cipher.apply_keystream(&mut content_enc_slice);
let mut block = Block::new_random_access(children, content, None);
//log_debug!(">>> make_block random access: {}", block.id());
//log_debug!("!! children: ({}) {:?}", children.len(), children);
let id = block.get_and_save_id();
already_existing.insert(key.clone(), id);
//log_debug!("putting *** {}", id);
storage.put(&block)?;
Ok((id, key))
}
fn make_parent_block(
conv_key: &[u8; blake3::OUT_LEN],
children: Vec<(BlockId, BlockKey)>,
already_existing: &mut HashMap<BlockKey, BlockId>,
storage: &Box<dyn RepoStore + Send + Sync + 'a>,
) -> Result<(BlockId, BlockKey), StorageError> {
let mut ids: Vec<BlockId> = Vec::with_capacity(children.len());
let mut keys: Vec<BlockKey> = Vec::with_capacity(children.len());
children.iter().for_each(|child| {
ids.push(child.0);
keys.push(child.1.clone());
});
let content = ChunkContentV0::InternalNode(keys);
let content_ser = serde_bare::to_vec(&content).unwrap();
Self::make_block(content_ser, conv_key, ids, already_existing, storage)
}
/// Build tree from leaves, returns parent nodes
fn make_tree(
already_existing: &mut HashMap<BlockKey, BlockId>,
leaves: &[(BlockId, BlockKey)],
conv_key: &ChaCha20Key,
arity: u16,
storage: &'a Box<dyn RepoStore + Send + Sync + 'a>,
) -> Result<(BlockId, BlockKey), StorageError> {
let mut parents: Vec<(BlockId, BlockKey)> = vec![];
let mut chunks = leaves.chunks(arity as usize);
while let Some(nodes) = chunks.next() {
//log_debug!("making parent");
parents.push(Self::make_parent_block(
conv_key,
nodes.to_vec(),
already_existing,
storage,
)?);
}
//log_debug!("level with {} parents", parents.len());
if 1 < parents.len() {
return Self::make_tree(
already_existing,
parents.as_slice(),
conv_key,
arity,
storage,
);
}
Ok(parents[0].clone())
}
/// returns content_block id/key pair, and root_block id/key pair
fn save_(
already_existing: &mut HashMap<BlockKey, BlockId>,
blocks: &[(BlockId, BlockKey)],
meta: &mut RandomAccessFileMeta,
conv_key: &ChaCha20Key,
storage: &'a Box<dyn RepoStore + Send + Sync + 'a>,
) -> Result<((BlockId, BlockKey), (BlockId, BlockKey)), FileError> {
let leaf_blocks_nbr = blocks.len();
let arity = meta.arity();
let mut depth: u8 = u8::MAX;
for i in 0..u8::MAX {
if leaf_blocks_nbr <= (arity as usize).pow(i.into()) {
depth = i;
break;
}
}
if depth == u8::MAX {
return Err(FileError::TooBig);
}
meta.set_depth(depth);
//log_debug!("depth={} leaves={}", depth, leaf_blocks_nbr);
let content_block = if depth == 0 {
assert!(blocks.len() == 1);
blocks[0].clone()
} else {
// we create the tree
Self::make_tree(already_existing, &blocks, &conv_key, arity, storage)?
};
let meta_object = Object::new_with_convergence_key(
ObjectContent::V0(ObjectContentV0::RandomAccessFileMeta(meta.clone())),
None,
store_valid_value_size(meta.chunk_size() as usize),
conv_key,
);
//log_debug!("saving meta object");
_ = meta_object.save(storage)?;
// creating the root block that contains as first child the meta_object, and as second child the content_block
// it is added to storage in make_parent_block
//log_debug!("saving root block");
let root_block = Self::make_parent_block(
conv_key,
vec![
(meta_object.id(), meta_object.key().unwrap()),
content_block.clone(),
],
already_existing,
storage,
)?;
Ok((content_block, root_block))
}
/// Creates a new file based on a content that is fully known at the time of creation.
/// If you want to stream progressively the content into the new file, you should use new_empty(), write() and save() instead
pub fn new_from_slice(
content: &[u8],
block_size: usize,
content_type: String,
metadata: Vec<u8>,
store: &StoreRepo,
store_secret: &ReadCapSecret,
storage: &'a Box<dyn RepoStore + Send + Sync + 'a>,
) -> Result<RandomAccessFile<'a>, FileError> {
//let max_block_size = store_max_value_size();
let valid_block_size = store_valid_value_size(block_size) - BLOCK_EXTRA;
let arity = ((valid_block_size) / CHILD_SIZE) as u16;
let total_size = content.len() as u64;
let mut conv_key = Object::convergence_key(store, store_secret);
let mut blocks: Vec<(BlockId, BlockKey)> = vec![];
let mut already_existing: HashMap<BlockKey, BlockId> = HashMap::new();
//log_debug!("making the leaves");
for chunck in content.chunks(valid_block_size) {
let data_chunk = ChunkContentV0::DataChunk(chunck.to_vec());
let content_ser = serde_bare::to_vec(&data_chunk).unwrap();
blocks.push(Self::make_block(
content_ser,
&conv_key,
vec![],
&mut already_existing,
storage,
)?);
}
assert_eq!(
(total_size as usize + valid_block_size - 1) / valid_block_size,
blocks.len()
);
let mut meta = RandomAccessFileMeta::V0(RandomAccessFileMetaV0 {
content_type,
metadata,
chunk_size: valid_block_size as u32,
total_size,
arity,
depth: 0,
});
let (content_block, root_block) = Self::save_(
&mut already_existing,
&blocks,
&mut meta,
&conv_key,
storage,
)?;
conv_key.zeroize();
Ok(Self {
storage,
meta,
block_contents: HashMap::new(), // not used in this case
blocks: vec![], // not used in this case
id: Some(root_block.0.clone()),
key: Some(root_block.1.clone()),
content_block: Some(content_block),
conv_key: None, // not used in this case
remainder: vec![], // not used in this case
size: 0, // not used in this case
})
}
pub fn new_empty(
block_size: usize,
content_type: String,
metadata: Vec<u8>,
store: &StoreRepo,
store_secret: &ReadCapSecret,
storage: &'a Box<dyn RepoStore + Send + Sync + 'a>,
) -> Self {
let valid_block_size = store_valid_value_size(block_size) - BLOCK_EXTRA;
let arity = ((valid_block_size) / CHILD_SIZE) as u16;
let meta = RandomAccessFileMeta::V0(RandomAccessFileMetaV0 {
content_type,
metadata,
chunk_size: valid_block_size as u32,
arity,
total_size: 0, // will be filled in later, during save
depth: 0, // will be filled in later, during save
});
Self {
storage,
meta,
block_contents: HashMap::new(),
blocks: vec![],
id: None,
key: None,
content_block: None,
conv_key: Some(Object::convergence_key(store, store_secret)),
remainder: vec![],
size: 0,
}
}
/// Appends some data at the end of the file currently created with new_empty() and not saved yet.
/// you can call it many times. Don't forget to eventually call save()
pub fn write(&mut self, data: &[u8]) -> Result<(), FileError> {
if self.id.is_some() {
return Err(FileError::AlreadySaved);
}
let remainder = self.remainder.len();
let chunk_size = self.meta.chunk_size() as usize;
let mut pos: usize = 0;
let conv_key = self.conv_key.unwrap();
// TODO: provide an option to search in storage for already existing, when doing a resume of previously aborted write
let mut already_existing: HashMap<BlockKey, BlockId> = HashMap::new();
if remainder > 0 {
if data.len() >= chunk_size - remainder {
let mut new_block = Vec::with_capacity(chunk_size);
new_block.append(&mut self.remainder);
pos = chunk_size - remainder;
self.size += chunk_size;
//log_debug!("size += chunk_size {} {}", self.size, chunk_size);
new_block.extend(data[0..pos].iter());
assert_eq!(new_block.len(), chunk_size);
let data_chunk = ChunkContentV0::DataChunk(new_block);
let content_ser = serde_bare::to_vec(&data_chunk).unwrap();
self.blocks.push(Self::make_block(
content_ser,
&conv_key,
vec![],
&mut already_existing,
self.storage,
)?);
} else {
// not enough data to create a new block
self.remainder.extend(data.iter());
return Ok(());
}
} else if data.len() < chunk_size {
self.remainder = Vec::from(data);
return Ok(());
}
for chunck in data[pos..].chunks(chunk_size) {
if chunck.len() == chunk_size {
self.size += chunk_size;
//log_debug!("size += chunk_size {} {}", self.size, chunk_size);
let data_chunk = ChunkContentV0::DataChunk(chunck.to_vec());
let content_ser = serde_bare::to_vec(&data_chunk).unwrap();
self.blocks.push(Self::make_block(
content_ser,
&conv_key,
vec![],
&mut already_existing,
self.storage,
)?);
} else {
self.remainder = Vec::from(chunck);
return Ok(());
}
}
Ok(())
}
pub fn save(&mut self) -> Result<(), FileError> {
if self.id.is_some() {
return Err(FileError::AlreadySaved);
}
// save the remainder, if any.
if self.remainder.len() > 0 {
self.size += self.remainder.len();
//log_debug!("size += remainder {} {}", self.size, self.remainder.len());
let mut remainder = Vec::with_capacity(self.remainder.len());
remainder.append(&mut self.remainder);
let data_chunk = ChunkContentV0::DataChunk(remainder);
let content_ser = serde_bare::to_vec(&data_chunk).unwrap();
self.blocks.push(Self::make_block(
content_ser,
&self.conv_key.unwrap(),
vec![],
&mut HashMap::new(),
self.storage,
)?);
}
self.meta.set_total_size(self.size as u64);
let mut already_existing: HashMap<BlockKey, BlockId> = HashMap::new();
let (content_block, root_block) = Self::save_(
&mut already_existing,
&self.blocks,
&mut self.meta,
self.conv_key.as_ref().unwrap(),
self.storage,
)?;
self.conv_key.as_mut().unwrap().zeroize();
self.conv_key = None;
self.id = Some(root_block.0);
self.key = Some(root_block.1.clone());
self.content_block = Some(content_block);
self.blocks = vec![];
self.blocks.shrink_to_fit();
Ok(())
}
/// Opens a file for read purpose.
pub fn open(
id: ObjectId,
key: SymKey,
storage: &'a Box<dyn RepoStore + Send + Sync + 'a>,
) -> Result<RandomAccessFile<'a>, FileError> {
// load root block
let root_block = storage.get(&id)?;
if root_block.children().len() != 2
|| *root_block.content().commit_header_obj() != CommitHeaderObject::RandomAccess
{
return Err(FileError::BlockDeserializeError);
}
let (root_sub_blocks, _) = root_block.read(&key)?;
// load meta object (first one in root block)
let meta_object = Object::load(
root_sub_blocks[0].0,
Some(root_sub_blocks[0].1.clone()),
storage,
)?;
let meta = match meta_object.content_v0()? {
ObjectContentV0::RandomAccessFileMeta(meta) => meta,
_ => return Err(FileError::InvalidChildren),
};
Ok(RandomAccessFile {
storage,
meta,
block_contents: HashMap::new(), // not used in this case
blocks: vec![], // not used in this case
id: Some(id),
key: Some(key),
content_block: Some(root_sub_blocks[1].clone()),
conv_key: None,
remainder: vec![],
size: 0,
})
}
pub fn blocks(&self) -> impl Iterator<Item = Block> + '_ {
self.blocks
.iter()
.map(|key| self.storage.get(&key.0).unwrap())
}
/// Size once encoded, before deduplication. Only available before save()
pub fn size(&self) -> usize {
let mut total = 0;
self.blocks().for_each(|b| total += b.size());
total
}
/// Real size on disk
pub fn dedup_size(&self) -> usize {
let mut total = 0;
self.block_contents
.values()
.for_each(|b| total += self.storage.get(b).unwrap().size());
total
}
pub fn depth(&self) -> Result<u8, NgError> {
Ok(self.meta.depth())
// unimplemented!();
// if self.key().is_none() {
// return Err(ObjectParseError::MissingRootKey);
// }
// let parents = vec![(self.id(), self.key().unwrap())];
// Self::collect_leaves(
// &self.blocks,
// &parents,
// self.blocks.len() - 1,
// &mut None,
// &mut None,
// &self.block_contents,
// )
}
}
impl fmt::Display for RandomAccessFile<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(
f,
"====== File ID {}",
self.id
.map_or("NOT SAVED".to_string(), |i| format!("{}", i))
)?;
writeln!(
f,
"== Key: {}",
self.key
.as_ref()
.map_or("None".to_string(), |k| format!("{}", k))
)?;
writeln!(f, "== depth: {}", self.meta.depth())?;
writeln!(f, "== arity: {}", self.meta.arity())?;
writeln!(f, "== chunk_size: {}", self.meta.chunk_size())?;
writeln!(f, "== total_size: {}", self.meta.total_size())?;
writeln!(f, "== content_type: {}", self.meta.content_type())?;
writeln!(f, "== metadata len: {}", self.meta.metadata().len())?;
if self.id.is_none() {
writeln!(f, "== blocks to save: {}", self.blocks.len())?;
}
Ok(())
}
}
#[cfg(test)]
mod test {
use time::Instant;
use crate::file::*;
use std::io::BufReader;
use std::io::Read;
struct Test<'a> {
storage: Box<dyn RepoStore + Send + Sync + 'a>,
}
impl<'a> Test<'a> {
fn storage(s: impl RepoStore + 'a) -> Self {
Test {
storage: Box::new(s),
}
}
fn s(&self) -> &Box<dyn RepoStore + Send + Sync + 'a> {
&self.storage
}
}
/// Checks that a content that does fit in one block, creates an arity of 0
#[test]
pub fn test_depth_0() {
let block_size = store_max_value_size();
//store_valid_value_size(0)
let hashmap_storage = HashMapRepoStore::new();
let t = Test::storage(hashmap_storage);
//let storage: Arc<&dyn RepoStore> = Arc::new(&hashmap_storage);
////// 1 MB of data!
let data_size = block_size - BLOCK_EXTRA;
let (store_repo, store_secret) = StoreRepo::dummy_public_v0();
log_debug!("creating 1MB of data");
let content: Vec<u8> = vec![99; data_size];
log_debug!("creating random access file with that data");
let file: RandomAccessFile = RandomAccessFile::new_from_slice(
&content,
block_size,
"text/plain".to_string(),
vec![],
&store_repo,
&store_secret,
t.s(),
)
.expect("new_from_slice");
log_debug!("{}", file);
let id = file.id.as_ref().unwrap().clone();
let file_size = file.size();
log_debug!("file size to save : {}", file_size);
log_debug!("data size: {}", data_size);
let read_content = file.read(0, data_size).expect("reading all");
assert_eq!(read_content, content);
let read_content2 = file.read(0, data_size + 1);
assert_eq!(read_content2, Err(FileError::EndOfFile));
let read_content = file.read(data_size - 9, 9).expect("reading end");
assert_eq!(read_content, vec![99, 99, 99, 99, 99, 99, 99, 99, 99]);
let read_content = file.read(data_size - 9, 10);
assert_eq!(read_content, Err(FileError::EndOfFile));
// log_debug!(
// "overhead: {} - {}%",
// file_size - data_size,
// ((file_size - data_size) * 100) as f32 / data_size as f32
// );
// let dedup_size = file.dedup_size();
// log_debug!(
// "dedup compression: {} - {}%",
// data_size - dedup_size,
// ((data_size - dedup_size) * 100) as f32 / data_size as f32
// );
// log_debug!("number of blocks : {}", file.blocks.len());
// assert_eq!(
// file.blocks.len(),
// MAX_ARITY_LEAVES * (MAX_ARITY_LEAVES + 1) * MAX_ARITY_LEAVES + MAX_ARITY_LEAVES + 1
// );
assert_eq!(file.depth(), Ok(0));
assert_eq!(t.s().len(), Ok(3));
let file = RandomAccessFile::open(id, file.key.unwrap(), t.s()).expect("re open");
log_debug!("{}", file);
let read_content = file.read(0, data_size).expect("reading all after re open");
assert_eq!(read_content, content);
}
/// Checks that a content that doesn't fit in all the children of first level in tree
#[test]
pub fn test_depth_1() {
const MAX_ARITY_LEAVES: usize = 15887;
const MAX_DATA_PAYLOAD_SIZE: usize = 1048564;
let hashmap_storage = HashMapRepoStore::new();
let t = Test::storage(hashmap_storage);
////// 16 GB of data!
let data_size = MAX_ARITY_LEAVES * MAX_DATA_PAYLOAD_SIZE;
let (store_repo, store_secret) = StoreRepo::dummy_public_v0();
log_debug!("creating 16GB of data");
let content: Vec<u8> = vec![99; data_size];
log_debug!("creating random access file with that data");
let file: RandomAccessFile = RandomAccessFile::new_from_slice(
&content,
store_max_value_size(),
"text/plain".to_string(),
vec![],
&store_repo,
&store_secret,
t.s(),
)
.expect("new_from_slice");
log_debug!("{}", file);
let _id = file.id.as_ref().unwrap().clone();
log_debug!("data size: {}", data_size);
assert_eq!(file.depth(), Ok(1));
assert_eq!(t.s().len(), Ok(4));
}
/// Checks that a content that doesn't fit in all the children of first level in tree
#[test]
pub fn test_depth_2() {
const MAX_ARITY_LEAVES: usize = 15887;
const MAX_DATA_PAYLOAD_SIZE: usize = 1048564;
let hashmap_storage = HashMapRepoStore::new();
let t = Test::storage(hashmap_storage);
////// 16 GB of data!
let data_size = MAX_ARITY_LEAVES * MAX_DATA_PAYLOAD_SIZE + 1;
let (store_repo, store_secret) = StoreRepo::dummy_public_v0();
log_debug!("creating 16GB of data");
let content: Vec<u8> = vec![99; data_size];
log_debug!("creating file with that data");
let file: RandomAccessFile = RandomAccessFile::new_from_slice(
&content,
store_max_value_size(),
"text/plain".to_string(),
vec![],
&store_repo,
&store_secret,
t.s(),
)
.expect("new_from_slice");
log_debug!("{}", file);
let file_size = file.size();
log_debug!("file size: {}", file_size);
log_debug!("data size: {}", data_size);
assert_eq!(file.depth().unwrap(), 2);
assert_eq!(t.s().len(), Ok(7));
}
/// Checks that a content that doesn't fit in all the children of first level in tree
#[test]
pub fn test_depth_3() {
const MAX_ARITY_LEAVES: usize = 61;
const MAX_DATA_PAYLOAD_SIZE: usize = 4084;
let hashmap_storage = HashMapRepoStore::new();
let t = Test::storage(hashmap_storage);
////// 900 MB of data!
let data_size =
MAX_ARITY_LEAVES * MAX_ARITY_LEAVES * MAX_ARITY_LEAVES * MAX_DATA_PAYLOAD_SIZE;
let (store_repo, store_secret) = StoreRepo::dummy_public_v0();
log_debug!("creating 900MB of data");
let content: Vec<u8> = vec![99; data_size];
log_debug!("creating file with that data");
let file: RandomAccessFile = RandomAccessFile::new_from_slice(
&content,
store_valid_value_size(0),
"text/plain".to_string(),
vec![],
&store_repo,
&store_secret,
t.s(),
)
.expect("new_from_slice");
log_debug!("{}", file);
let file_size = file.size();
log_debug!("file size: {}", file_size);
let read_content = file.read(0, data_size).expect("reading all");
assert_eq!(read_content.len(), MAX_DATA_PAYLOAD_SIZE);
let read_content = file.read(9000, 10000).expect("reading 10k");
assert_eq!(read_content, vec![99; 3252]);
// log_debug!("data size: {}", data_size);
// log_debug!(
// "overhead: {} - {}%",
// file_size - data_size,
// ((file_size - data_size) * 100) as f32 / data_size as f32
// );
// let dedup_size = file.dedup_size();
// log_debug!(
// "dedup compression: {} - {}%",
// data_size - dedup_size,
// ((data_size - dedup_size) * 100) as f32 / data_size as f32
// );
// log_debug!("number of blocks : {}", file.blocks.len());
// assert_eq!(
// file.blocks.len(),
// MAX_ARITY_LEAVES * (MAX_ARITY_LEAVES + 1) * MAX_ARITY_LEAVES + MAX_ARITY_LEAVES + 1
// );
assert_eq!(file.depth().unwrap(), 3);
assert_eq!(t.s().len(), Ok(6));
}
/// Checks that a content that doesn't fit in all the children of first level in tree
#[test]
pub fn test_depth_4() {
const MAX_ARITY_LEAVES: usize = 61;
const MAX_DATA_PAYLOAD_SIZE: usize = 4084;
////// 52GB of data!
let data_size = MAX_ARITY_LEAVES
* MAX_ARITY_LEAVES
* MAX_ARITY_LEAVES
* MAX_ARITY_LEAVES
* MAX_DATA_PAYLOAD_SIZE;
let hashmap_storage = HashMapRepoStore::new();
let t = Test::storage(hashmap_storage);
let (store_repo, store_secret) = StoreRepo::dummy_public_v0();
log_debug!("creating 55GB of data");
let content: Vec<u8> = vec![99; data_size];
log_debug!("creating file with that data");
let file: RandomAccessFile = RandomAccessFile::new_from_slice(
&content,
store_valid_value_size(0),
"text/plain".to_string(),
vec![],
&store_repo,
&store_secret,
t.s(),
)
.expect("new_from_slice");
log_debug!("{}", file);
let file_size = file.size();
log_debug!("file size: {}", file_size);
log_debug!("data size: {}", data_size);
assert_eq!(file.depth().unwrap(), 4);
assert_eq!(t.s().len(), Ok(7));
}
/// Test async write to a file all at once
#[test]
pub fn test_write_all_at_once() {
let f = std::fs::File::open("tests/test.jpg").expect("open of tests/test.jpg");
let mut reader = BufReader::new(f);
let mut img_buffer: Vec<u8> = Vec::new();
reader
.read_to_end(&mut img_buffer)
.expect("read of test.jpg");
let hashmap_storage = HashMapRepoStore::new();
let t = Test::storage(hashmap_storage);
let (store_repo, store_secret) = StoreRepo::dummy_public_v0();
log_debug!("creating file with the JPG content");
let mut file: RandomAccessFile = RandomAccessFile::new_empty(
store_max_value_size(), //store_valid_value_size(0),//
"image/jpeg".to_string(),
vec![],
&store_repo,
&store_secret,
t.s(),
);
log_debug!("{}", file);
file.write(&img_buffer).expect("write all at once");
// !!! all those tests work only because store_max_value_size() is bigger than the actual size of the JPEG file. so it fits in one block.
assert_eq!(
file.read(0, img_buffer.len()).expect("read before save"),
img_buffer
);
// asking too much, receiving just enough
assert_eq!(
file.read(0, img_buffer.len() + 1)
.expect("read before save"),
img_buffer
);
// reading too far, well behind the size of the JPG
assert_eq!(file.read(100000, 1), Err(FileError::EndOfFile));
assert_eq!(file.read(10000, 1).expect("read before save"), vec![41]);
// reading one byte after the end of the file size.
assert_eq!(file.read(29454, 1), Err(FileError::EndOfFile));
assert_eq!(file.read(29454, 0), Err(FileError::InvalidArgument));
file.save().expect("save");
let res = file.read(0, img_buffer.len()).expect("read all");
assert_eq!(res, img_buffer);
// asking too much, receiving an error, as now we know the total size of file, and we check it
assert_eq!(
file.read(0, img_buffer.len() + 1),
Err(FileError::EndOfFile)
);
// reading too far, well behind the size of the JPG
assert_eq!(file.read(100000, 1), Err(FileError::EndOfFile));
assert_eq!(file.read(10000, 1).expect("read after save"), vec![41]);
// reading one byte after the end of the file size.
assert_eq!(file.read(29454, 1), Err(FileError::EndOfFile));
assert_eq!(file.read(29454, 0), Err(FileError::InvalidArgument));
}
/// Test async write to a file by increments
#[test]
pub fn test_write_by_increments() {
let f = std::fs::File::open("tests/test.jpg").expect("open of tests/test.jpg");
let mut reader = BufReader::new(f);
let mut img_buffer: Vec<u8> = Vec::new();
reader
.read_to_end(&mut img_buffer)
.expect("read of test.jpg");
let hashmap_storage = HashMapRepoStore::new();
let t = Test::storage(hashmap_storage);
let (store_repo, store_secret) = StoreRepo::dummy_public_v0();
log_debug!("creating file with the JPG content");
let mut file: RandomAccessFile = RandomAccessFile::new_empty(
store_max_value_size(), //store_valid_value_size(0),//
"image/jpeg".to_string(),
vec![],
&store_repo,
&store_secret,
t.s(),
);
log_debug!("{}", file);
for chunk in img_buffer.chunks(1000) {
file.write(chunk).expect("write a chunk");
}
assert_eq!(
file.read(0, img_buffer.len()).expect("read before save"),
img_buffer
);
// asking too much, receiving just enough
assert_eq!(
file.read(0, img_buffer.len() + 1)
.expect("read before save"),
img_buffer
);
// reading too far, well behind the size of the JPG
assert_eq!(file.read(100000, 1), Err(FileError::EndOfFile));
assert_eq!(file.read(10000, 1).expect("read before save"), vec![41]);
// reading one byte after the end of the file size.
assert_eq!(file.read(29454, 1), Err(FileError::EndOfFile));
assert_eq!(file.read(29454, 0), Err(FileError::InvalidArgument));
file.save().expect("save");
// this works only because store_max_value_size() is bigger than the actual size of the JPEG file. so it fits in one block.
let res = file.read(0, img_buffer.len()).expect("read all");
assert_eq!(res, img_buffer);
// asking too much, receiving an error, as now we know the total size of file, and we check it
assert_eq!(
file.read(0, img_buffer.len() + 1),
Err(FileError::EndOfFile)
);
// reading too far, well behind the size of the JPG
assert_eq!(file.read(100000, 1), Err(FileError::EndOfFile));
assert_eq!(file.read(10000, 1).expect("read after save"), vec![41]);
// reading one byte after the end of the file size.
assert_eq!(file.read(29454, 1), Err(FileError::EndOfFile));
assert_eq!(file.read(29454, 0), Err(FileError::InvalidArgument));
}
/// Test async write to a file by increments small blocks
#[test]
pub fn test_write_by_increments_small_blocks() {
let f = std::fs::File::open("tests/test.jpg").expect("open of tests/test.jpg");
let mut reader = BufReader::new(f);
let mut img_buffer: Vec<u8> = Vec::new();
reader
.read_to_end(&mut img_buffer)
.expect("read of test.jpg");
let hashmap_storage = HashMapRepoStore::new();
let t = Test::storage(hashmap_storage);
let (store_repo, store_secret) = StoreRepo::dummy_public_v0();
log_debug!("creating file with the JPG content");
let mut file: RandomAccessFile = RandomAccessFile::new_empty(
store_valid_value_size(0),
"image/jpeg".to_string(),
vec![],
&store_repo,
&store_secret,
t.s(),
);
log_debug!("{}", file);
let first_block_content = img_buffer[0..4084].to_vec();
for chunk in img_buffer.chunks(1000) {
file.write(chunk).expect("write a chunk");
}
log_debug!("{}", file);
assert_eq!(
file.read(0, img_buffer.len()).expect("read before save"),
first_block_content
);
// asking too much, receiving just enough
assert_eq!(
file.read(0, img_buffer.len() + 1)
.expect("read before save"),
first_block_content
);
// reading too far, well behind the size of the JPG
assert_eq!(file.read(100000, 1), Err(FileError::EndOfFile));
assert_eq!(file.read(10000, 1).expect("read before save"), vec![41]);
// reading one byte after the end of the file size.
assert_eq!(file.read(29454, 1), Err(FileError::EndOfFile));
assert_eq!(file.read(29454, 0), Err(FileError::InvalidArgument));
file.save().expect("save");
log_debug!("{}", file);
assert_eq!(img_buffer.len(), file.meta.total_size() as usize);
let res = file.read(0, img_buffer.len()).expect("read all");
assert_eq!(res, first_block_content);
// asking too much, receiving an error, as now we know the total size of file, and we check it
assert_eq!(
file.read(0, img_buffer.len() + 1),
Err(FileError::EndOfFile)
);
// reading too far, well behind the size of the JPG
assert_eq!(file.read(100000, 1), Err(FileError::EndOfFile));
assert_eq!(file.read(10000, 1).expect("read after save"), vec![41]);
// reading one byte after the end of the file size.
assert_eq!(file.read(29454, 1), Err(FileError::EndOfFile));
assert_eq!(file.read(29454, 0), Err(FileError::InvalidArgument));
}
/// Test async write to a file all at once
#[test]
pub fn test_write_all_at_once_small_blocks() {
let f = std::fs::File::open("tests/test.jpg").expect("open of tests/test.jpg");
let mut reader = BufReader::new(f);
let mut img_buffer: Vec<u8> = Vec::new();
reader
.read_to_end(&mut img_buffer)
.expect("read of test.jpg");
let first_block_content = img_buffer[0..4084].to_vec();
let hashmap_storage = HashMapRepoStore::new();
let t = Test::storage(hashmap_storage);
let (store_repo, store_secret) = StoreRepo::dummy_public_v0();
log_debug!("creating file with the JPG content");
let mut file: RandomAccessFile = RandomAccessFile::new_empty(
store_valid_value_size(0),
"image/jpeg".to_string(),
vec![],
&store_repo,
&store_secret,
t.s(),
);
log_debug!("{}", file);
file.write(&img_buffer).expect("write all at once");
assert_eq!(
file.read(0, img_buffer.len()).expect("read before save"),
first_block_content
);
// asking too much, receiving just enough
assert_eq!(
file.read(0, img_buffer.len() + 1)
.expect("read before save"),
first_block_content
);
// reading too far, well behind the size of the JPG
assert_eq!(file.read(100000, 1), Err(FileError::EndOfFile));
assert_eq!(file.read(10000, 1).expect("read before save"), vec![41]);
// reading one byte after the end of the file size.
assert_eq!(file.read(29454, 1), Err(FileError::EndOfFile));
assert_eq!(file.read(29454, 0), Err(FileError::InvalidArgument));
file.save().expect("save");
let res = file.read(0, img_buffer.len()).expect("read all");
assert_eq!(res, first_block_content);
let res = file.read(10, img_buffer.len() - 10).expect("read all");
assert_eq!(res, first_block_content[10..].to_vec());
// asking too much, receiving an error, as now we know the total size of file, and we check it
assert_eq!(
file.read(0, img_buffer.len() + 1),
Err(FileError::EndOfFile)
);
// reading too far, well behind the size of the JPG
assert_eq!(file.read(100000, 1), Err(FileError::EndOfFile));
assert_eq!(file.read(10000, 1).expect("read after save"), vec![41]);
// reading one byte after the end of the file size.
assert_eq!(file.read(29454, 1), Err(FileError::EndOfFile));
assert_eq!(file.read(29454, 0), Err(FileError::InvalidArgument));
}
/// Test depth 4 with 52GB of data, but using write in small increments, so the memory burden on the system will be minimal
#[test]
pub fn test_depth_4_write_small() {
const MAX_ARITY_LEAVES: usize = 61;
const MAX_DATA_PAYLOAD_SIZE: usize = 4084;
////// 52GB of data!
let data_size = MAX_ARITY_LEAVES
* MAX_ARITY_LEAVES
* MAX_ARITY_LEAVES
* MAX_ARITY_LEAVES
* MAX_DATA_PAYLOAD_SIZE;
// chunks of 5 MB
let chunk_nbr = data_size / 5000000;
let last_chunk = data_size % 5000000;
let hashmap_storage = HashMapRepoStore::new();
let t = Test::storage(hashmap_storage);
let (store_repo, store_secret) = StoreRepo::dummy_public_v0();
log_debug!("creating empty file");
let mut file: RandomAccessFile = RandomAccessFile::new_empty(
store_valid_value_size(0),
"image/jpeg".to_string(),
vec![],
&store_repo,
&store_secret,
t.s(),
);
log_debug!("{}", file);
let chunk = vec![99; 5000000];
let last_chunk = vec![99; last_chunk];
for _i in 0..chunk_nbr {
file.write(&chunk).expect("write a chunk");
}
file.write(&last_chunk).expect("write last chunk");
log_debug!("{}", file);
file.save().expect("save");
log_debug!("{}", file);
let file_size = file.size();
log_debug!("file size: {}", file_size);
log_debug!("data size: {}", data_size);
assert_eq!(data_size, file.meta.total_size() as usize);
assert_eq!(file.depth().unwrap(), 4);
assert_eq!(t.s().len(), Ok(7));
}
/// Test open
#[test]
pub fn test_open() {
let f = std::fs::File::open("tests/test.jpg").expect("open of tests/test.jpg");
let mut reader = BufReader::new(f);
let mut img_buffer: Vec<u8> = Vec::new();
reader
.read_to_end(&mut img_buffer)
.expect("read of test.jpg");
let hashmap_storage = HashMapRepoStore::new();
let t = Test::storage(hashmap_storage);
let (store_repo, store_secret) = StoreRepo::dummy_public_v0();
log_debug!("creating file with the JPG content");
let mut file: RandomAccessFile = RandomAccessFile::new_empty(
store_max_value_size(), //store_valid_value_size(0),//
"image/jpeg".to_string(),
vec![],
&store_repo,
&store_secret,
t.s(),
);
log_debug!("{}", file);
for chunk in img_buffer.chunks(1000) {
file.write(chunk).expect("write a chunk");
}
file.save().expect("save");
let file2 = RandomAccessFile::open(file.id().unwrap(), file.key.unwrap(), t.s())
.expect("reopen file");
// this works only because store_max_value_size() is bigger than the actual size of the JPEG file. so it fits in one block.
let res = file2.read(0, img_buffer.len()).expect("read all");
log_debug!("{}", file2);
assert_eq!(res, img_buffer);
// asking too much, receiving an error, as now we know the total size of file, and we check it
assert_eq!(
file2.read(0, img_buffer.len() + 1),
Err(FileError::EndOfFile)
);
// reading too far, well behind the size of the JPG
assert_eq!(file2.read(100000, 1), Err(FileError::EndOfFile));
assert_eq!(file2.read(10000, 1).expect("read after save"), vec![41]);
// reading one byte after the end of the file size.
assert_eq!(file2.read(29454, 1), Err(FileError::EndOfFile));
assert_eq!(file2.read(29454, 0), Err(FileError::InvalidArgument));
}
/// Test read JPEG file small
#[test]
pub fn test_read_small_file() {
let f = std::fs::File::open("tests/test.jpg").expect("open of tests/test.jpg");
let mut reader = BufReader::new(f);
let mut img_buffer: Vec<u8> = Vec::new();
reader
.read_to_end(&mut img_buffer)
.expect("read of test.jpg");
let len = img_buffer.len();
let content = ObjectContent::new_file_v0_with_content(img_buffer.clone(), "image/jpeg");
let max_object_size = store_max_value_size();
let (store_repo, store_secret) = StoreRepo::dummy_public_v0();
let mut obj = Object::new(content, None, max_object_size, &store_repo, &store_secret);
log_debug!("{}", obj);
let hashmap_storage = HashMapRepoStore::new();
let t = Test::storage(hashmap_storage);
let _ = obj.save_in_test(t.s()).expect("save");
let file = File::open(obj.id(), obj.key().unwrap(), t.s()).expect("open");
let res = file.read(0, len).expect("read all");
assert_eq!(res, img_buffer);
}
/// Test read JPEG file random access
#[test]
pub fn test_read_random_access_file() {
let f = std::fs::File::open("tests/test.jpg").expect("open of tests/test.jpg");
let mut reader = BufReader::new(f);
let mut img_buffer: Vec<u8> = Vec::new();
reader
.read_to_end(&mut img_buffer)
.expect("read of test.jpg");
let len = img_buffer.len();
let max_object_size = store_max_value_size();
let (store_repo, store_secret) = StoreRepo::dummy_public_v0();
let hashmap_storage = HashMapRepoStore::new();
let t = Test::storage(hashmap_storage);
log_debug!("creating empty file");
let mut file: RandomAccessFile = RandomAccessFile::new_empty(
max_object_size,
"image/jpeg".to_string(),
vec![],
&store_repo,
&store_secret,
t.s(),
);
file.write(&img_buffer).expect("write all");
log_debug!("{}", file);
file.save().expect("save");
log_debug!("{}", file);
let file = File::open(
file.id().unwrap(),
file.key().as_ref().unwrap().clone(),
t.s(),
)
.expect("open");
// this only works because we chose a big block size (1MB) so the small JPG file files in one block.
// if not, we would have to call read repeatedly and append the results into a buffer, in order to get the full file
let res = file.read(0, len).expect("read all");
assert_eq!(res, img_buffer);
}
/// Test depth 4, but using write in increments, so the memory burden on the system will be minimal
#[test]
pub fn test_depth_4_big_write_small() {
let encoding_big_file = Instant::now();
let f = std::fs::File::open("[enter path of a big file here]").expect("open of a big file");
let mut reader = BufReader::new(f);
let hashmap_storage = HashMapRepoStore::new();
let t = Test::storage(hashmap_storage);
let (store_repo, store_secret) = StoreRepo::dummy_public_v0();
log_debug!("creating empty file");
let mut file: RandomAccessFile = RandomAccessFile::new_empty(
store_valid_value_size(0),
"image/jpeg".to_string(),
vec![],
&store_repo,
&store_secret,
t.s(),
);
log_debug!("{}", file);
let mut chunk = [0u8; 1000000];
loop {
let size = reader.read(&mut chunk).expect("read a chunk");
//log_debug!("{}", size);
file.write(&chunk[0..size]).expect("write a chunk");
if size != 1000000 {
break;
}
}
log_debug!("{}", file);
file.save().expect("save");
log_debug!("{}", file);
log_debug!("data size: {}", file.meta.total_size());
//assert_eq!(data_size, file.meta.total_size() as usize);
assert_eq!(file.depth().unwrap(), 4);
log_debug!(
"encoding_big_file took: {} s",
encoding_big_file.elapsed().as_seconds_f32()
);
}
/// Test depth 4 with 2.7GB of data, but using write in increments, so the memory burden on the system will be minimal
#[test]
pub fn test_depth_4_big_write_big() {
let encoding_big_file = Instant::now();
let f = std::fs::File::open("[enter path of a big file here]").expect("open of a big file");
let mut reader = BufReader::new(f);
let hashmap_storage = HashMapRepoStore::new();
let t = Test::storage(hashmap_storage);
let (store_repo, store_secret) = StoreRepo::dummy_public_v0();
log_debug!("creating empty file");
let mut file: RandomAccessFile = RandomAccessFile::new_empty(
store_max_value_size(),
"image/jpeg".to_string(),
vec![],
&store_repo,
&store_secret,
t.s(),
);
log_debug!("{}", file);
let mut chunk = [0u8; 2000000];
loop {
let size = reader.read(&mut chunk).expect("read a chunk");
//log_debug!("{}", size);
file.write(&chunk[0..size]).expect("write a chunk");
if size != 2000000 {
break;
}
}
log_debug!("{}", file);
file.save().expect("save");
log_debug!("{}", file);
log_debug!("data size: {}", file.meta.total_size());
//assert_eq!(data_size, file.meta.total_size() as usize);
assert_eq!(file.depth().unwrap(), 1);
log_debug!(
"encoding_big_file took: {} s",
encoding_big_file.elapsed().as_seconds_f32()
);
}
}