SimpleDb supports reading & writing object.
This commit is contained in:
@@ -40,12 +40,17 @@ error_chain! {
|
|||||||
|
|
||||||
InvalidObjectIdCharacter {
|
InvalidObjectIdCharacter {
|
||||||
description("Object id contains invalid character")
|
description("Object id contains invalid character")
|
||||||
display("Object id contains invalid character")
|
display("Object id contains invalid character.")
|
||||||
}
|
}
|
||||||
|
|
||||||
InvalidObjectType(otype: [u8; 4]) {
|
InvalidObjectType(otype: [u8; 4]) {
|
||||||
description("Object has an invalid object type")
|
description("Object has an invalid object type")
|
||||||
display("Object has an invalid object type {:?}", otype)
|
display("Object has an invalid object type {:?}.", otype)
|
||||||
|
}
|
||||||
|
|
||||||
|
MismatchingObjectSize(actual: u64, expected: u64) {
|
||||||
|
description("Mismatching object size")
|
||||||
|
display("Mismatching object size: expected {}, got {}.", actual, expected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ pub const NAME: &'static str = env!("CARGO_PKG_NAME");
|
|||||||
pub const VERSION: &'static str = env!("CARGO_PKG_VERSION");
|
pub const VERSION: &'static str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
|
|
||||||
pub use error::Error;
|
pub use error::{Error, Result, ErrorKind};
|
||||||
pub use config::{Config, RepositoryConfig};
|
pub use config::{Config, RepositoryConfig};
|
||||||
pub use object_id::ObjectId;
|
pub use object_id::ObjectId;
|
||||||
pub use repository::Repository;
|
pub use repository::Repository;
|
||||||
|
|||||||
@@ -23,10 +23,8 @@ pub mod simple_db;
|
|||||||
|
|
||||||
|
|
||||||
pub use crate::core::{
|
pub use crate::core::{
|
||||||
Error,
|
Error, Result, ErrorKind,
|
||||||
Config, RepositoryConfig,
|
Config, RepositoryConfig,
|
||||||
ObjectId,
|
ObjectId,
|
||||||
Repository,
|
Repository,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use crate::simple_db::SimpleDb;
|
|
||||||
|
|||||||
@@ -19,3 +19,7 @@ pub mod object;
|
|||||||
|
|
||||||
|
|
||||||
pub use simple_db::SimpleDb;
|
pub use simple_db::SimpleDb;
|
||||||
|
pub use object::{
|
||||||
|
OTYPE_BLOB, OTYPE_TREE,
|
||||||
|
ObjectReader,
|
||||||
|
};
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ extern crate digest;
|
|||||||
extern crate sha2;
|
extern crate sha2;
|
||||||
extern crate flate2;
|
extern crate flate2;
|
||||||
|
|
||||||
use std::io::{Read, Write, copy};
|
use std::io::{Read, Seek, SeekFrom, Write, copy, sink};
|
||||||
|
|
||||||
use digest::Digest;
|
use digest::Digest;
|
||||||
|
|
||||||
@@ -32,15 +32,17 @@ use crate::core::error::*;
|
|||||||
use crate::core::ObjectId;
|
use crate::core::ObjectId;
|
||||||
|
|
||||||
|
|
||||||
type ObjectType = [u8; 4];
|
pub type ObjectType = [u8; 4];
|
||||||
|
|
||||||
pub const TYPE_BLOB: &ObjectType = b"blob";
|
pub const OTYPE_BLOB: &ObjectType = b"blob";
|
||||||
pub const TYPE_TREE: &ObjectType = b"tree";
|
pub const OTYPE_TREE: &ObjectType = b"tree";
|
||||||
|
|
||||||
|
|
||||||
pub struct ObjectWriter<W: Write, D: Digest> {
|
pub struct ObjectWriter<W: Write, D: Digest> {
|
||||||
writer: GzEncoder<W>,
|
writer: GzEncoder<W>,
|
||||||
digest: D,
|
digest: D,
|
||||||
|
size: u64,
|
||||||
|
written_size: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -61,16 +63,24 @@ impl<W: Write, D: Digest> ObjectWriter<W, D> {
|
|||||||
Ok(ObjectWriter {
|
Ok(ObjectWriter {
|
||||||
writer: zwriter,
|
writer: zwriter,
|
||||||
digest,
|
digest,
|
||||||
|
size,
|
||||||
|
written_size: 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn finish(mut self) -> Result<(ObjectId, W)> {
|
pub fn finish(mut self) -> Result<(ObjectId, W)> {
|
||||||
self.writer.try_finish()?;
|
self.writer.try_finish()?;
|
||||||
|
|
||||||
|
if self.written_size == self.size {
|
||||||
Ok((
|
Ok((
|
||||||
ObjectId::new(&self.digest.finalize()),
|
ObjectId::new(&self.digest.finalize()),
|
||||||
self.writer.finish()?,
|
self.writer.finish()?,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
Err(ErrorKind::MismatchingObjectSize(self.written_size, self.size).into())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -78,6 +88,7 @@ impl<W: Write, D: Digest> Write for ObjectWriter<W, D> {
|
|||||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||||
let size = self.writer.write(buf)?;
|
let size = self.writer.write(buf)?;
|
||||||
self.digest.update(&buf[..size]);
|
self.digest.update(&buf[..size]);
|
||||||
|
self.written_size += size as u64;
|
||||||
Ok(size)
|
Ok(size)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,6 +98,61 @@ impl<W: Write, D: Digest> Write for ObjectWriter<W, D> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub struct ObjectReader<R: Read> {
|
||||||
|
otype: ObjectType,
|
||||||
|
size: u64,
|
||||||
|
reader: GzDecoder<R>,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl<R: Read> ObjectReader<R> {
|
||||||
|
pub fn new(reader: R)
|
||||||
|
-> Result<ObjectReader<R>> {
|
||||||
|
|
||||||
|
let mut zreader = GzDecoder::new(reader);
|
||||||
|
|
||||||
|
let mut buffer = [0u8; 12];
|
||||||
|
zreader.read_exact(&mut buffer)?;
|
||||||
|
|
||||||
|
let otype = {
|
||||||
|
let mut otype = [0; 4];
|
||||||
|
otype.copy_from_slice(&buffer[0..4]);
|
||||||
|
otype
|
||||||
|
};
|
||||||
|
let size = {
|
||||||
|
let mut size_bytes = [0; 8];
|
||||||
|
size_bytes.copy_from_slice(&buffer[4..12]);
|
||||||
|
u64::from_be_bytes(size_bytes)
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(ObjectReader {
|
||||||
|
otype,
|
||||||
|
size,
|
||||||
|
reader: zreader,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn object_type(&self) -> ObjectType {
|
||||||
|
self.otype
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn size(&self) -> u64 {
|
||||||
|
self.size
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn close(self) -> Result<R> {
|
||||||
|
Ok(self.reader.into_inner())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl<R: Read> Read for ObjectReader<R> {
|
||||||
|
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||||
|
self.reader.read(buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn write_object<R, W>(reader: &mut R, writer: W,
|
pub fn write_object<R, W>(reader: &mut R, writer: W,
|
||||||
otype: &ObjectType, size: u64)
|
otype: &ObjectType, size: u64)
|
||||||
-> Result<(ObjectId, W)> where
|
-> Result<(ObjectId, W)> where
|
||||||
@@ -101,49 +167,23 @@ pub fn write_object<R, W>(reader: &mut R, writer: W,
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub struct ObjectReader<R: Read> {
|
pub trait WriteAsObject {
|
||||||
reader: GzDecoder<R>,
|
fn write_as_object<W: Write>(&mut self, writer: W, otype: &ObjectType) -> Result<ObjectId>;
|
||||||
}
|
|
||||||
|
|
||||||
|
fn object_id(&mut self, otype: &ObjectType) -> Result<ObjectId> {
|
||||||
impl<R: Read> ObjectReader<R> {
|
self.write_as_object(sink(), otype)
|
||||||
pub fn new(reader: R)
|
|
||||||
-> Result<(ObjectType, u64, ObjectReader<R>)> {
|
|
||||||
|
|
||||||
let mut zreader = GzDecoder::new(reader);
|
|
||||||
|
|
||||||
let mut buffer = [0u8; 12];
|
|
||||||
zreader.read_exact(&mut buffer)?;
|
|
||||||
|
|
||||||
let otype = {
|
|
||||||
let mut otype = [0; 4];
|
|
||||||
otype.copy_from_slice(&buffer[0..4]);
|
|
||||||
otype
|
|
||||||
};
|
|
||||||
let size = {
|
|
||||||
let mut size_bytes = [0; 8];
|
|
||||||
size_bytes.copy_from_slice(&buffer[4..12]);
|
|
||||||
u64::from_be_bytes(size_bytes)
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok((
|
|
||||||
otype,
|
|
||||||
size,
|
|
||||||
ObjectReader {
|
|
||||||
reader: zreader,
|
|
||||||
}
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn close(self) -> Result<R> {
|
|
||||||
Ok(self.reader.into_inner())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
impl<R: Read> Read for ObjectReader<R> {
|
impl<T: Read + Seek> WriteAsObject for T {
|
||||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
fn write_as_object<W: Write>(&mut self, writer: W, otype: &ObjectType) -> Result<ObjectId> {
|
||||||
self.reader.read(buf)
|
let start = self.seek(SeekFrom::Current(0))?;
|
||||||
|
let end = self.seek(SeekFrom::End(0))?;
|
||||||
|
self.seek(SeekFrom::Start(start))?;
|
||||||
|
|
||||||
|
let (oid, _) = write_object(self, writer, otype, end - start)?;
|
||||||
|
Ok(oid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,27 +192,58 @@ impl<R: Read> Read for ObjectReader<R> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
const PAYLOAD: &[u8; 12] = b"Hello World!";
|
||||||
|
const PAYLOAD_OID: &str = "c3b4032160b015b2261530532a6c49f2bdadbe0687fb1f5a6a32e083";
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn object_read_write() {
|
fn object_read_write() -> Result<()> {
|
||||||
use std::io::{Cursor, Seek, SeekFrom};
|
use std::io::{Cursor, Seek, SeekFrom};
|
||||||
|
|
||||||
let payload = b"Hello World!";
|
let mut source = Cursor::new(PAYLOAD);
|
||||||
let mut source = Cursor::new(payload);
|
|
||||||
let mut fake_file = Cursor::new(vec![]);
|
let mut fake_file = Cursor::new(vec![]);
|
||||||
let mut output = vec![];
|
let mut output = vec![];
|
||||||
|
|
||||||
let (oid, _) = write_object(&mut source, &mut fake_file,
|
let (oid, _) = write_object(&mut source, &mut fake_file,
|
||||||
TYPE_BLOB, payload.len() as u64).unwrap();
|
OTYPE_BLOB, PAYLOAD.len() as u64)?;
|
||||||
|
|
||||||
assert_eq!(oid, ObjectId::from_str("c3b4032160b015b2261530532a6c49f2bdadbe0687fb1f5a6a32e083").unwrap());
|
assert_eq!(oid, ObjectId::from_str(PAYLOAD_OID)?);
|
||||||
|
|
||||||
fake_file.seek(SeekFrom::Start(0)).unwrap();
|
fake_file.seek(SeekFrom::Start(0))?;
|
||||||
let (otype, size, mut reader) = ObjectReader::new(fake_file).unwrap();
|
let mut reader = ObjectReader::new(fake_file)?;
|
||||||
let read_size = reader.read_to_end(&mut output).unwrap();
|
let read_size = reader.read_to_end(&mut output)?;
|
||||||
|
|
||||||
assert_eq!(otype, *TYPE_BLOB);
|
assert_eq!(reader.object_type(), *OTYPE_BLOB);
|
||||||
assert_eq!(size, payload.len() as u64);
|
assert_eq!(reader.size(), PAYLOAD.len() as u64);
|
||||||
assert_eq!(read_size, payload.len());
|
assert_eq!(read_size, PAYLOAD.len());
|
||||||
assert_eq!(output, payload);
|
assert_eq!(output, PAYLOAD);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn object_write_invalid_size() {
|
||||||
|
let mut source = PAYLOAD.as_ref();
|
||||||
|
let fake_file = vec![];
|
||||||
|
|
||||||
|
let result = write_object(&mut source, fake_file, OTYPE_BLOB, 13);
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Err(Error(ErrorKind::MismatchingObjectSize(actual, expected), _))
|
||||||
|
if actual == 12 && expected == 13 => (),
|
||||||
|
Err(error) => panic!("Unexpected error: {:?}", error),
|
||||||
|
Ok(_) => panic!("Unexpected success"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn write_as_object() -> Result<()> {
|
||||||
|
use std::io::{Cursor, sink};
|
||||||
|
|
||||||
|
let mut source = Cursor::new(PAYLOAD);
|
||||||
|
let oid = source.write_as_object(sink(), OTYPE_BLOB)?;
|
||||||
|
|
||||||
|
assert_eq!(oid, ObjectId::from_str(PAYLOAD_OID)?);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,13 +14,25 @@
|
|||||||
// along with cdb. If not, see <https://www.gnu.org/licenses/>.
|
// along with cdb. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
extern crate tempfile;
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::io::{Read, Seek};
|
||||||
|
use std::fs::{File, OpenOptions, create_dir, create_dir_all};
|
||||||
|
|
||||||
|
use tempfile::{NamedTempFile};
|
||||||
|
|
||||||
use crate::core::error::*;
|
use crate::core::error::*;
|
||||||
use crate::core::ObjectId;
|
use crate::core::ObjectId;
|
||||||
|
|
||||||
|
use super::object::{
|
||||||
|
OTYPE_BLOB,
|
||||||
|
ObjectReader, ObjectType, WriteAsObject,
|
||||||
|
};
|
||||||
|
|
||||||
const OBJECTS_FOLDER: &str = "objects";
|
|
||||||
|
const OBJECTS_DIR: &str = "objects";
|
||||||
|
const TMP_DIR: &str = "tmp";
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -37,6 +49,14 @@ impl SimpleDb {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn setup(&self) -> Result<()> {
|
||||||
|
create_dir(self.objects_dir())?;
|
||||||
|
create_dir(self.tmp_dir())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn has_object(&self, oid: &ObjectId) -> bool {
|
pub fn has_object(&self, oid: &ObjectId) -> bool {
|
||||||
let obj_path = self.path_from_id(oid);
|
let obj_path = self.path_from_id(oid);
|
||||||
if let Ok(metadata) = std::fs::metadata(obj_path) {
|
if let Ok(metadata) = std::fs::metadata(obj_path) {
|
||||||
@@ -47,16 +67,63 @@ impl SimpleDb {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn store_object<R: Read + Seek>(&self, otype: &ObjectType, mut reader: R)
|
||||||
|
-> Result<ObjectId> {
|
||||||
|
let mut tmp_file = self.create_tmp_file()?;
|
||||||
|
let oid = reader.write_as_object(&mut tmp_file, otype)?;
|
||||||
|
let dst_file_path = self.path_from_id(&oid);
|
||||||
|
|
||||||
|
// TODO: Check if dst_file_path exists
|
||||||
|
|
||||||
|
create_dir_all(dst_file_path.parent().unwrap())?;
|
||||||
|
match tmp_file.persist(dst_file_path) {
|
||||||
|
Ok(file) => {
|
||||||
|
file.sync_data()?;
|
||||||
|
Ok(oid)
|
||||||
|
},
|
||||||
|
Err(err) => Err(err.error.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn store_file_as_blob(&self, file_path: &Path) -> Result<ObjectId> {
|
||||||
|
let file = OpenOptions::new().read(true).open(file_path)?;
|
||||||
|
self.store_object(OTYPE_BLOB, file)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_object(&self, oid: &ObjectId) -> Result<ObjectReader<File>> {
|
||||||
|
let path = self.path_from_id(oid);
|
||||||
|
let file = OpenOptions::new().read(true).open(path)?;
|
||||||
|
ObjectReader::new(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn objects_dir(&self) -> PathBuf {
|
||||||
|
let mut path = self.path.clone();
|
||||||
|
path.push(OBJECTS_DIR);
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tmp_dir(&self) -> PathBuf {
|
||||||
|
let mut path = self.path.clone();
|
||||||
|
path.push(TMP_DIR);
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
fn path_from_id(&self, oid: &ObjectId) -> PathBuf {
|
fn path_from_id(&self, oid: &ObjectId) -> PathBuf {
|
||||||
let oid_str = oid.to_string();
|
let oid_str = oid.to_string();
|
||||||
let mut path = self.path.clone();
|
let mut path = self.path.clone();
|
||||||
|
|
||||||
path.push(OBJECTS_FOLDER);
|
path.push(OBJECTS_DIR);
|
||||||
path.push(oid_str.get(..4).unwrap()); // unwrap is ok here because we know oid_str is a
|
path.push(oid_str.get(..4).unwrap()); // unwrap is ok here because we know oid_str is a
|
||||||
path.push(oid_str.get(4..).unwrap()); // hexadecimal number (i.e. ascii only).
|
path.push(oid_str.get(4..).unwrap()); // hexadecimal number (i.e. ascii only).
|
||||||
|
|
||||||
path
|
path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_tmp_file(&self) -> Result<NamedTempFile> {
|
||||||
|
let tmp_dir = self.tmp_dir();
|
||||||
|
|
||||||
|
Ok(tempfile::Builder::new().tempfile_in(tmp_dir)?)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,23 +18,72 @@ extern crate tempfile;
|
|||||||
extern crate libbsv;
|
extern crate libbsv;
|
||||||
|
|
||||||
|
|
||||||
|
use std::path::{PathBuf};
|
||||||
|
use std::io::{Cursor, Read};
|
||||||
|
use std::fs::{create_dir_all, write};
|
||||||
|
|
||||||
|
use tempfile::{TempDir};
|
||||||
|
|
||||||
|
use libbsv::{
|
||||||
|
Result,
|
||||||
|
ObjectId,
|
||||||
|
simple_db::{
|
||||||
|
OTYPE_BLOB,
|
||||||
|
SimpleDb,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const PAYLOAD: &[u8; 12] = b"Hello World!";
|
||||||
|
const PAYLOAD_OID: &str = "c3b4032160b015b2261530532a6c49f2bdadbe0687fb1f5a6a32e083";
|
||||||
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn simple_db_has_object() {
|
fn simple_db_has_object() {
|
||||||
let temp_dir = tempfile::TempDir::new().unwrap();
|
let temp_dir = TempDir::new().unwrap();
|
||||||
let db = libbsv::SimpleDb::new(temp_dir.path()).unwrap();
|
let db = SimpleDb::new(temp_dir.path()).unwrap();
|
||||||
let oid = libbsv::ObjectId::from_str("0001020304050607").unwrap();
|
db.setup().unwrap();
|
||||||
|
|
||||||
|
let oid = ObjectId::from_str("0001020304050607").unwrap();
|
||||||
|
|
||||||
assert!(!db.has_object(&oid));
|
assert!(!db.has_object(&oid));
|
||||||
|
|
||||||
let mut object_path: std::path::PathBuf = temp_dir.path().into();
|
let mut object_path: PathBuf = temp_dir.path().into();
|
||||||
object_path.push("objects");
|
object_path.push("objects");
|
||||||
object_path.push("0001");
|
object_path.push("0001");
|
||||||
|
|
||||||
std::fs::create_dir_all(&object_path).unwrap();
|
create_dir_all(&object_path).unwrap();
|
||||||
|
|
||||||
object_path.push("020304050607");
|
object_path.push("020304050607");
|
||||||
|
|
||||||
std::fs::write(object_path, "test").unwrap();
|
write(object_path, "test").unwrap();
|
||||||
|
|
||||||
assert!(db.has_object(&oid));
|
assert!(db.has_object(&oid));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn simple_db_store_read() -> Result<()> {
|
||||||
|
let expected_oid = ObjectId::from_str(PAYLOAD_OID)?;
|
||||||
|
let temp_dir = TempDir::new()?;
|
||||||
|
let db = SimpleDb::new(temp_dir.path())?;
|
||||||
|
db.setup()?;
|
||||||
|
|
||||||
|
assert!(!db.has_object(&expected_oid));
|
||||||
|
|
||||||
|
let oid = db.store_object(OTYPE_BLOB, Cursor::new(PAYLOAD))?;
|
||||||
|
|
||||||
|
assert!(db.has_object(&expected_oid));
|
||||||
|
assert_eq!(oid, expected_oid);
|
||||||
|
|
||||||
|
let mut reader = db.read_object(&oid)?;
|
||||||
|
|
||||||
|
assert_eq!(reader.object_type(), *OTYPE_BLOB);
|
||||||
|
assert_eq!(reader.size(), PAYLOAD.len() as u64);
|
||||||
|
|
||||||
|
let mut data = vec![];
|
||||||
|
reader.read_to_end(&mut data)?;
|
||||||
|
|
||||||
|
assert_eq!(data, *PAYLOAD);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user